feat: wire lobby task and VIP prefab flows and sync UI assets
This commit is contained in:
@@ -4,7 +4,7 @@ import {readFileSync} from 'node:fs';
|
||||
import {runInNewContext} from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
|
||||
test('wire logs correctly label both directions and preserve transmitted frames',()=>{
|
||||
test('wire frame logging stays muted and preserves transmitted frames',()=>{
|
||||
for(const debugIO of [false,true]){
|
||||
const logs:unknown[][]=[],sent:string[]=[],received:string[]=[];
|
||||
let socket:any;
|
||||
@@ -21,14 +21,9 @@ test('wire logs correctly label both directions and preserve transmitted frames'
|
||||
transport.send(frame);socket.onmessage({data:frame});socket.onmessage({data:'{"com":"@serverheartbeat"}'});
|
||||
assert.deepEqual(sent,[frame]);assert.equal(received[0],frame);
|
||||
const frames=logs.filter(args=>String(args[0]).includes('包 '));
|
||||
assert.equal(frames.length,debugIO?3:0);
|
||||
if(debugIO){
|
||||
assert.equal(frames[0][0],'[发包 C→S] agent/player_login');
|
||||
assert.equal(frames[1][0],'[收包 S→C] agent/player_login');
|
||||
assert.equal(frames[2][0],'[收包 S→C] @serverheartbeat');
|
||||
assert.equal(typeof frames[0][2],'object');
|
||||
}
|
||||
else assert.equal(logs.length,0);
|
||||
assert.equal(frames.length,0);
|
||||
assert.equal(received[1],'{"com":"@serverheartbeat"}');
|
||||
if(!debugIO)assert.equal(logs.length,0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openLobbyUrl } from '../../YouleNexus/assets/framework/adapters/native/lobby-links.ts';
|
||||
import { openLobbyUrl, resolveLobbyLaunchMode } from '../../YouleNexus/assets/framework/adapters/native/lobby-links.ts';
|
||||
test('launch mode uses original query or injected bridge field and rejects invalid values',()=>{
|
||||
assert.equal(resolveLobbyLaunchMode('',false,undefined),0);
|
||||
assert.equal(resolveLobbyLaunchMode('?Launchtype=1',false,0),1);
|
||||
assert.equal(resolveLobbyLaunchMode('?Launchtype=0',true,'1'),1);
|
||||
assert.throws(()=>resolveLobbyLaunchMode('',true,undefined),/Launchtype/);
|
||||
assert.throws(()=>resolveLobbyLaunchMode('?Launchtype=bad',false,undefined),/Launchtype/);
|
||||
});
|
||||
test('announcement links preserve fixUrl and distinct native timestamp conventions',()=>{
|
||||
const calls:unknown[]=[];
|
||||
openLobbyUrl('https://example.test/?a=1?b=2',{kind:'settings',browser:u=>calls.push(u)},()=>123);
|
||||
|
||||
@@ -59,6 +59,50 @@ function makeCommands(store: PlatformStore): {
|
||||
};
|
||||
}
|
||||
|
||||
test('VIP query and both entry forms use the exact agent protocol',()=>{
|
||||
const store=makeStore('outside'),{commands,sent}=makeCommands(store);
|
||||
commands.getVipRooms(['123456']);commands.joinVipRoom('123456',null);commands.joinVipRoom(900001,{city:'测试'});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'getInfoByShortCode',data:{agentid:'agent-from-config',gameid:'game-from-config',shortcodeList:['123456']}});
|
||||
assert.deepEqual(sent[1],{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:'123456',joinmode:0,location:null,ip:store.getState().players.entities[430511].ip}});
|
||||
assert.equal((sent[2].data as {roomcode:number}).roomcode,900001);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getVipRooms([]),/logged-in/);
|
||||
});
|
||||
|
||||
test('task requests preserve legacy identity fields and reject unclaimable entries',()=>{
|
||||
const store=makeStore('outside');const {commands,sent}=makeCommands(store);
|
||||
commands.getPlayerTasks();
|
||||
store.applyTaskList({tasks:[{taskid:7,title:'任务',award:1,finish:1,total:1,state:1}]});
|
||||
commands.claimTask(7);
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_player_task',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config'}},
|
||||
{app:'youle',route:'agent',rpc:'get_task_award',data:{agentid:'agent-from-config',playerid:430511,taskid:7}},
|
||||
]);
|
||||
store.applyTaskAward({taskid:7,taskstate:0});
|
||||
assert.equal(store.getState().tasks?.[0].state,2);
|
||||
assert.equal(store.getState().players.entities[430511].taskstate,0);
|
||||
assert.throws(()=>commands.claimTask(7),/claimable/);
|
||||
const before=store.getState();
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{taskid:7}]}));
|
||||
assert.equal(store.getState(),before);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerTasks(),/logged-in/);
|
||||
});
|
||||
|
||||
test('task numeric strings normalize at ingestion and invalid values never commit',()=>{
|
||||
const store=makeStore('outside');
|
||||
const task={taskid:'7',title:'任务',award:'3',finish:'2',total:'5',state:'0'};
|
||||
store.applyTaskList({tasks:[task]});
|
||||
assert.deepEqual(store.getState().tasks,[{...task,award:3,finish:2,total:5,state:0}]);
|
||||
assert.equal(task.award,'3');
|
||||
const before=store.getState();
|
||||
for(const field of ['award','finish','total','state']){
|
||||
for(const value of ['', ' ', 'abc', 'Infinity', null, true, [], {}, undefined]){
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{...task,[field]:value}]}),new RegExp(field));
|
||||
assert.equal(store.getState(),before);
|
||||
}
|
||||
}
|
||||
assert.throws(()=>store.applyTaskList({tasks:[{...task,state:'1.5'}]}),/state/);
|
||||
});
|
||||
|
||||
test('prepare and exitRoom use the first-slice builders with authoritative config and room state', () => {
|
||||
const { commands, sent } = makeCommands(makeStore('inside', 0));
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ test('applyLoginSuccess commits the complete login once and notifies once', () =
|
||||
sex: 0,
|
||||
bean: 0,
|
||||
ip: '127.0.0.1',
|
||||
roomcard: 3, taskstate: 1, score: 0, sign: null,
|
||||
roomcard: 3, taskstate: 1, advanced: 0, score: 0, sign: null,
|
||||
});
|
||||
assert.deepEqual(state.room, { kind: 'outside' });
|
||||
});
|
||||
|
||||
@@ -310,6 +310,39 @@ function makeRuntime(config: RuntimeConfig = runtimeConfig(), entry = makeGameEn
|
||||
return { runtime, wire, scene, clock, factoryConfigs, config };
|
||||
}
|
||||
|
||||
test('VIP wire query response reaches canonical state and clears on login replacement',async()=>{
|
||||
const {runtime,wire}=makeRuntime();await runtime.start();
|
||||
wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT);
|
||||
const login=()=>wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}});
|
||||
login();runtime.getVipRooms(['123456']);
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'getInfoByShortCode',data:{roomInfo:[{shortCode:123456,owner:'房主',playerList:[]}]}}});
|
||||
assert.equal(runtime.state.vipRooms?.[0].shortCode,123456);
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'getInfoByShortCode',data:{roomInfo:[]}}});
|
||||
assert.deepEqual(runtime.state.vipRooms,[]);
|
||||
login();assert.equal(runtime.state.vipRooms,undefined);runtime.stop();
|
||||
});
|
||||
|
||||
test('task wire responses reach the store and claim does not invent reward balances',async()=>{
|
||||
const {runtime,wire}=makeRuntime();await runtime.start();
|
||||
wire.emit({type:'open',server:'ws://agent'});runtime.login(ACCOUNT);
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'player_login',data:fixture('player-login-success.json')}});
|
||||
runtime.getPlayerTasks();
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'get_player_task',data:{tasks:[{taskid:7,title:'任务',award:'3',finish:'1',total:'1',state:'1'}]}}});
|
||||
assert.equal(runtime.state.tasks?.[0].award,3);
|
||||
const balance=runtime.state.players.entities[430511].roomcard;
|
||||
runtime.claimTask(7);
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'get_task_award',data:{taskid:'7',taskstate:0}}});
|
||||
assert.equal(runtime.state.tasks?.[0].state,2);
|
||||
assert.equal(runtime.state.players.entities[430511].taskstate,0);
|
||||
assert.equal(runtime.state.players.entities[430511].roomcard,balance);
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'update_roomcard',data:{roomcard:25}}});
|
||||
assert.equal(runtime.state.players.entities[430511].roomcard,25);
|
||||
wire.emit({type:'message',message:{route:'agent',rpc:'player_finish_task',data:{state:1}}});
|
||||
assert.equal(runtime.state.players.entities[430511].taskstate,1);
|
||||
wire.emit({type:'slow'});assert.throws(()=>runtime.getPlayerTasks(),/logged-in/);
|
||||
runtime.stop();
|
||||
});
|
||||
|
||||
test('UI observes canonical commits and unsubscribe is idempotent', async () => {
|
||||
const { runtime, wire } = makeRuntime();
|
||||
const seen: unknown[] = [];
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||
import { resolveLobbyMenuConfig, lobbyMenuState, lobbyTrayState, LOBBY_MENU_ACTIONS, LOBBY_UNAVAILABLE } from '../../YouleNexus/assets/framework/presentation/lobby-menu.ts';
|
||||
|
||||
const identity={agentid:'a',gameid:'g',channelid:'c',marketid:'m',version:1};
|
||||
const remote=(data:Record<string,unknown>)=>createRemoteConfig(data,identity);
|
||||
const source=readFileSync(new URL('../../../projects/Game_Surface_3/js/00_Surface/11_GameUI.js',import.meta.url),'utf8');
|
||||
const ids=[11,12,13,14,17,7,234,483,568,286,287,10,508,496,509,3233,3232,3256,3264,76,3139,679,99,3120,3161,3162,3016,3018];
|
||||
function original(config:ReturnType<typeof resolveLobbyMenuConfig>,versionState:number,launchMode:number) {
|
||||
const visible:Record<number,boolean>=Object.fromEntries(ids.map(id=>[id,true]));
|
||||
const left:Record<number,number>={};
|
||||
const noop=()=>{};
|
||||
const ui=new Proxy({onstate:[],isprepare:[]} as Record<string,any>,{get:(o,k)=>k in o?o[k as string]:noop});
|
||||
const context=vm.createContext({GameUI:ui,GameData:{...config,Scene:1,versionState,LaunchMode:launchMode,
|
||||
hallConfig:config.hall,sysConfig:config.sys,isLocation:true,submitPhoneInfo:true,submitAddressBook:true},
|
||||
Game_Config:{sysConfig:config.template,Max:{},Share:{}},C_Player:{},ConstVal:{isGameHall:config.template.isGameHall,
|
||||
Launch_mode:{from_self:0,from_hall:1},sysNotice:{}},Logic:new Proxy({},{get:()=>noop}),Func:new Proxy({},{get:()=>noop}),
|
||||
Utl:new Proxy({},{get:()=>noop}),Game_Modify:{},gameHallImport:{jumpMenuScene:noop},
|
||||
set_self:(id:number,key:number,value:number)=>{if(key===37)visible[id]=Boolean(value);if(key===18)left[id]=value;},
|
||||
get_self:(id:number,key:number)=>key===37?Number(visible[id]):0,set_group:noop,shareLink:noop,isArray:Array.isArray});
|
||||
for(const name of ['JumpMenuScene','adaptSmartBar','adaptSmartBar2','updateActBtn','showStoreBtn']){
|
||||
const start=source.indexOf(`GameUI.${name} = function`);
|
||||
assert.ok(start>=0,name);
|
||||
const end=source.indexOf('\nGameUI.',start+1);
|
||||
vm.runInContext(source.slice(start,end),context);
|
||||
}
|
||||
ui.JumpMenuScene();
|
||||
return {visible,left};
|
||||
}
|
||||
|
||||
test('menu visibility and visible-only spacing match original JumpMenuScene across configuration combinations',()=>{
|
||||
for(const versionState of [0,1] as const)for(const isGameHall of [false,true])for(const agentmode of [1,2,3])
|
||||
for(const androidPay of [0,1])for(const flags of [0,1,2,3,4,7]){
|
||||
const config=resolveLobbyMenuConfig(remote({agentmode,androidPay,rankList:flags&1,hideTaskBtn:flags&1,
|
||||
hideWxShare:flags&2,miniPro:flags&4,hideRoomCard:flags&2?2:1,cdKey:flags&4?2:1,hall_config:{actData:flags&1?['',0,'','']:0}}),
|
||||
{isGameHall,mainMenuButton:isGameHall,hideNotice:isGameHall,vipInfinite:false});
|
||||
for(const launchMode of [0,1] as const){
|
||||
const expected=original(config,versionState,launchMode);
|
||||
const actual=lobbyMenuState(config,{versionState,launchMode});
|
||||
for(const id of ids){
|
||||
assert.equal(actual.visible[id]??true,expected.visible[id],JSON.stringify({id,versionState,isGameHall,agentmode,androidPay,flags}));
|
||||
assert.equal(actual.left[id],expected.left[id],`spacing ${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
test('source hall merge preserves scoped top-level override and rejects invalid data',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({hall_config:{androidPay:1,rankList:2},rankList:1}));
|
||||
assert.equal(config.hall.androidPay,1);assert.equal(config.hall.rankList,1);
|
||||
assert.throws(()=>resolveLobbyMenuConfig(remote({hall_config:'bad'})),/hall_config/);
|
||||
assert.throws(()=>resolveLobbyMenuConfig(remote({agentmode:'bad'})),/agentmode/);
|
||||
});
|
||||
test('legacy hallConfigName actData colon typo leaves activity sourced only from hall_config',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({actData:['top'],hall_config:{actData:['hall']}}));
|
||||
assert.deepEqual(config.hall.actData,['hall']);
|
||||
assert.equal(resolveLobbyMenuConfig(remote({actData:['top']})).hall.actData,0);
|
||||
});
|
||||
test('source truthiness is preserved for string zero flags rather than coerced to numeric zero',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({hideTaskBtn:'0',hideWxShare:'0',rankList:'0'}));
|
||||
const actual=lobbyMenuState(config,{versionState:0,launchMode:0});
|
||||
const expected=original(config,0,0);
|
||||
for(const id of [17,14,3233])assert.equal(actual.visible[id],expected.visible[id]);
|
||||
assert.equal(actual.visible[17],false);assert.equal(actual.visible[3233],true);
|
||||
});
|
||||
test('main menu override hides all four entries and cdKey runs after review restriction',()=>{
|
||||
const config=resolveLobbyMenuConfig(remote({cdKey:2}),{isGameHall:false,mainMenuButton:true,hideNotice:false,vipInfinite:true});
|
||||
const normal=lobbyMenuState(config,{versionState:0,launchMode:0});
|
||||
for(const id of [11,12,483,568,3139,679])assert.equal(normal.visible[id],false);
|
||||
assert.equal(lobbyMenuState(config,{versionState:1,launchMode:0}).visible[3264],true);
|
||||
});
|
||||
test('tray excludes hidden entries from placement and keeps original feedback hidden',()=>{
|
||||
const state=lobbyTrayState(resolveLobbyMenuConfig(remote({agentmode:2,rankList:1,bagUrl:'bag',wareHouse:1,miniPro:1})),{versionState:0,launchMode:0});
|
||||
assert.equal(state.visible[235],false);assert.equal(state.visible[15],false);
|
||||
assert.deepEqual([state.left[604],state.left[3017],state.left[3057],state.left[5]],[216,371,526,681]);
|
||||
});
|
||||
test('every menu entry has an existing local action or an explicit unavailable business description',()=>{
|
||||
const local=['settings','profile','join-room','notice','tray','tasks','share','vip-rooms'];
|
||||
for(const action of Object.values(LOBBY_MENU_ACTIONS))assert.ok(local.includes(action)||LOBBY_UNAVAILABLE[action],action);
|
||||
for(const id of [10,286,287,3161] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-roomcard');
|
||||
for(const id of [508,509,496,3162] as const)assert.equal(LOBBY_MENU_ACTIONS[id],'buy-bean');
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRemoteConfig } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||
import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||||
import { resolveSharePolicy,sharePayload } from '../../YouleNexus/assets/framework/presentation/share-model.ts';
|
||||
test('share reads url_download and preserves original friend and timeline payloads',()=>{
|
||||
const identity={agentid:'a',channelid:'c',gameid:'g',marketid:4,version:1};
|
||||
const remoteConfig=createRemoteConfig({url_download:'https://example.test/app',sharename:'标题',hall_config:{shareDesc:'描述',shareType:1,shareImgUrl:'https://example.test/share.png'}},identity);
|
||||
const policy=resolveSharePolicy({identity,remoteConfig} as RuntimeConfig);
|
||||
assert.equal(policy.url,'https://example.test/app?agentid=a&channelid=c&gameid=g&marketid=4');
|
||||
assert.deepEqual(sharePayload(policy,1),{sharefriend:1,type:1,webpageUrl:policy.url,title:'标题',description:'描述',sharetype:0});
|
||||
assert.deepEqual(sharePayload(policy,2),{sharefriend:2,type:3,webpageUrl:'https://example.test/share.png',title:'标题描述',description:undefined,sharetype:0});
|
||||
assert.equal(sharePayload({...policy,imageTimeline:false},2),null);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { taskItemModel } from '../../YouleNexus/assets/framework/presentation/task-item-model.ts';
|
||||
const task={taskid:7,title:'一二三四五六七八九',award:3,finish:2,total:5,state:0};
|
||||
test('task item preserves title wrapping, reward wording and incomplete progress',()=>{
|
||||
const model=taskItemModel(task,0,'房卡');
|
||||
assert.equal(model.title,'一二三四五六七八\n九');
|
||||
assert.equal(model.reward,'奖励房卡,数量:3');
|
||||
assert.equal(model.progress,0.4);assert.equal(model.progressText,'2/5');
|
||||
assert.equal(model.showProgress,true);assert.equal(model.showDo,true);assert.equal(model.showClaim,false);assert.equal(model.showClaimed,false);
|
||||
assert.equal(taskItemModel(task,1,'房卡').showDo,false);
|
||||
});
|
||||
test('claimable and claimed states replace progress and action controls',()=>{
|
||||
for(const state of [1,2]){
|
||||
const model=taskItemModel({...task,state},0,'金币');
|
||||
assert.equal(model.showProgress,false);assert.equal(model.showDo,false);
|
||||
assert.equal(model.showClaim,state===1);assert.equal(model.showClaimed,state===2);
|
||||
}
|
||||
});
|
||||
test('incomplete tasks reject invalid totals instead of inventing progress',()=>{
|
||||
assert.throws(()=>taskItemModel({...task,total:0},0,'房卡'),/total/);
|
||||
assert.throws(()=>taskItemModel({...task,finish:NaN},0,'房卡'),/finish/);
|
||||
assert.throws(()=>{
|
||||
// @ts-expect-error Exercise malformed data at the runtime boundary.
|
||||
taskItemModel({...task,title:undefined},0,'房卡');
|
||||
},/title/);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {parseVipRooms} from '../../YouleNexus/assets/framework/protocol/contracts/vip-room.ts';
|
||||
import {orderedVipRooms,occupiedSeats} from '../../YouleNexus/assets/framework/presentation/vip-room-model.ts';
|
||||
import {VipRoomHistory} from '../../YouleNexus/assets/framework/adapters/native/vip-room-history.ts';
|
||||
import {PlatformStore} from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
|
||||
test('VIP response follows saved short codes and counts occupied seats, without mutating source',()=>{
|
||||
const one={shortCode:'200001',owner:'房主',playerList:[[0,2,null,4]],roomcodes:{roomtype:[1,2],roomlist:['800001']},deskState:[1],seatCount:[4]};
|
||||
const two={shortCode:'200002',destwo:'另一间',owner:123456,playerList:[]};
|
||||
const rooms=parseVipRooms({roomInfo:[one,two]});
|
||||
assert.deepEqual(orderedVipRooms(rooms,['200002','missing','200001']).map(r=>r.shortCode),['200002','200001']);
|
||||
assert.equal(rooms[0].title,'vip房间');assert.equal(rooms[1].roomcodes,null);
|
||||
assert.equal(orderedVipRooms(rooms,[200001])[0],rooms[0]);
|
||||
assert.equal(occupiedSeats(rooms[0].playerList[0]),2);
|
||||
assert.deepEqual(one.deskState,[1]);
|
||||
const store=new PlatformStore();store.applyVipRooms({roomInfo:[one]});const before=store.getState();
|
||||
assert.throws(()=>store.applyVipRooms({roomInfo:[{...one,seatCount:[]}]}),/matching lengths/);
|
||||
assert.equal(store.getState(),before);
|
||||
store.applyVipRooms({roomInfo:[]});assert.deepEqual(store.getState().vipRooms,[]);
|
||||
});
|
||||
test('VIP history preserves legacy storage key and most recent first ordering',()=>{
|
||||
const data=new Map<string,string>();const h=new VipRoomHistory({getItem:k=>data.get(k)??null,setItem:(k,v)=>{data.set(k,v);}},'g','a');
|
||||
assert.deepEqual(h.read(),[]);assert.equal(data.get('tsgame_vipRoom_ga'),'{"data":[]}');
|
||||
h.add('1');h.add('2');h.add('1');assert.deepEqual(h.read(),['1','2']);
|
||||
data.set('tsgame_vipRoom_ga','{"data":false}');assert.throws(()=>h.read(),/Invalid/);
|
||||
});
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
|
||||
test('FIRST_SLICE_RPC_ROUTE 是第一纵向切片唯一且精确的 rpc route 表', () => {
|
||||
assert.deepEqual(FIRST_SLICE_RPC_ROUTE, {
|
||||
getInfoByShortCode: 'agent',
|
||||
get_player_task: 'agent',
|
||||
get_task_award: 'agent',
|
||||
update_roomcard: 'agent',
|
||||
player_finish_task: 'agent',
|
||||
player_login: 'agent',
|
||||
self_join_room: 'agent',
|
||||
player_prepare: 'room',
|
||||
|
||||
Reference in New Issue
Block a user