feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
@@ -0,0 +1,96 @@
/** Run in Cocos Game View. The actual socket is stopped before all local fixtures. */
export async function verifyRoomEntryPreview(e,loginFixture){
const checks=[],assert=(ok,text)=>{if(!ok)throw Error(text);checks.push(text);};
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const load=id=>new Promise((r,j)=>e.assetManager.loadAny(id,(err,a)=>err?j(err):r(a)));
const exported=name=>Array.from(System.entries()).map(([,m])=>m).find(m=>m[name])?.[name];
const startup=e.director.getScene().getComponentInChildren('PlatformStartup');
assert(startup,'persistent Startup loaded');startup.unsubscribe?.();startup.unsubscribe=null;startup.runtime.stop();startup.panel?.dispose();startup.panel=null;
const canvas=e.director.getScene().getChildByName('Canvas');
let node=null;const tips=[],sent=[];let cancel=0;
const mount=async id=>{if(node){node.destroy();await sleep(50);}node=e.instantiate(await load(id));node.parent=canvas;node.setPosition(0,0,0);const w=node.getComponent(e.Widget);if(w){w.target=canvas;w.updateAlignment();}await sleep(100);return node;};
let pointer=30;
const click=async n=>{const camera=canvas.getComponent(e.Canvas).cameraComponent,p=camera.worldToScreen(n.worldPosition);const touch=new e.Touch(p.x,p.y,pointer++);
e.input._dispatchEventTouch(new e.EventTouch([touch],false,e.Input.EventType.TOUCH_START,[touch]));
e.input._dispatchEventTouch(new e.EventTouch([touch],false,e.Input.EventType.TOUCH_END,[touch]));await sleep(25);};
const keys=['tsgame_roomtype_','tsgame_shortcode_'].map(k=>k+startup.config.identity.gameid+startup.config.identity.agentid);
const old=keys.map(k=>localStorage.getItem(k));
let runtime=null;
try{
await mount('98e42432-0731-4b9e-a158-72032b2c157d');const join=node.getComponent('JoinRoomView');
assert(join&&join.slots.every(s=>!!s.font),'join prefab has wired BitmapFont slots');
join.open({initialCode:'',submit:n=>{sent.push(n);join.setBusy(true);},cancel:()=>cancel++,paste:async()=> '房号123-456789',reportError:t=>tips.push(t)});
for(const d of [0,1,2,3,4,5])await click(join.digitButtons[d].node);
assert(sent.length===1&&sent[0]===12345,'coordinate keypad submits numeric code on sixth digit');
assert(join.slots.map(s=>s.string).join('')==='012345','join slots render all six digits including leading zero');
await click(join.submitButton.node);assert(sent.length===1,'busy submit does not duplicate request');
await click(join.cancelButtons[1].node);assert(cancel===1,'close callback works while busy');
join.setBusy(false);await click(join.deleteButton.node);assert(join.slots.filter(s=>s.node.active).length===5,'delete hides final slot');
await click(join.clearButton.node);assert(join.slots.every(s=>!s.node.active),'clear hides all slots');
await click(join.pasteButton.node);assert(sent[1]===123456,'clipboard strips separators and truncates to six digits');
let finishPaste;const late=[];
join.open({initialCode:'',submit:n=>late.push(n),cancel(){},paste:()=>new Promise(r=>finishPaste=r),reportError:t=>tips.push(t)});
await click(join.pasteButton.node);
const panel=Object.create(exported('LobbyPanel').prototype);panel.roomPage=join;panel.modal=node;panel.modalKind='join';panel.modalReleases=[];
panel.cancelRankRequest=panel.cancelInviteRequest=panel.cancelTaskRequest=()=>{};
panel.cancelRoomPage();node=null;finishPaste('123456');await sleep(40);
assert(late.length===0&&panel.roomPage===null,'disconnect closes room page and invalidates deferred clipboard auto-submit');
await mount('584dbb85-4acc-4bea-9d9b-e0cbeb8741da');const create=node.getComponent('CreateRoomPage');
assert(create&&create.constructor.name==='TemplateCreateRoomView','platform resolves subgame through base page component');
const packets=[];create.open({previousRoomtype:null,submit:r=>{packets.push(r);create.setBusy(true);},cancel:()=>cancel++,reportError:t=>tips.push(t)});
assert(create.options[0].isChecked&&create.options[2].isChecked,'template initial options match source');
await click(create.options[1].node);await click(create.options[3].node);
assert(!create.options[0].isChecked&&create.options[1].isChecked&&!create.options[2].isChecked&&create.options[3].isChecked,'Cocos ToggleContainer enforces each option group');
await click(create.options[1].node);assert(create.options[1].isChecked,'clicking the selected option keeps it selected like the source');
await click(create.submitButton.node);await click(create.submitButton.node);
const roomtype=[1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]];
assert(packets.length===1&&JSON.stringify(packets[0])===JSON.stringify(roomtype),'create page submits source fixed array once');
create.close();create.open({previousRoomtype:roomtype,submit:()=>{},cancel:()=>{},reportError:t=>tips.push(t)});
assert(!create.options[2].isChecked&&!create.options[3].isChecked,'saved template index 4 does not invent a matching option');
node.destroy();node=null;await sleep(50);
// Exercise the real runtime + GameEntry + Startup scene callback with a local wire.
const game=exported('createTemplateGame')(startup.config.identity.gameid),listeners=new Set(),wire={sent:[],subscribe:fn=>{listeners.add(fn);return()=>listeners.delete(fn);},start(){},stop(){},send(p){this.sent.push(p);},switchServer(){},reconnectCurrent(){},emit(ev){for(const fn of [...listeners])fn(ev);}};
startup.game=game;startup.loginResponse=null;let roomPromise=Promise.resolve(),lobbies=0;const fatal=[];
runtime=new (exported('PlatformRuntime'))({gameEntry:game.entry,resolveRuntimeConfig:async()=>startup.config,createWireClient:()=>wire,
scene:{showLoading(){},showLogin(){},showLobby(){lobbies++;},showRoom(){roomPromise=startup.showRoom();},showReconnect(){startup.endRoomRequest();},showTips:t=>tips.push(t),showServerDenial(){startup.endRoomRequest();},showKicked(){},showFatal:error=>fatal.push(String(error))},
loadResources:async()=>{},waitForMinimumDisplay:async()=>{},getLoginDeviceSnapshot:()=>({ip:'127.0.0.1',location:null,machineid:'preview',machineroom:'',deviceLogin:{enabled:false},loginPlayerId:{enabled:false}})});
startup.runtime=runtime;await runtime.start();wire.emit({type:'open'});
runtime.login({openid:'o1',unionid:'test_unionid',nickname:'测试号',avatar:'',sex:0,province:'江西',city:'南昌'});
wire.emit({type:'message',message:{...loginFixture,data:{...loginFixture.data,avatar:''}}});
assert(runtime.state.room.kind==='outside'&&lobbies===1,'local login commits lobby state');
const originalTimer=window.setTimeout;let expire;
try{window.setTimeout=(fn,ms,...args)=>ms===15000?(expire=fn,999999):originalTimer(fn,ms,...args);
startup.requestRoom('create_room',()=>{});
}finally{window.setTimeout=originalTimer;}
const originalTip=startup.tip;startup.tip=t=>tips.push(t);expire();startup.tip=originalTip;
assert(startup.roomPending===null&&tips.pop()==='房间请求超时,请重试','room timeout releases guard and reports retry');
startup.requestRoom('create_room',()=>runtime.createRoom({roomtype,ip:'127.0.0.1',location:null}));
startup.requestRoom('create_room',()=>{throw Error('duplicate request invoked');});
assert(wire.sent.filter(p=>p.rpc==='create_room').length===1,'Startup suppresses duplicate room requests');
wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:{state:7,showerror:1,error:'测试拒绝'}}});
assert(runtime.state.room.kind==='outside'&&startup.roomPending===null,'denial releases wait and keeps lobby');
startup.requestRoom('create_room',()=>runtime.createRoom({roomtype,ip:'127.0.0.1',location:null}));
const created={state:0,roomcode:'654321',seat:0,roomtype,makewar:0,asetcount:10};startup.roomReceipt={rpc:'create_room',data:created};
wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:created}});await roomPromise;await sleep(100);
assert(fatal.length===0&&runtime.state.room.kind==='inside'&&e.director.getScene().name==='TemplateRoom','create success crosses real Startup scene load');
const roomRoot=e.director.getScene().getChildByName('Canvas').getChildByName('TemplateRoom'),roomView=roomRoot.getComponent('TemplateRoomView');
assert(roomView.roomLabel.string.includes('654321')&&roomView.playersLabel.string.includes('测试号'),'room view consumes module snapshot');
wire.emit({type:'message',message:{route:'agent',rpc:'create_room',data:created}});await sleep(50);
assert(e.isValid(roomRoot),'duplicate successful reply does not remount room');
roomView.prepareButton.node.emit(e.Button.EventType.CLICK);assert(wire.sent.at(-1).rpc==='player_prepare','subgame prepare travels through GameHost');
wire.emit({type:'message',message:{route:'room',rpc:'player_prepare',data:{seat:0}}});
roomView.exitButton.node.emit(e.Button.EventType.CLICK);assert(wire.sent.at(-1).rpc==='self_exit_room','subgame exit travels through GameHost');
wire.emit({type:'message',message:{route:'room',rpc:'self_exit_room',data:{seat:0}}});
assert(runtime.state.room.kind==='outside'&&lobbies===2&&roomView.actions===null,'exit clears canonical room and disposes view binding');
startup.requestRoom('self_join_room',()=>runtime.joinRoom({roomcode:123456,ip:'127.0.0.1',location:null}));
assert(wire.sent.at(-1).data.roomcode===123456,'manual join preserves numeric wire roomcode');
const players=Array(10).fill(null);players[2]={...loginFixture.data,avatar:'',isprepare:0,onstate:0};
const deskinfo={round:2,cards:[1,2,3]};
wire.emit({type:'message',message:{route:'agent',rpc:'self_join_room',data:{state:0,roomcode:'123456',seat:2,roomtype,players,deskinfo,deskwar:1,needprepare:1,infinite:0}}});
await roomPromise;await sleep(80);
assert(fatal.length===0&&runtime.state.room.selfSeat===2&&game.restoredSnapshot===deskinfo,'join and reconnect snapshot reach template module unchanged');
assert(e.director.getScene().getChildByName('Canvas').getChildByName('TemplateRoom').getComponent('TemplateRoomView').roomLabel.string.includes('123456'),'join success binds its own room scene');
assert(tips.length===0&&fatal.length===0,'no unexpected fixture UI errors');
return {checks,count:checks.length};
}finally{node?.destroy();runtime?.stop();startup.endRoomRequest();keys.forEach((key,i)=>old[i]===null?localStorage.removeItem(key):localStorage.setItem(key,old[i]));}
}