Skip to content

Commit c04f0e5

Browse files
committed
Fix guild invite flow without historical guild create
1 parent 7b0adef commit c04f0e5

4 files changed

Lines changed: 182 additions & 1 deletion

File tree

app/js/engine/guild.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,57 @@ var GuildSystem = (function() {
9292
wars: [],
9393
quests: [],
9494
announcements: [],
95-
totalDelegated: 0 // Sum of all member delegations (integer, micro-SHARES)
95+
totalDelegated: 0, // Sum of all member delegations (integer, micro-SHARES)
96+
isPlaceholder: false
9697
};
9798
}
9899

100+
/**
101+
* Ensure a guild shell exists even when this client has not replayed the
102+
* original guild.create yet (e.g. fresh browser with a short sync window).
103+
* This keeps invite/join flows functional instead of dropping newer actions.
104+
*
105+
* @param {Object<string, Object>} guilds
106+
* @param {string} guildId
107+
* @param {string} founderAccount
108+
* @param {number} blockNum
109+
* @returns {Object|null}
110+
*/
111+
function ensureGuildShell(guilds, guildId, founderAccount, blockNum) {
112+
if (!guilds || !guildId) return null;
113+
if (guilds[guildId]) return guilds[guildId];
114+
115+
var normalizedTag = String(guildId).replace(/[^a-z0-9]/gi, '').toUpperCase();
116+
if (normalizedTag.length < 2) normalizedTag = 'GM';
117+
normalizedTag = normalizedTag.slice(0, 5);
118+
119+
guilds[guildId] = {
120+
id: guildId,
121+
name: guildId,
122+
tag: normalizedTag,
123+
school: null,
124+
motto: '',
125+
charter: {
126+
tithe_pct: DEFAULT_CHARTER.tithe_pct,
127+
membership: MEMBERSHIP.INVITE,
128+
min_shares: DEFAULT_CHARTER.min_shares
129+
},
130+
founder: founderAccount || '',
131+
createdBlock: blockNum | 0,
132+
level: 1,
133+
xp: 0,
134+
members: {},
135+
invites: {},
136+
wars: [],
137+
quests: [],
138+
announcements: [],
139+
totalDelegated: 0,
140+
isPlaceholder: true
141+
};
142+
143+
return guilds[guildId];
144+
}
145+
99146
/**
100147
* Create initial members object with founder
101148
*/
@@ -483,6 +530,7 @@ var GuildSystem = (function() {
483530
WAR_STATE: WAR_STATE,
484531
DEFAULT_CHARTER: DEFAULT_CHARTER,
485532
createGuild: createGuild,
533+
ensureGuildShell: ensureGuildShell,
486534
addInvite: addInvite,
487535
joinGuild: joinGuild,
488536
leaveGuild: leaveGuild,

app/js/engine/state-engine.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,14 +638,34 @@ var StateEngine = (function() {
638638

639639
function _handleGuildInvite(sender, data, blockNum) {
640640
var guild = worldState.guilds[data.guild_id];
641+
if (!guild && typeof GuildSystem.ensureGuildShell === 'function') {
642+
guild = GuildSystem.ensureGuildShell(worldState.guilds, data.guild_id, sender, blockNum);
643+
}
641644
if (!guild) return [];
645+
646+
if (guild.isPlaceholder && !guild.members[sender]) {
647+
guild.members[sender] = {
648+
account: sender,
649+
rank: GuildSystem.RANKS.FOUNDER,
650+
joinedBlock: blockNum,
651+
delegatedShares: 0,
652+
pvpWins: 0,
653+
pvpLosses: 0,
654+
questContributions: 0
655+
};
656+
if (!guild.founder) guild.founder = sender;
657+
}
658+
642659
var ok = GuildSystem.addInvite(guild, sender, data.target, blockNum);
643660
if (!ok) return [];
644661
return [{ type: 'guild_invite', guildId: data.guild_id, inviter: sender, target: data.target }];
645662
}
646663

647664
function _handleGuildAccept(sender, data, blockNum) {
648665
var guild = worldState.guilds[data.guild_id];
666+
if (!guild && typeof GuildSystem.ensureGuildShell === 'function') {
667+
guild = GuildSystem.ensureGuildShell(worldState.guilds, data.guild_id, '', blockNum);
668+
}
649669
if (!guild) return [];
650670
var ok = GuildSystem.joinGuild(guild, sender, blockNum);
651671
if (!ok) return [];
@@ -654,6 +674,9 @@ var StateEngine = (function() {
654674

655675
function _handleGuildLeave(sender, data, blockNum) {
656676
var guild = worldState.guilds[data.guild_id];
677+
if (!guild && typeof GuildSystem.ensureGuildShell === 'function') {
678+
guild = GuildSystem.ensureGuildShell(worldState.guilds, data.guild_id, '', blockNum);
679+
}
657680
if (!guild) return [];
658681
var ok = GuildSystem.leaveGuild(guild, sender);
659682
if (!ok) return [];
@@ -662,7 +685,36 @@ var StateEngine = (function() {
662685

663686
function _handleGuildPromote(sender, data, blockNum) {
664687
var guild = worldState.guilds[data.guild_id];
688+
if (!guild && typeof GuildSystem.ensureGuildShell === 'function') {
689+
guild = GuildSystem.ensureGuildShell(worldState.guilds, data.guild_id, sender, blockNum);
690+
}
665691
if (!guild) return [];
692+
693+
if (guild.isPlaceholder && !guild.members[sender]) {
694+
guild.members[sender] = {
695+
account: sender,
696+
rank: GuildSystem.RANKS.FOUNDER,
697+
joinedBlock: blockNum,
698+
delegatedShares: 0,
699+
pvpWins: 0,
700+
pvpLosses: 0,
701+
questContributions: 0
702+
};
703+
if (!guild.founder) guild.founder = sender;
704+
}
705+
706+
if (guild.isPlaceholder && data.target && !guild.members[data.target]) {
707+
guild.members[data.target] = {
708+
account: data.target,
709+
rank: GuildSystem.RANKS.INITIATE,
710+
joinedBlock: blockNum,
711+
delegatedShares: 0,
712+
pvpWins: 0,
713+
pvpLosses: 0,
714+
questContributions: 0
715+
};
716+
}
717+
666718
var ok = GuildSystem.promoteRank(guild, sender, data.target, data.rank);
667719
if (!ok) return [];
668720
return [{ type: 'guild_promoted', guildId: data.guild_id, promoter: sender, target: data.target, rank: data.rank }];

app/js/ui/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ var App = (function() {
383383
quests: true,
384384
map: true,
385385
chronicle: true,
386+
guild: true,
386387
marketplace: true,
387388
leaderboard: true
388389
};

tests/guild-invite-shell.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const assert = require('assert');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const vm = require('vm');
5+
6+
const root = path.resolve(__dirname, '..');
7+
8+
function loadScript(ctx, relativePath) {
9+
const code = fs.readFileSync(path.join(root, relativePath), 'utf8');
10+
vm.runInContext(code, ctx, { filename: relativePath });
11+
}
12+
13+
const context = vm.createContext({
14+
console,
15+
Date,
16+
Math,
17+
setTimeout,
18+
clearTimeout,
19+
VizMagicConfig: null,
20+
GuildSystem: null,
21+
ActionValidator: null,
22+
CheckpointSystem: {
23+
init(callback) { callback(null); },
24+
loadLatestCheckpoint(account, callback) { callback(null, null); },
25+
saveCheckpoint(account, block, state, callback) { callback(null); }
26+
},
27+
QuestSystem: undefined,
28+
TerritorySystem: undefined,
29+
CharacterSystem: undefined,
30+
InventorySystem: undefined,
31+
MarketplaceSystem: undefined,
32+
LociSystem: undefined,
33+
WorldBossSystem: undefined,
34+
SocialSystem: undefined,
35+
DuelStateManager: undefined
36+
});
37+
context.window = context;
38+
39+
loadScript(context, 'app/js/config.js');
40+
loadScript(context, 'app/js/engine/guild.js');
41+
loadScript(context, 'app/js/engine/validator.js');
42+
loadScript(context, 'app/js/engine/state-engine.js');
43+
44+
const AT = context.VizMagicConfig.ACTION_TYPES;
45+
const StateEngine = context.StateEngine;
46+
47+
function processAction(sender, type, data, blockNum) {
48+
return StateEngine.processBlock({
49+
vmActions: [{ sender, action: { type, data: data || {} } }],
50+
voicePosts: [],
51+
veEvents: [],
52+
awards: [],
53+
blockHash: 'block-' + blockNum,
54+
blockNum,
55+
timestamp: ''
56+
});
57+
}
58+
59+
const inviteEvents = processAction('guildleader', AT.GUILD_INVITE, {
60+
guild_id: 'ancients',
61+
target: 'targetmage'
62+
}, 101);
63+
64+
const stateAfterInvite = StateEngine.getState();
65+
assert.strictEqual(inviteEvents.length, 1, 'invite event should be emitted');
66+
assert.ok(stateAfterInvite.guilds.ancients, 'placeholder guild should be materialized');
67+
assert.strictEqual(stateAfterInvite.guilds.ancients.isPlaceholder, true, 'guild should be marked as placeholder');
68+
assert.ok(stateAfterInvite.guilds.ancients.invites.targetmage, 'target invite should be stored');
69+
assert.ok(stateAfterInvite.guilds.ancients.members.guildleader, 'inviter should be added as placeholder founder');
70+
71+
const joinEvents = processAction('targetmage', AT.GUILD_ACCEPT, {
72+
guild_id: 'ancients'
73+
}, 102);
74+
75+
const stateAfterJoin = StateEngine.getState();
76+
assert.strictEqual(joinEvents.length, 1, 'join event should be emitted');
77+
assert.ok(stateAfterJoin.guilds.ancients.members.targetmage, 'target should be able to join placeholder guild');
78+
assert.strictEqual(stateAfterJoin.guilds.ancients.invites.targetmage, undefined, 'invite should be consumed on join');
79+
80+
console.log('guild invite shell flow: ok');

0 commit comments

Comments
 (0)