feat: migrate platform login scenes and native login flows

This commit is contained in:
2026-09-06 09:06:00 +08:00
parent f2a948f059
commit 695d3be25c
140 changed files with 7599 additions and 4397 deletions
@@ -63,8 +63,9 @@ export function assertLocalStartupContext(context: LocalStartupContext): void {
if (!startupConfig.useLocalServer) fail('useLocalServer', 'source must be true');
if (config.mode !== 'debug') fail('mode', 'must be debug');
if (config.isDebugger !== true) fail('isDebugger', 'must be true');
if (config.source !== 'remote') fail('source', 'must be remote');
if (config.gameserver !== startupConfig.gameserver) fail('gameserver', 'must equal startupConfig remote entry');
if (config.source !== startupConfig.configSource) fail('source', 'must equal startupConfig.configSource');
const configEntry = startupConfig.configSource === 'local' ? startupConfig.localConfigResource : startupConfig.gameserver;
if (config.gameserver !== configEntry) fail('gameserver', 'must equal startupConfig selected entry');
if (!config.remoteConfig || typeof config.remoteConfig.getValue !== 'function') fail('remoteConfig', 'must provide memory view');
if (!config.rawConfig || config.rawConfig !== config.remoteConfig.raw) fail('rawConfig', 'must share remoteConfig.raw');
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "1d920343-a60b-4b40-8eba-f29b4a7d53fa",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -13,6 +13,10 @@ export interface DebugAccount {
export interface StartupConfig {
readonly mode: RuntimeMode;
/** 配置内容来源;local 仅允许 debug,独立于 WebSocket 服务器选择。 */
readonly configSource: 'remote' | 'local';
/** resources 下的 txt 路径,不含扩展名。 */
readonly localConfigResource: string;
readonly gameserver: string;
readonly useLocalServer: boolean;
readonly localServers: readonly string[];
@@ -21,7 +25,10 @@ export interface StartupConfig {
/** 唯一发布配置入口;mode 与服务器选择互相独立。 */
export const STARTUP_CONFIG: Readonly<StartupConfig> = Object.freeze({
mode: 'debug',
gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt',
// 开发时改为 local,编辑 assets/resources/dev-config/update_jsonv2.txt 后重新预览。
configSource: 'local',
localConfigResource: 'dev-config/update_jsonv2',
gameserver: 'https://tsgames.daoqi88.cn/config_test/update_jsonv2.txt',
useLocalServer: true,
localServers: Object.freeze(['ws://127.0.0.1:3088']),
});
@@ -46,6 +53,11 @@ export function validateLocalServer(value: string, field = 'localServers'): void
export function validateStartupConfig(source: Readonly<StartupConfig>): Readonly<StartupConfig> {
if (!source || typeof source !== 'object') invalid('source', 'must be an object');
const mode = resolveRuntimeMode(source.mode);
if (source.configSource !== 'remote' && source.configSource !== 'local') invalid('configSource', 'must be remote or local');
if (source.configSource === 'local' && mode !== 'debug') invalid('configSource', 'local is only allowed in debug mode');
if (typeof source.localConfigResource !== 'string' || !/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(source.localConfigResource)) {
invalid('localConfigResource', 'must be a resources path without extension or traversal');
}
if (typeof source.useLocalServer !== 'boolean') invalid('useLocalServer', 'must be boolean');
if (typeof source.gameserver !== 'string' || source.gameserver.trim() !== source.gameserver) invalid('gameserver', 'must be http(s) URL');
let url: URL;
@@ -53,7 +65,7 @@ export function validateStartupConfig(source: Readonly<StartupConfig>): Readonly
if (url.protocol !== 'http:' && url.protocol !== 'https:') invalid('gameserver', 'must be http(s) URL');
if (!Array.isArray(source.localServers) || source.localServers.length === 0) invalid('localServers', 'must be a nonempty array');
for (let index = 0; index < source.localServers.length; index++) validateLocalServer(source.localServers[index], 'localServers[' + index + ']');
return Object.freeze({mode, gameserver:source.gameserver, useLocalServer:source.useLocalServer, localServers:Object.freeze([...source.localServers])});
return Object.freeze({mode, configSource:source.configSource, localConfigResource:source.localConfigResource, gameserver:source.gameserver, useLocalServer:source.useLocalServer, localServers:Object.freeze([...source.localServers])});
}
/** 忠实旧 min_replaceAll 链:- → /,# → :,再包成 http://...txt。 */
@@ -51,29 +51,52 @@ function validateHierarchy(scope: ConfigObject, depth: number): void {
for (const entry of list) validateHierarchy(requireObject(entry, listName), depth + 1);
}
/** Preserve first loose identity match, truthy overrides and whole-object replacement. */
/**
* Port of Game_Surface_3/12_Logic.js get_paravalue. Only GameData globals are
* replaced with the resolved identity argument; lookup order and truthiness stay exact.
* createRemoteConfig validates the downloaded JSON before exposing this reader.
*/
export function get_paravalue(
o_config: { readonly data?: Readonly<ConfigObject> | null },
paraname: string,
identity: Readonly<ChannelIdentity>,
): unknown {
const {agentid, gameid, channelid, marketid} = identity;
let paravalue: unknown = null;
const do_get_para = (arylist: unknown, keyname: string, keyvalue: string | number): ConfigObject | null => {
if (arylist && keyname && keyvalue) {
const list = arylist as ConfigObject[];
for (let i = 0; i < list.length; i++) {
if (list[i][keyname] == keyvalue) {
if (list[i][paraname]) paravalue = list[i][paraname];
return list[i];
}
}
}
return null;
};
if (!o_config.data) return paravalue;
if (o_config.data[paraname]) paravalue = o_config.data[paraname];
const _o_agent = do_get_para(o_config.data.agentlist, 'agentid', agentid);
if (!_o_agent) return paravalue;
const _o_game = do_get_para(_o_agent.gamelist, 'gameid', gameid);
if (!_o_game) return paravalue;
const _o_channel = do_get_para(_o_game.channellist, 'channelid', channelid);
if (!_o_channel) return paravalue;
const _o_market = do_get_para(_o_channel.marketlist, 'marketid', marketid);
if (!_o_market) return paravalue;
return paravalue;
}
/** All consumers share get_paravalue; no separate hierarchy or override rules. */
export function createRemoteConfig(input: unknown, identity: Readonly<ChannelIdentity>): RemoteConfig {
const raw = requireObject(input, 'root');
validateHierarchy(raw, 0);
const scopes = [raw];
let scope = raw;
for (const [listName, key] of SCOPES) {
const list = scope[listName] as ConfigObject[] | undefined;
// Legacy do_get_para skips any falsy identity, including numeric marketid=0.
if (!list || !identity[key]) break;
const match = list.find(entry => entry[key] == identity[key]);
if (!match) break;
scopes.push(match);
scope = match;
}
const wrapper = {data: raw};
return Object.freeze({
raw,
getValue(name: string): unknown {
let value: unknown = null;
for (const current of scopes) {
if (Object.prototype.hasOwnProperty.call(current, name) && current[name]) value = current[name];
}
return value;
return get_paravalue(wrapper, name, identity);
},
});
}
@@ -32,7 +32,8 @@ export interface RuntimeConfig {
readonly isDebugger: boolean;
readonly identity: Readonly<ChannelIdentity>;
readonly servers: readonly string[];
readonly source: 'remote';
readonly source: 'remote' | 'local';
/** 实际读取入口:remote 为最终 URL,local 为 resources 资源路径。 */
readonly gameserver: string;
readonly rawConfig: RemoteConfig['raw'];
readonly remoteConfig: RemoteConfig;
@@ -44,6 +45,8 @@ export interface ResolveRuntimeConfigOptions {
readonly win: NativeHost;
readonly search: string;
readonly fetcher: ConfigFetcher;
/** 由引擎层注入项目 txt 读取器;仅 local 模式需要,缺失直接报错。 */
readonly localFetcher?: ConfigFetcher;
readonly cacheBust?: () => string;
}
@@ -78,17 +81,21 @@ export async function resolveRuntimeConfig(
const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig);
const identity = resolveHostIdentity(options);
const gameserver = resolveGameServer({
// 先选择唯一来源;本地模式不读取或应用宿主的远程 gameconfig 覆盖。
const local = startupConfig.configSource === 'local';
const gameserver = local ? startupConfig.localConfigResource : resolveGameServer({
configured: startupConfig.gameserver,
injectedGameConfig: resolveInjectedGameConfig(options),
});
const fetcher = local ? options.localFetcher : options.fetcher;
if (!fetcher) throw new ConfigFetchError('本地配置缺少 localFetcher');
let raw: unknown;
try {
raw = await options.fetcher.fetch(gameserver, options.cacheBust);
raw = await fetcher.fetch(gameserver, options.cacheBust);
} catch (error) {
if (error instanceof ConfigFetchError || error instanceof ConfigParseError) throw error;
throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`);
throw new ConfigFetchError(`${local ? '本地配置读取失败' : '远程配置抓取失败'}: ${gameserver}`);
}
const remoteConfig = createRemoteConfig(raw, identity);
const servers = startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig);
@@ -99,7 +106,7 @@ export async function resolveRuntimeConfig(
isDebugger: startupConfig.mode === 'debug',
identity,
servers,
source: 'remote',
source: startupConfig.configSource,
gameserver,
rawConfig: remoteConfig.raw,
remoteConfig,
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4e90f0c8-b271-40a4-9909-68aa8f7d5974",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -8,8 +8,9 @@ export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
* gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。
*/
export const BUILD_IDENTITY: ChannelIdentity = {
agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv',
channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK',
// This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel.
agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy',
channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',
gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl',
marketid: 4,
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号)