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,25 @@
/** Game_Surface_3 Logic.InputRoomCode/pasteRoomCode and GameUI Spirit602. */
export class JoinRoomInput {
private value='';
private busy=false;
constructor(private readonly submit:(code:number)=>void){}
get code():string{return this.value;}
setBusy(value:boolean):void{this.busy=value;}
prefill(value:string):void{
if(!/^\d{0,6}$/.test(value))throw Error('Invalid saved room shortcode');
this.value=value;
}
digit(value:number):void{
if(!Number.isInteger(value)||value<0||value>9)throw Error('Invalid room digit');
if(this.busy)return;
if(this.value.length<6)this.value+=value;
if(this.value.length===6)this.submit(Number(this.value));
}
remove():void{if(!this.busy)this.value=this.value.slice(0,-1);}
clear():void{if(!this.busy)this.value='';}
paste(value:string):void{
if(this.busy)return;this.value=value.replace(/[^0-9]/g,'').slice(0,6);
if(this.value.length===6)this.submit(Number(this.value));
}
confirm():void{if(!this.busy&&Number(this.value)>0)this.submit(Number(this.value));}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9301c0f4-8779-44f8-8d19-5f540b499e82",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a834856f-7a53-445b-b517-6588ad346e59",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,121 +1,131 @@
import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
}
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
});
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
const SYS_DEFAULTS=Object.freeze({hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,unknown>{
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>;
}
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
}
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[key]);
}
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
}
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
}
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
const rawAgent=remote.getValue('agentmode');
const agentmode=rawAgent==null?1:Number(rawAgent);
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template});
import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
}
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
});
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,isReSetOwnerNote:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
const SYS_DEFAULTS=Object.freeze({vipRankLimit:10,hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,unknown>{
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>;
}
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
}
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[key]);
}
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
}
if(!Number.isInteger(Number(sys.vipRankLimit))||Number(sys.vipRankLimit)<=0)throw Error('Invalid vipRankLimit');
sys.vipRankLimit=Number(sys.vipRankLimit);
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
}
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
const rawAgent=remote.getValue('agentmode');
const agentmode=rawAgent==null?1:Number(rawAgent);
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
// Original GameUI draw cases 568/483/3139: missing vipConfig means unopened;
// the earlier matchhtml condition is commented out in the source.
const vipConfig=remote.getValue('vipConfig');
const locked=Object.freeze({568:!hall.worldRoomList,483:!vipConfig,3139:!sys.ofcShortCode});
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template,locked});
}
export type LobbyMenuConfig=ReturnType<typeof resolveLobbyMenuConfig>;
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const {hall,sys,template,agentmode}=config;
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
hide([219,220]);
if(context.versionState===1){
if(context.launchMode===0)hide([234]);
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
}else{
if(agentmode===1)hide([10,509]);
visible[235]=false;
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
visible[3016]=Boolean(hall.storeUrl);
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
if(template.mainMenuButton)hide([11,12,568,483]);
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
if(sys.hideTaskBtn)hide([17]);
if(sys.miniPro||sys.hideWxShare)hide([14]);
if(!template.isGameHall){
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
}
visible[3233]=Boolean(hall.rankList);
visible[3256]=false; // Both original deviceLogin branches hide this entry.
if(template.isGameHall){
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
}
}
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
visible[3264]=sys.cdKey===2;
return {visible,left,top};
/** Original click messages intentionally use different comma characters. */
export function lobbyLockedTip(config:LobbyMenuConfig,action:'world-rooms'|'match'|'official-rooms'):string|null{
if(action==='world-rooms')return config.locked[568]?'暂未开放,敬请期待':null;
return config.locked[action==='match'?483:3139]?'暂未开放,敬请期待':null;
}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
const left:Record<number,number>={3121:200},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
return {visible,left,top};
}
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
export const LOBBY_MENU_ACTIONS=Object.freeze({
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
} as const);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
'buy-roomcard':'购买房卡:尚未接入支付模块','buy-bean':'购买星星:尚未接入支付模块',
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
activity:'活动:尚未接入活动身份参数与原生接口','my-rooms':'我的房间:尚未接入房间记录与进房',qrcode:'二维码:尚未接入分享模块',
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
rank:'排行:尚未接入排行榜', 'vip-rank':'VIP 排行:尚未接入排行榜','bind-phone':'绑定手机:尚未接入绑定流程',
cdkey:'兑换码:尚未接入兑换请求',bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
});
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const {hall,sys,template,agentmode}=config;
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
hide([219,220]);
if(context.versionState===1){
if(context.launchMode===0)hide([234]);
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
}else{
if(agentmode===1)hide([10,509]);
visible[235]=false;
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
visible[3016]=Boolean(hall.storeUrl);
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
if(template.mainMenuButton)hide([11,12,568,483]);
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
if(sys.hideTaskBtn)hide([17]);
if(sys.miniPro||sys.hideWxShare)hide([14]);
if(!template.isGameHall){
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
}
visible[3233]=Boolean(hall.rankList);
visible[3256]=false; // Both original deviceLogin branches hide this entry.
if(template.isGameHall){
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
}
}
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
visible[3264]=sys.cdKey===2;
return {visible,left,top};
}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
const left:Record<number,number>={3121:200},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
return {visible,left,top};
}
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
export const LOBBY_MENU_ACTIONS=Object.freeze({
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
} as const);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
activity:'活动:尚未接入活动身份参数与原生接口',
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
'bind-phone':'绑定手机:尚未接入绑定流程',
bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
});
@@ -1,66 +1,74 @@
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
export type LobbyModel=ReturnType<typeof selectLobbyModel>;
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
const cardName=remote.getValue('roomCardName');
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
/** Game_Config.Notice: speed 0.1 px/ms, 15 px per gblen byte. */
export function announcementMotion(text:string,width:number,elapsedSeconds:number){
if(!Number.isFinite(width)||width<=0)throw Error('Invalid announcement width');
if(!Number.isFinite(elapsedSeconds)||elapsedSeconds<0)throw Error('Invalid announcement elapsed time');
const textWidth=text.replace(/[^\x00-\xff]/g,'**').length*15;
const duration=(width+textWidth)/100;
return {visible:text.length>0,textWidth,duration,left:width-(elapsedSeconds%duration)*100};
}
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
const cardName=remote.getValue('roomCardName');
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
}
@@ -0,0 +1,24 @@
import {LEGACY_AUTH_DISTRICTS} from './legacy-auth-districts.ts';
/** Game_Surface_3 05_Func.IDValidator + 11_GameUI cases 484/493.
* Local format validation only. Deliberately retain the source's 15-digit,
* parent-district and month/day range semantics; this is not server identity proof.
*/
export function validateProfileAuth(name:string,id:string):'name'|'id'|null {
if(name.length<=1)return 'name';
const code=id.toUpperCase();
if(code.length!==15&&code.length!==18)return 'id';
const body=code.length===18?code.slice(0,17):code;
const address=body.slice(0,6);
const hasAddress=(key:string)=>Object.prototype.hasOwnProperty.call(LEGACY_AUTH_DISTRICTS,key);
if(!hasAddress(address)&&!hasAddress(address.slice(0,4)+'00')&&!hasAddress(address.slice(0,2)+'0000'))return 'id';
const birth=code.length===18?body.slice(6,14):body.slice(6,12);
const month=parseInt(birth.slice(-4,-2),10),day=parseInt(birth.slice(-2),10);
if(month>12||month===0||day>31||day===0)return 'id';
if(code.length===15)return null;
let sum=0;
for(let index=0;index<body.length;index++)sum+=parseInt(body[index],10)*(Math.pow(2,17-index)%11);
const value=12-sum%11;
const check=value===10?'X':String(value>10?value%11:value);
return check===code.slice(-1)?null:'id';
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "11f5830e-5f4b-46fb-9e06-b2b65edbed89",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,14 @@
export type LoadingRpc='get_player_task'|'get_task_award'|'getVipRankList';
/** Wire receipt, rather than a successful model update, completes request loading. */
export class RequestLoading {
private readonly pending=new Set<LoadingRpc>();
constructor(private readonly show:(visible:boolean)=>void){}
begin(rpc:LoadingRpc):void{const empty=this.pending.size===0;this.pending.add(rpc);if(empty)this.show(true);}
cancel(rpc:LoadingRpc):void{if(this.pending.delete(rpc)&&this.pending.size===0)this.show(false);}
received(message:{route:string;rpc:string;data?:unknown}):void{
if(message.route!=='agent')return;
if(message.rpc==='show_message'){this.clear();return;}
if(message.rpc==='get_player_task'||message.rpc==='get_task_award'||message.rpc==='getVipRankList')this.cancel(message.rpc);
}
clear():void{if(this.pending.size){this.pending.clear();this.show(false);}}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8bb6a294-bfcf-4126-98f1-0845b58c72db",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,19 +1,27 @@
import type { RuntimeConfig } from '../config/runtime-config.ts';
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
export function resolveSharePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
}
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
description:friend===1?policy.description:undefined,sharetype:0};
/** Open_ewm uses this exact config key; absent configuration intentionally does nothing. */
export function resolveQrCodePolicy(config:RuntimeConfig){
const url=config.remoteConfig.getValue('WechatPublicNo_ewm');
if(!url)return null;
if(typeof url!=='string')throw Error('Invalid WechatPublicNo_ewm');
const policy=resolveSharePolicy(config);
return {url,share:{sharefriend:1,type:3,webpageUrl:url,title:policy.title+policy.description,description:undefined,sharetype:0}};
}
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
export function resolveSharePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
}
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
description:friend===1?policy.description:undefined,sharetype:0};
}