mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Up to now `eslint-plugin-brace-rules` was used to enforce a common brace style for JavaScript code. This plugin was however updated the last time over 9 years ago and will be incompatible with ESLint v10, as that [removes `context.getSourceCode()`][1], the plugin relies on. To keep the eslint config working with ESLint v10, this replaces `eslint-plugin-brace-rules` with the [`@stylistic/brace-style`][2] rule from `@stylistic/eslint-plugin`, a package we already use. While `@stylistic/brace-style` doesn't offer an option to format braces in exactly the same way as before, the "allman" style seems to be the one closest to the existing code. [1]: https://eslint.org/blog/2025/11/eslint-v10.0.0-alpha.0-released/#removed-deprecated-rule-context-members [2]: https://eslint.style/rules/brace-style
539 lines
13 KiB
JavaScript
539 lines
13 KiB
JavaScript
/**
|
|
* Whether we are attempting to join or host a game.
|
|
*/
|
|
var g_IsConnecting = false;
|
|
|
|
/**
|
|
* "server" or "client"
|
|
*/
|
|
var g_GameType;
|
|
|
|
/**
|
|
* Server title shown in the lobby gamelist.
|
|
*/
|
|
var g_ServerName = "";
|
|
|
|
/**
|
|
* Identifier if server is using password.
|
|
*/
|
|
var g_ServerHasPassword = false;
|
|
|
|
var g_IsRejoining = false;
|
|
var g_PlayerAssignments; // used when rejoining
|
|
var g_UserRating;
|
|
|
|
const cancelTag = Symbol("cancelTag");
|
|
|
|
/**
|
|
* When the cancel button is pressed the returned promise will resolve to
|
|
* `cancelTag`. When the passed in promise resolves the returned promise will
|
|
* resolve to that result.
|
|
*/
|
|
function cancelOr(costumPromise)
|
|
{
|
|
return Promise.race([costumPromise, new Promise(resolve =>
|
|
{
|
|
Engine.GetGUIObjectByName("cancelButton").onPress = resolve.bind(undefined, cancelTag);
|
|
})]);
|
|
}
|
|
|
|
async function waitOnEvent(loadSavedGame, joinFromLobby)
|
|
{
|
|
while (true)
|
|
{
|
|
if (!joinFromLobby)
|
|
{
|
|
const continueResult = await cancelOr(new Promise(resolve =>
|
|
{
|
|
Engine.GetGUIObjectByName("continueButton").onPress = resolve;
|
|
}));
|
|
if (continueResult === cancelTag)
|
|
{
|
|
if (cancelSetup())
|
|
return;
|
|
continue;
|
|
}
|
|
try
|
|
{
|
|
confirmSetup(loadSavedGame);
|
|
}
|
|
catch(e)
|
|
{
|
|
if (cancelSetup())
|
|
return;
|
|
continue;
|
|
}
|
|
}
|
|
while (true)
|
|
{
|
|
const tickResult = await cancelOr(new Promise(resolve =>
|
|
{
|
|
Engine.GetGUIObjectByName("multiplayerPages").onTick = resolve;
|
|
}));
|
|
if (tickResult === cancelTag || await onTick(loadSavedGame))
|
|
break;
|
|
}
|
|
if (cancelSetup())
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function init(attribs)
|
|
{
|
|
g_UserRating = attribs.rating;
|
|
|
|
switch (attribs.multiplayerGameType)
|
|
{
|
|
case "join":
|
|
{
|
|
if (!Engine.HasXmppClient())
|
|
{
|
|
switchSetupPage("pageJoin");
|
|
break;
|
|
}
|
|
if (attribs.hasPassword)
|
|
{
|
|
g_ServerName = attribs.name;
|
|
switchSetupPage("pagePassword");
|
|
const passwordResult = await cancelOr(new Promise(resolve =>
|
|
{
|
|
Engine.GetGUIObjectByName("confirmPasswordButton").onPress = resolve;
|
|
}));
|
|
if (passwordResult === cancelTag)
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
startJoinFromLobby(attribs.name, attribs.hostJID,
|
|
attribs.hasPassword ? Engine.GetGUIObjectByName("clientPassword").caption : "");
|
|
switchSetupPage("pageConnecting");
|
|
}
|
|
catch(e)
|
|
{
|
|
if (cancelSetup())
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case "host":
|
|
{
|
|
const hasXmppClient = Engine.HasXmppClient();
|
|
Engine.GetGUIObjectByName("hostPasswordWrapper").hidden = !hasXmppClient;
|
|
if (hasXmppClient)
|
|
{
|
|
Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name;
|
|
Engine.GetGUIObjectByName("hostServerName").caption =
|
|
sprintf(translate("%(name)s's game"), { "name": attribs.name });
|
|
}
|
|
|
|
switchSetupPage("pageHost");
|
|
break;
|
|
}
|
|
default:
|
|
error("Unrecognized multiplayer game type: " + attribs.multiplayerGameType);
|
|
break;
|
|
}
|
|
|
|
await waitOnEvent(attribs.loadSavedGame,
|
|
attribs.multiplayerGameType === "join" && Engine.HasXmppClient());
|
|
}
|
|
|
|
function cancelSetup()
|
|
{
|
|
if (g_IsConnecting)
|
|
Engine.DisconnectNetworkGame();
|
|
|
|
if (Engine.HasXmppClient())
|
|
Engine.LobbySetPlayerPresence("available");
|
|
|
|
// Keep the page open if an attempt to join/host by ip failed
|
|
if (!g_IsConnecting || (Engine.HasXmppClient() && g_GameType == "client"))
|
|
return true;
|
|
|
|
g_IsConnecting = false;
|
|
Engine.GetGUIObjectByName("hostFeedback").caption = "";
|
|
|
|
if (g_GameType == "client")
|
|
switchSetupPage("pageJoin");
|
|
else if (g_GameType == "server")
|
|
switchSetupPage("pageHost");
|
|
else
|
|
error("cancelSetup: Unrecognized multiplayer game type: " + g_GameType);
|
|
return false;
|
|
}
|
|
|
|
function confirmSetup(loadSavedGame)
|
|
{
|
|
if (!Engine.GetGUIObjectByName("pageJoin").hidden)
|
|
{
|
|
const joinPlayerName = Engine.GetGUIObjectByName("joinPlayerName").caption;
|
|
const joinServer = Engine.GetGUIObjectByName("joinServer").caption;
|
|
const joinPort = Engine.GetGUIObjectByName("joinPort").caption;
|
|
|
|
startJoin(joinPlayerName, joinServer, getValidPort(joinPort));
|
|
switchSetupPage("pageConnecting");
|
|
return;
|
|
}
|
|
|
|
if (!Engine.GetGUIObjectByName("pageHost").hidden)
|
|
{
|
|
const hostServerName = Engine.GetGUIObjectByName("hostServerName").caption;
|
|
if (!hostServerName)
|
|
{
|
|
Engine.GetGUIObjectByName("hostFeedback").caption = translate("Please enter a valid server name.");
|
|
return;
|
|
}
|
|
|
|
const hostPort = Engine.GetGUIObjectByName("hostPort").caption;
|
|
if (getValidPort(hostPort) != +hostPort)
|
|
{
|
|
Engine.GetGUIObjectByName("hostFeedback").caption = sprintf(
|
|
translate("Server port number must be between %(min)s and %(max)s."), {
|
|
"min": g_ValidPorts.min,
|
|
"max": g_ValidPorts.max
|
|
});
|
|
return;
|
|
}
|
|
|
|
const hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption;
|
|
const hostPassword = Engine.GetGUIObjectByName("hostPassword").caption;
|
|
startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword, loadSavedGame);
|
|
switchSetupPage("pageConnecting");
|
|
return;
|
|
}
|
|
}
|
|
|
|
function startConnectionStatus(type)
|
|
{
|
|
g_GameType = type;
|
|
g_IsConnecting = true;
|
|
g_IsRejoining = false;
|
|
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Connecting to server...");
|
|
}
|
|
|
|
function onTick(loadSavedGame)
|
|
{
|
|
if (!g_IsConnecting)
|
|
return false;
|
|
|
|
return pollAndHandleNetworkClient(loadSavedGame);
|
|
}
|
|
|
|
function getConnectionFailReason(reason)
|
|
{
|
|
switch (reason)
|
|
{
|
|
case "not_server": return translate("Server is not running.");
|
|
case "invalid_password": return translate("Password is invalid.");
|
|
case "banned": return translate("You have been banned.");
|
|
case "local_ip_failed": return translate("Failed to get local IP of the server (it was assumed to be on the same network).");
|
|
default:
|
|
warn("Unknown connection failure reason: " + reason);
|
|
return sprintf(translate("\\[Invalid value %(reason)s]"), { "reason": reason });
|
|
}
|
|
}
|
|
|
|
function reportConnectionFail(reason)
|
|
{
|
|
messageBox(
|
|
400, 200,
|
|
(translate("Failed to connect to the server.")
|
|
) + "\n\n" + getConnectionFailReason(reason),
|
|
translate("Connection failed")
|
|
);
|
|
}
|
|
|
|
function pollAndHandleNetworkClient(loadSavedGame)
|
|
{
|
|
while (true)
|
|
{
|
|
var message = Engine.PollNetworkClient();
|
|
if (!message)
|
|
return false;
|
|
|
|
log(sprintf("Net message: %(message)s", { "message": uneval(message) }));
|
|
// If we're rejoining an active game, we don't want to actually display
|
|
// the game setup screen, so perform similar processing to gamesetup.js
|
|
// in this screen
|
|
if (g_IsRejoining)
|
|
{
|
|
switch (message.type)
|
|
{
|
|
case "serverdata":
|
|
switch (message.status)
|
|
{
|
|
case "failed":
|
|
reportConnectionFail(message.reason, false);
|
|
return true;
|
|
|
|
default:
|
|
error("Unrecognized netstatus type: " + message.status);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "netstatus":
|
|
switch (message.status)
|
|
{
|
|
case "disconnected":
|
|
if (message.reason === 16)
|
|
reportHandshakeDisconnect(message.mismatch_type, message.client_mismatch, message.server_mismatch);
|
|
else
|
|
reportDisconnect(message, false);
|
|
return true;
|
|
|
|
default:
|
|
error("Unrecognized netstatus type: " + message.status);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "players":
|
|
g_PlayerAssignments = message.newAssignments;
|
|
break;
|
|
|
|
case "start":
|
|
Engine.SwitchGuiPage("page_loading.xml", {
|
|
"attribs": message.initAttributes,
|
|
"isRejoining": g_IsRejoining,
|
|
"playerAssignments": g_PlayerAssignments
|
|
});
|
|
|
|
// Process further pending netmessages in the session page
|
|
return false;
|
|
|
|
case "chat":
|
|
break;
|
|
|
|
case "netwarn":
|
|
break;
|
|
|
|
default:
|
|
error("Unrecognized net message type: " + message.type);
|
|
}
|
|
}
|
|
else
|
|
// Not rejoining - just trying to connect to server.
|
|
{
|
|
switch (message.type)
|
|
{
|
|
case "serverdata":
|
|
switch (message.status)
|
|
{
|
|
case "failed":
|
|
reportConnectionFail(message.reason, false);
|
|
return true;
|
|
|
|
default:
|
|
error("Unrecognized netstatus type: " + message.status);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "netstatus":
|
|
switch (message.status)
|
|
{
|
|
case "connected":
|
|
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Registering with server...");
|
|
break;
|
|
|
|
case "authenticated":
|
|
return handleAuthenticated(message, loadSavedGame);
|
|
|
|
case "disconnected":
|
|
if (message.reason === 16)
|
|
reportHandshakeDisconnect(message.mismatch_type, message.client_mismatch_component, message.server_mismatch_component);
|
|
else
|
|
reportDisconnect(message, false);
|
|
return false;
|
|
|
|
default:
|
|
error("Unrecognized netstatus type: " + message.status);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "netwarn":
|
|
break;
|
|
|
|
default:
|
|
error("Unrecognized net message type: " + message.type);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleAuthenticated(message, loadSavedGame)
|
|
{
|
|
if (message.rejoining)
|
|
{
|
|
Engine.GetGUIObjectByName("connectionStatus").caption =
|
|
translate("Game has already started, rejoining...");
|
|
g_IsRejoining = true;
|
|
return false; // we'll process the game setup messages in the next tick
|
|
}
|
|
g_IsConnecting = false;
|
|
|
|
const savegameID = loadSavedGame ? await Engine.OpenChildPage("page_loadgame.xml") : undefined;
|
|
|
|
if (loadSavedGame && !savegameID)
|
|
{
|
|
Engine.DisconnectNetworkGame();
|
|
cancelSetup();
|
|
return true;
|
|
}
|
|
|
|
Engine.SwitchGuiPage("page_gamesetup.xml", {
|
|
"savedGame": savegameID, // Undefined or the savegame ID
|
|
"serverName": g_ServerName,
|
|
"hasPassword": g_ServerHasPassword
|
|
});
|
|
return false; // don't process any more messages - leave them for the game GUI loop
|
|
}
|
|
|
|
function switchSetupPage(newPage)
|
|
{
|
|
const multiplayerPages = Engine.GetGUIObjectByName("multiplayerPages");
|
|
for (const page of multiplayerPages.children)
|
|
if (page.name.startsWith("page"))
|
|
page.hidden = true;
|
|
|
|
if (newPage == "pageJoin" || newPage == "pageHost")
|
|
{
|
|
const halfHeight = newPage == "pageJoin" ? 145 : Engine.HasXmppClient() ? 140 : 125;
|
|
multiplayerPages.size.top = -halfHeight;
|
|
multiplayerPages.size.bottom = halfHeight;
|
|
}
|
|
else if (newPage == "pagePassword")
|
|
{
|
|
const halfHeight = 60;
|
|
multiplayerPages.size.top = -halfHeight;
|
|
multiplayerPages.size.bottom = halfHeight;
|
|
}
|
|
|
|
Engine.GetGUIObjectByName(newPage).hidden = false;
|
|
|
|
Engine.GetGUIObjectByName("hostPlayerNameWrapper").hidden = Engine.HasXmppClient();
|
|
Engine.GetGUIObjectByName("hostServerNameWrapper").hidden = !Engine.HasXmppClient();
|
|
|
|
Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting" || newPage == "pagePassword";
|
|
}
|
|
|
|
function startHost(playername, servername, port, password, loadSavedGame)
|
|
{
|
|
startConnectionStatus("server");
|
|
|
|
Engine.ConfigDB_CreateValue("user", "playername.multiplayer", playername);
|
|
Engine.ConfigDB_CreateValue("user", "multiplayerhosting.port", port);
|
|
Engine.ConfigDB_SaveChanges("user");
|
|
|
|
const hostFeedback = Engine.GetGUIObjectByName("hostFeedback");
|
|
|
|
// Disallow identically named games in the multiplayer lobby
|
|
if (Engine.HasXmppClient() &&
|
|
Engine.GetGameList().some(game => game.name == servername))
|
|
{
|
|
hostFeedback.caption = translate("Game name already in use.");
|
|
throw new Error();
|
|
}
|
|
|
|
try
|
|
{
|
|
Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port,
|
|
password, loadSavedGame, true);
|
|
}
|
|
catch(e)
|
|
{
|
|
messageBox(
|
|
400, 200,
|
|
sprintf(translate("Cannot host game: %(message)s."), { "message": e.message }),
|
|
translate("Error")
|
|
);
|
|
throw new Error();
|
|
}
|
|
|
|
g_ServerName = servername;
|
|
g_ServerHasPassword = !!password;
|
|
|
|
if (Engine.HasXmppClient())
|
|
Engine.LobbySetPlayerPresence("playing");
|
|
}
|
|
|
|
/**
|
|
* Connect via direct IP (used by the 'simple' MP screen)
|
|
*/
|
|
function startJoin(playername, ip, port)
|
|
{
|
|
try
|
|
{
|
|
Engine.StartNetworkJoin(playername, ip, port, true);
|
|
}
|
|
catch(e)
|
|
{
|
|
messageBox(
|
|
400, 200,
|
|
sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }),
|
|
translate("Error")
|
|
);
|
|
throw new Error();
|
|
}
|
|
|
|
startConnectionStatus("client");
|
|
|
|
// Future-proofing: there could be an XMPP client even if we join a game directly.
|
|
if (Engine.HasXmppClient())
|
|
Engine.LobbySetPlayerPresence("playing");
|
|
|
|
// Only save the player name and host address if they're valid.
|
|
Engine.ConfigDB_CreateValue("user", "playername.multiplayer", playername);
|
|
Engine.ConfigDB_CreateValue("user", "multiplayerserver", ip);
|
|
Engine.ConfigDB_CreateValue("user", "multiplayerjoining.port", port);
|
|
Engine.ConfigDB_SaveChanges("user");
|
|
}
|
|
|
|
/**
|
|
* Connect via the lobby.
|
|
*/
|
|
function startJoinFromLobby(playername, hostJID, password)
|
|
{
|
|
if (!Engine.HasXmppClient())
|
|
{
|
|
messageBox(
|
|
400, 200,
|
|
sprintf("You cannot join a lobby game without logging in to the lobby."),
|
|
translate("Error")
|
|
);
|
|
throw new Error();
|
|
}
|
|
|
|
try
|
|
{
|
|
Engine.StartNetworkJoinLobby(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), hostJID, password);
|
|
}
|
|
catch(e)
|
|
{
|
|
messageBox(
|
|
400, 200,
|
|
sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }),
|
|
translate("Error")
|
|
);
|
|
throw new Error();
|
|
}
|
|
|
|
startConnectionStatus("client");
|
|
|
|
Engine.LobbySetPlayerPresence("playing");
|
|
}
|
|
|
|
function getDefaultGameName()
|
|
{
|
|
return sprintf(translate("%(playername)s's game"), {
|
|
"playername": multiplayerName()
|
|
});
|
|
}
|
|
|
|
function getDefaultPassword()
|
|
{
|
|
return "";
|
|
}
|