Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 27191) +++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 27192) @@ -1,490 +1,489 @@ /** * @file This GUI page displays all available mods and allows the player to enabled and launch a set of compatible mods. */ /** * A mod is defined by a mod.json file, for example * { * "name": "0ad", * "version": "0.0.27", * "label": "0 A.D. - Empires Ascendant", * "url": "https://wildfiregames.com/", * "description": "A free, open-source, historical RTS game.", * "dependencies": [] * } * * Or: * { * "name": "mod2", * "label": "Mod 2", * "version": "1.1", * "description": "", * "dependencies": ["0ad<=0.0.27", "rote"] * } * * A mod is identified by the directory name. * A mod must define the "name", "version", "label", "description" and "dependencies" property. * The "url" property is optional. * * The property "name" can consist alphanumeric characters, underscore and dash. * The name is used for version comparison of mod dependencies. * The property "version" may only contain numbers and up to two periods. * The property "label" is a human-readable name of the mod. * The property "description" is a human-readable summary of the features of the mod. * The property "url" is reference to a website about the mod. * The property "dependencies" is an array of strings. Each string is either a modname or a mod version comparison. * A mod version comparison is a modname, followed by an operator (=, <, >, <= or >=), followed by a mod version. * This allows mods to express upwards and downwards compatibility. */ /** * Mod definitions loaded from the files, including invalid mods. */ var g_Mods = {}; /** * Folder names of all mods that are or can be launched. */ var g_ModsEnabled = []; var g_ModsDisabled = []; var g_ModsEnabledFiltered = []; var g_ModsDisabledFiltered = []; /** * Cache mod compatibility recomputed when some mod is enbaled/disabled. */ var g_ModsCompatibility = []; /** * Name of the mods installed by the ModInstaller. */ var g_InstalledMods; var g_HasIncompatibleMods; var g_FakeMod = { "name": translate("This mod does not exist"), "version": "", "label": "", "url": "", "description": "", "dependencies": [] }; var g_ColorNoModSelected = "255 255 100"; var g_ColorDependenciesMet = "100 255 100"; var g_ColorDependenciesNotMet = "255 100 100"; function init(data, hotloadData) { g_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || []; g_HasIncompatibleMods = Engine.HasIncompatibleMods(); initMods(); initGUIButtons(data); if (g_HasIncompatibleMods) Engine.PushGuiPage("page_incompatible_mods.xml", {}); } function initMods() { loadMods(); loadEnabledMods(); recomputeCompatibility(); validateMods(); initGUIFilters(); } function getHotloadData() { return { "installedMods": g_InstalledMods }; } function loadMods() { g_Mods = Engine.GetAvailableMods(); deepfreeze(g_Mods); } /** * Return fake mod for mods which do not exist */ function getMod(folder) { return !!g_Mods[folder] ? g_Mods[folder] : g_FakeMod; } function loadEnabledMods() { if (g_HasIncompatibleMods) g_ModsEnabled = Engine.GetEnabledMods().concat(Engine.GetIncompatibleMods()) .filter(folder => folder != "mod"); else g_ModsEnabled = Engine.GetEnabledMods().filter(folder => !!g_Mods[folder]); g_ModsDisabled = Object.keys(g_Mods).filter(folder => g_ModsEnabled.indexOf(folder) == -1); g_ModsEnabledFiltered = g_ModsEnabled; g_ModsDisabledFiltered = g_ModsDisabled; } function validateMods() { for (let folder in g_Mods) validateMod(folder, g_Mods[folder], true); } function initGUIFilters() { Engine.GetGUIObjectByName("negateFilter").checked = false; Engine.GetGUIObjectByName("modCompatibleFilter").checked = true; displayModLists(); } function initGUIButtons(data) { // Either get back to the previous page or quit if there is no previous page let hasPreviousPage = !data || data.cancelbutton || false; Engine.GetGUIObjectByName("cancelButton").hidden = !hasPreviousPage; Engine.GetGUIObjectByName("quitButton").hidden = hasPreviousPage; // Turn 'save' off, it will be enabled on any change. Engine.GetGUIObjectByName("saveConfigurationButton").enabled = false; Engine.GetGUIObjectByName("toggleModButton").caption = translateWithContext("mod activation", "Enable"); } function saveMods() { sortEnabledMods(); - Engine.ConfigDB_CreateValue("user", "mod.enabledmods", ["mod"].concat(g_ModsEnabled).join(" ")); - Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + Engine.ConfigDB_CreateAndSaveValue("user", "mod.enabledmods", ["mod"].concat(g_ModsEnabled).join(" ")); Engine.GetGUIObjectByName("saveConfigurationButton").enabled = false; } function startMods() { saveMods(); if (!Engine.SetModsAndRestartEngine(["mod"].concat(g_ModsEnabled))) Engine.GetGUIObjectByName("message").caption = coloredText(translate('Dependencies not met'), g_ColorDependenciesNotMet); } function displayModLists() { g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled, true); g_ModsDisabledFiltered = displayModList("modsDisabledList", g_ModsDisabled, false); } function displayModList(listObjectName, folders, enabled) { let listObject = Engine.GetGUIObjectByName(listObjectName); if (listObjectName == "modsDisabledList") { let sortFolder = folder => String(getMod(folder)[listObject.selected_column] || folder); folders.sort((folder1, folder2) => listObject.selected_column_order * sortFolder(folder1).localeCompare(sortFolder(folder2))); } folders = folders.filter(filterMod); if (!enabled && Engine.GetGUIObjectByName("modCompatibleFilter").checked) folders = folders.filter(folder => g_ModsCompatibility[folder]); let selected = listObject.selected !== -1 ? listObject.list_name[listObject.selected] : null; listObject.list_name = folders.map(folder => colorMod(folder, getMod(folder).name || "", enabled)); listObject.list_folder = folders.map(folder => colorMod(folder, folder, enabled)); listObject.list_label = folders.map(folder => colorMod(folder, getMod(folder).label || "", enabled)); listObject.list_url = folders.map(folder => colorMod(folder, getMod(folder).url || "", enabled)); listObject.list_version = folders.map(folder => colorMod(folder, getMod(folder).version || "", enabled)); listObject.list_dependencies = folders.map(folder => colorMod(folder, getMod(folder)?.dependencies.join(" ") || "", enabled)); listObject.list = folders; listObject.selected = selected ? listObject.list_name.indexOf(selected) : -1; return folders; } function getModColor(folder, enabled) { if (!g_ModsCompatibility[folder]) return enabled ? g_ColorDependenciesNotMet : "gray"; if (g_InstalledMods.indexOf(getMod(folder).name) != -1) return "green"; return false; } function colorMod(folder, text, enabled) { let color = getModColor(folder, enabled); return color ? coloredText(text, color) : text; } function reloadDisabledMods() { g_ModsDisabled = Object.keys(g_Mods).filter(folder => g_ModsEnabled.indexOf(folder) == -1); } function enableMod() { let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList"); let pos = modsDisabledList.selected; if (pos == -1 || !g_ModsCompatibility[g_ModsDisabledFiltered[pos]]) return; g_ModsEnabled.push(g_ModsDisabledFiltered.splice(pos, 1)[0]); reloadDisabledMods(); recomputeCompatibility(); Engine.GetGUIObjectByName("saveConfigurationButton").enabled = true; if (pos >= g_ModsDisabledFiltered.length) --pos; displayModLists(); Engine.GetGUIObjectByName("message").caption = ""; modsDisabledList.selected = pos; } function disableMod() { let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList"); let pos = modsEnabledList.selected; if (pos == -1) return; // Find true position of disabled mod and remove it let disabledMod = g_ModsEnabledFiltered[pos]; for (let i = 0; i < g_ModsEnabled.length; ++i) if (g_ModsEnabled[i] == disabledMod) { g_ModsEnabled.splice(i, 1); break; } if (!!g_Mods[disabledMod]) g_ModsDisabled.push(disabledMod); // Remove mods that required the removed mod and cascade // Sort them, so we know which ones can depend on the removed mod // TODO: Find position where the removed mod would have fit (for now assume idx 0) sortEnabledMods(); for (let i = 0; i < g_ModsEnabled.length; ++i) if (!areDependenciesMet(g_ModsEnabled[i], true)) { g_ModsDisabled.push(g_ModsEnabled.splice(i, 1)[0]); --i; } Engine.GetGUIObjectByName("saveConfigurationButton").enabled = true; recomputeCompatibility(true); displayModLists(); Engine.GetGUIObjectByName("message").caption = ""; modsEnabledList.selected = Math.min(pos, g_ModsEnabledFiltered.length - 1); } function filterMod(folder) { let mod = getMod(folder); let negateFilter = Engine.GetGUIObjectByName("negateFilter").checked; let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption; if (searchText && folder.indexOf(searchText) == -1 && (mod.name || "").indexOf(searchText) == -1 && (mod.label || "").indexOf(searchText) == -1 && (mod.url || "").indexOf(searchText) == -1 && (mod.version || "").indexOf(searchText) == -1 && (mod.description || "").indexOf(searchText) == -1 && (mod.dependencies || "").indexOf(searchText) == -1) return negateFilter; return !negateFilter; } function closePage() { Engine.SwitchGuiPage("page_pregame.xml", {}); } /** * Moves an item in the list up or down. */ function moveCurrItem(objectName, up) { // Prevent moving while filters are applied // because we would need to map filtered positions // to not filtered positions so changes will persist. if (Engine.GetGUIObjectByName("modGenericFilter").caption) return; let obj = Engine.GetGUIObjectByName(objectName); let idx = obj.selected; if (idx == -1) return; let num = obj.list.length; let idx2 = idx + (up ? -1 : 1); if (idx2 < 0 || idx2 >= num) return; let tmp = g_ModsEnabled[idx]; g_ModsEnabled[idx] = g_ModsEnabled[idx2]; g_ModsEnabled[idx2] = tmp; g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled, true); obj.selected = idx2; } function areDependenciesMet(folder, disabledAction = false) { // If we disabled mod it will not change satus of incompatible mods if (disabledAction && !g_ModsCompatibility[folder]) return g_ModsCompatibility[folder]; if (!g_Mods[folder]) return false; for (let dependency of getMod(folder).dependencies) { if (!isDependencyMet(dependency)) return false; } return true; } function recomputeCompatibility(disabledAction = false) { for (let mod in g_Mods) g_ModsCompatibility[mod] = areDependenciesMet(mod, disabledAction); } /** * @param dependency is a mod name or a mod version comparison. */ function isDependencyMet(dependency) { let operator = dependency.match(g_RegExpComparisonOperator); let [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined]; return g_ModsEnabled.some(folder => getMod(folder).name == name && (!operator || versionSatisfied(getMod(folder).version, operator[0], version))); } /** * Compares the given versions using the given operator. * '-' or '_' is ignored. Only numbers are supported. * @note "5.3" < "5.3.0" */ function versionSatisfied(version1, operator, version2) { let versionList1 = version1.split(/[-_]/)[0].split(/\./g); let versionList2 = version2.split(/[-_]/)[0].split(/\./g); let eq = operator.indexOf("=") != -1; let lt = operator.indexOf("<") != -1; let gt = operator.indexOf(">") != -1; for (let i = 0; i < Math.min(versionList1.length, versionList2.length); ++i) { let diff = +versionList1[i] - +versionList2[i]; if (gt && diff > 0 || lt && diff < 0) return true; if (gt && diff < 0 || lt && diff > 0 || eq && diff) return false; } // common prefix matches let ldiff = versionList1.length - versionList2.length; if (!ldiff) return eq; // NB: 2.3 != 2.3.0 if (ldiff < 0) return lt; return gt; } function sortEnabledMods() { let dependencies = {}; for (let folder of g_ModsEnabled) dependencies[folder] = getMod(folder).dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]); g_ModsEnabled.sort((folder1, folder2) => dependencies[folder1].indexOf(getMod(folder2).name) != -1 ? 1 : dependencies[folder2].indexOf(getMod(folder1).name) != -1 ? -1 : 0); g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled, true); } function selectedMod(listObjectName) { let listObject = Engine.GetGUIObjectByName(listObjectName); let isPickedDisabledList = listObjectName == "modsDisabledList"; let otherListObject = Engine.GetGUIObjectByName(isPickedDisabledList ? "modsEnabledList" : "modsDisabledList"); let toggleModButton = Engine.GetGUIObjectByName("toggleModButton"); let isModSelected = listObject.selected != -1; if (isModSelected) { otherListObject.selected = -1; toggleModButton.onPress = isPickedDisabledList ? enableMod : disableMod; } const isFiltering = Engine.GetGUIObjectByName("modGenericFilter").caption; Engine.GetGUIObjectByName("visitWebButton").enabled = isModSelected && !!getSelectedModUrl(); toggleModButton.caption = isPickedDisabledList ? translateWithContext("mod activation", "Enable") : translateWithContext("mod activation", "Disable"); toggleModButton.enabled = isPickedDisabledList ? isModSelected && g_ModsCompatibility[listObject.list[listObject.selected]] || false : isModSelected; Engine.GetGUIObjectByName("enabledModUp").enabled = isModSelected && listObjectName == "modsEnabledList" && !isFiltering; Engine.GetGUIObjectByName("enabledModDown").enabled = isModSelected && listObjectName == "modsEnabledList" && !isFiltering; Engine.GetGUIObjectByName("globalModDescription").caption = listObject.list[listObject.selected] ? getMod(listObject.list[listObject.selected]).description : '[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]'; if (!g_ModsEnabled.length) Engine.GetGUIObjectByName("message").caption = coloredText(translate('Enable at least 0ad mod'), g_ColorDependenciesNotMet); if (!Engine.GetGUIObjectByName("startButton").hidden) Engine.GetGUIObjectByName("startButton").enabled = g_ModsEnabled.length > 0; } /** * @returns {string} The url of the currently selected mod. */ function getSelectedModUrl() { let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList"); let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList"); let list = modsEnabledList.selected == -1 ? modsDisabledList : modsEnabledList; let folder = list.list[list.selected]; return folder && getMod(folder) && getMod(folder).url || undefined; } function visitModWebsite() { let url = getSelectedModUrl(); if (!url) return; if (!url.startsWith("http://") && !url.startsWith("https://")) url = "http://" + url; openURL(url); } Index: ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js (revision 27191) +++ ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js (revision 27192) @@ -1,153 +1,152 @@ // Cached run for CampaignRun.getCurrentRun() // TODO: Move this to a static member once linters accept it. var g_CurrentCampaignRun; /** * A campaign "Run" saves metadata on a campaign progession. * It is equivalent to a saved game for a game. * It is named a "run" in an attempt to disambiguate with saved games from campaign runs, * campaign templates, and the actual concept of a campaign at large. * * The intent is that this file should be lightweight to load/save. */ class CampaignRun { static getCurrentRunFilename() { return Engine.ConfigDB_GetValue("user", "currentcampaign"); } static hasCurrentRun() { return !!CampaignRun.getCurrentRunFilename(); } static getCurrentRun() { let current = CampaignRun.getCurrentRunFilename(); if (g_CurrentCampaignRun && g_CurrentCampaignRun.ID == current) return g_CurrentCampaignRun.run; let run = new CampaignRun(current).load(); g_CurrentCampaignRun = { "run": run, "ID": current }; return run; } static clearCurrentRun() { - Engine.ConfigDB_RemoveValue("user", "currentcampaign"); - Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + Engine.ConfigDB_RemoveValueAndSave("user", "currentcampaign"); } constructor(name = "") { this.filename = name; // Metadata on the run, such as its description. this.meta = {}; // 'User' data this.data = {}; // ID of the campaign templates. this.template = null; } setData(data) { if (!data) { warn("Invalid campaign scenario end data. Nothing will be saved."); return this; } this.data = data; this.save(); return this; } setTemplate(template) { this.template = template; this.save(); return this; } setMeta(description) { this.meta.userDescription = description; this.save(); return this; } setCurrent() { Engine.ConfigDB_CreateValue("user", "currentcampaign", this.filename); Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", this.filename, "config/user.cfg"); g_CurrentCampaignRun = { "ID": this.filename, "run": this }; return this; } isCurrent() { return this.filename === CampaignRun.getCurrentRunFilename(); } getMenuPath() { return "campaigns/" + this.template.interface + "/page.xml"; } getEndGamePath() { return "campaigns/" + this.template.interface + "/endgame/page.xml"; } /** * Return a readable name for this campaign. * @param full - if true, include both user description and template name. Otherwise, skip one if they're identical. */ getLabel(full) { if (!full && this.meta.userDescription === translateWithContext("Campaign Template", this.template.Name)) return this.meta.userDescription; return sprintf(translate("%(userDesc)s - %(templateName)s"), { "userDesc": this.meta.userDescription, "templateName": translateWithContext("Campaign Template", this.template.Name) }); } load() { if (!Engine.FileExists("saves/campaigns/" + this.filename + ".0adcampaign")) throw new Error("Campaign file does not exist"); let data = Engine.ReadJSONFile("saves/campaigns/" + this.filename + ".0adcampaign"); this.data = data.data; this.meta = data.meta; this.template = CampaignTemplate.getTemplate(data.template_identifier); if (!this.template) throw new Error("Campaign template " + data.template_identifier + " does not exist (perhaps it comes from a mod?)"); return this; } save() { let data = { "data": this.data, "meta": this.meta, "template_identifier": this.template.identifier }; Engine.WriteJSONFile("saves/campaigns/" + this.filename + ".0adcampaign", data); return this; } destroy() { Engine.DeleteCampaignSave("saves/campaigns/" + this.filename + ".0adcampaign"); if (CampaignRun.getCurrentRunFilename() === this.filename) CampaignRun.clearCurrentRun(); } } Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js (revision 27191) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js (revision 27192) @@ -1,233 +1,233 @@ /** * This class provides a property independent interface to g_PlayerAssignment events and actions. */ class PlayerAssignmentsController { constructor(setupWindow, netMessages) { this.clientJoinHandlers = new Set(); this.clientLeaveHandlers = new Set(); this.playerAssignmentsChangeHandlers = new Set(); if (!g_IsNetworked) { let name = singleplayerName(); // Replace empty player name when entering a single-player match for the first time. - Engine.ConfigDB_CreateAndWriteValueToFile("user", this.ConfigNameSingleplayer, name, "config/user.cfg"); + Engine.ConfigDB_CreateAndSaveValue("user", this.ConfigNameSingleplayer, name); // By default, assign the player to the first slot. g_PlayerAssignments = { "local": { "name": name, "player": 1 } }; } // Keep a list of last assigned slot for each player, so we can try to re-assign them // if they disconnect/rejoin. this.lastAssigned = {}; g_GameSettings.playerCount.watch(() => this.unassignInvalidPlayers(), ["nbPlayers"]); setupWindow.registerLoadHandler(this.onLoad.bind(this)); setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this)); netMessages.registerNetMessageHandler("players", this.onPlayerAssignmentMessage.bind(this)); this.registerClientJoinHandler(this.onClientJoin.bind(this)); } registerPlayerAssignmentsChangeHandler(handler) { this.playerAssignmentsChangeHandlers.add(handler); } unregisterPlayerAssignmentsChangeHandler(handler) { this.playerAssignmentsChangeHandlers.delete(handler); } registerClientJoinHandler(handler) { this.clientJoinHandlers.add(handler); } unregisterClientJoinHandler(handler) { this.clientJoinHandlers.delete(handler); } registerClientLeaveHandler(handler) { this.clientLeaveHandlers.add(handler); } unregisterClientLeaveHandler(handler) { this.clientLeaveHandlers.delete(handler); } onLoad(initData, hotloadData) { if (hotloadData) { g_PlayerAssignments = hotloadData.playerAssignments; this.updatePlayerAssignments(); } else if (!g_IsNetworked) { // Simulate a net message for the local player to keep a common path. this.onPlayerAssignmentMessage({ "newAssignments": g_PlayerAssignments }); } } onGetHotloadData(object) { object.playerAssignments = g_PlayerAssignments; } /** * On client join, try to assign them to a free slot. * (This is called before g_PlayerAssignments is updated). */ onClientJoin(newGUID, newAssignments) { if (!g_IsController || newAssignments[newGUID].player != -1) return; // Assign the client (or only buddies if prefered) to a free slot if (newGUID != Engine.GetPlayerGUID()) { const assignOption = Engine.ConfigDB_GetValue("user", this.ConfigAssignPlayers); if (assignOption == "disabled" || assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(newAssignments[newGUID].name).nick) == -1) return; } // Find a player slot that no other player is assigned to. const possibleSlots = [...Array(g_GameSettings.playerCount.nbPlayers).keys()].map(i => i + 1); let slot; const newName = newAssignments[newGUID].name; // First check if we know them and try to give them their old assignment back. if (this.lastAssigned[newName] > 0 && this.lastAssigned[newName] <= g_GameSettings.playerCount.nbPlayers) { let free = true; for (const guid in newAssignments) if (newAssignments[guid].player === this.lastAssigned[newName]) { free = false; break; } if (free) slot = this.lastAssigned[newName]; } if (!slot) slot = possibleSlots.find(i => { for (const guid in newAssignments) if (newAssignments[guid].player == i) return false; return true; }); if (slot === undefined) return; this.assignClient(newGUID, slot); } /** * To be called when g_PlayerAssignments is modified. */ updatePlayerAssignments() { Engine.ProfileStart("updatePlayerAssignments"); for (const guid in g_PlayerAssignments) this.lastAssigned[g_PlayerAssignments[guid].name] = g_PlayerAssignments[guid].player; for (const handler of this.playerAssignmentsChangeHandlers) handler(); Engine.ProfileStop(); } /** * Called whenever a client joins or leaves or any game setting is changed. */ onPlayerAssignmentMessage(message) { let newAssignments = message.newAssignments; for (let guid in newAssignments) if (!g_PlayerAssignments[guid]) for (let handler of this.clientJoinHandlers) handler(guid, message.newAssignments); for (let guid in g_PlayerAssignments) if (!newAssignments[guid]) for (let handler of this.clientLeaveHandlers) handler(guid); g_PlayerAssignments = newAssignments; this.updatePlayerAssignments(); } assignClient(guid, playerIndex) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerIndex, guid); else { g_PlayerAssignments[guid].player = playerIndex; this.updatePlayerAssignments(); } } /** * If both clients are assigned players, this will swap their assignments. */ assignPlayer(guidToAssign, playerIndex) { if (g_PlayerAssignments[guidToAssign].player != -1) { for (let guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player == playerIndex + 1) { this.assignClient(guid, g_PlayerAssignments[guidToAssign].player); break; } } this.assignClient(guidToAssign, playerIndex + 1); } unassignClient(playerID) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerID, ""); else if (g_PlayerAssignments.local.player == playerID) { g_PlayerAssignments.local.player = -1; this.updatePlayerAssignments(); } } unassignInvalidPlayers() { if (g_IsNetworked) { for (let guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player > g_GameSettings.playerCount.nbPlayers) Engine.AssignNetworkPlayer(g_PlayerAssignments[guid].player, ""); } else if (g_PlayerAssignments.local.player > g_GameSettings.playerCount.nbPlayers) { g_PlayerAssignments.local.player = -1; this.updatePlayerAssignments(); } } } PlayerAssignmentsController.prototype.ConfigNameSingleplayer = "playername.singleplayer"; PlayerAssignmentsController.prototype.ConfigAssignPlayers = "gui.gamesetup.assignplayers"; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/TipsPanel.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/TipsPanel.js (revision 27191) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/TipsPanel.js (revision 27192) @@ -1,45 +1,45 @@ /** * The TipsPanel shows some hints to newcomers. * It is only shown in Singleplayer mode since the chat is shown instead in multiplayer mode. */ class TipsPanel { constructor(gameSettingsPanel) { let available = !g_IsNetworked && Engine.ConfigDB_GetValue("user", this.Config) == "true"; this.spTips = Engine.GetGUIObjectByName("spTips"); this.spTips.hidden = !available; if (!available) return; this.displaySPTips = Engine.GetGUIObjectByName("displaySPTips"); this.displaySPTips.onPress = this.onPress.bind(this); Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile(this.File)); gameSettingsPanel.registerGameSettingsPanelResizeHandler(this.onGameSettingsPanelResize.bind(this)); } onPress() { - Engine.ConfigDB_CreateAndWriteValueToFile( + Engine.ConfigDB_CreateAndSaveValue( "user", this.Config, - String(this.displaySPTips.checked), - "config/user.cfg"); + String(this.displaySPTips.checked) + ); } onGameSettingsPanelResize(settingsPanel) { this.spTips.hidden = this.spTips.getComputedSize().right > settingsPanel.getComputedSize().left; } } TipsPanel.prototype.File = "gui/gamesetup/Pages/GameSetupPage/Panels/Tips.txt"; TipsPanel.prototype.Config = "gui.gamesetup.enabletips"; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js (revision 27191) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js (revision 27192) @@ -1,472 +1,472 @@ /** * 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_ServerId; var g_IsRejoining = false; var g_PlayerAssignments; // used when rejoining var g_UserRating; 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; g_ServerId = attribs.hostJID; switchSetupPage("pagePassword"); } else if (startJoinFromLobby(attribs.name, attribs.hostJID, "")) switchSetupPage("pageConnecting"); break; } case "host": { let hasXmppClient = Engine.HasXmppClient(); Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !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 }); Engine.GetGUIObjectByName("useSTUN").checked = Engine.ConfigDB_GetValue("user", "lobby.stun.enabled") == "true"; } switchSetupPage("pageHost"); break; } default: error("Unrecognised multiplayer game type: " + attribs.multiplayerGameType); break; } } 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")) { Engine.PopGuiPage(); return; } g_IsConnecting = false; Engine.GetGUIObjectByName("hostFeedback").caption = ""; if (g_GameType == "client") switchSetupPage("pageJoin"); else if (g_GameType == "server") switchSetupPage("pageHost"); else error("cancelSetup: Unrecognised multiplayer game type: " + g_GameType); } function confirmPassword() { if (Engine.GetGUIObjectByName("pagePassword").hidden) return; if (startJoinFromLobby(g_ServerName, g_ServerId, Engine.GetGUIObjectByName("clientPassword").caption)) switchSetupPage("pageConnecting"); } function confirmSetup() { if (!Engine.GetGUIObjectByName("pageJoin").hidden) { let joinPlayerName = Engine.GetGUIObjectByName("joinPlayerName").caption; let joinServer = Engine.GetGUIObjectByName("joinServer").caption; let joinPort = Engine.GetGUIObjectByName("joinPort").caption; if (startJoin(joinPlayerName, joinServer, getValidPort(joinPort))) switchSetupPage("pageConnecting"); } else if (!Engine.GetGUIObjectByName("pageHost").hidden) { let hostServerName = Engine.GetGUIObjectByName("hostServerName").caption; if (!hostServerName) { Engine.GetGUIObjectByName("hostFeedback").caption = translate("Please enter a valid server name."); return; } let 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; } let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption; let hostPassword = Engine.GetGUIObjectByName("hostPassword").caption; if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword)) switchSetupPage("pageConnecting"); } } function startConnectionStatus(type) { g_GameType = type; g_IsConnecting = true; g_IsRejoining = false; Engine.GetGUIObjectByName("connectionStatus").caption = translate("Connecting to server..."); } function onTick() { if (!g_IsConnecting) return; pollAndHandleNetworkClient(); } 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() { while (true) { var message = Engine.PollNetworkClient(); if (!message) break; log(sprintf(translate("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": cancelSetup(); reportConnectionFail(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "netstatus": switch (message.status) { case "disconnected": cancelSetup(); reportDisconnect(message.reason, false); return; default: error("Unrecognised 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; case "chat": break; case "netwarn": break; default: error("Unrecognised net message type: " + message.type); } } else // Not rejoining - just trying to connect to server. { switch (message.type) { case "serverdata": switch (message.status) { case "failed": cancelSetup(); reportConnectionFail(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "netstatus": switch (message.status) { case "connected": Engine.GetGUIObjectByName("connectionStatus").caption = translate("Registering with server..."); break; case "authenticated": if (message.rejoining) { Engine.GetGUIObjectByName("connectionStatus").caption = translate("Game has already started, rejoining..."); g_IsRejoining = true; return; // we'll process the game setup messages in the next tick } Engine.SwitchGuiPage("page_gamesetup.xml", { "serverName": g_ServerName, "hasPassword": g_ServerHasPassword }); return; // don't process any more messages - leave them for the game GUI loop case "disconnected": cancelSetup(); reportDisconnect(message.reason, false); return; default: error("Unrecognised netstatus type: " + message.status); break; } break; case "netwarn": break; default: error("Unrecognised net message type: " + message.type); break; } } } } function switchSetupPage(newPage) { let multiplayerPages = Engine.GetGUIObjectByName("multiplayerPages"); for (let page of multiplayerPages.children) if (page.name.startsWith("page")) page.hidden = true; if (newPage == "pageJoin" || newPage == "pageHost") { let pageSize = multiplayerPages.size; let halfHeight = newPage == "pageJoin" ? 145 : Engine.HasXmppClient() ? 140 : 125; pageSize.top = -halfHeight; pageSize.bottom = halfHeight; multiplayerPages.size = pageSize; } else if (newPage == "pagePassword") { let pageSize = multiplayerPages.size; let halfHeight = 60; pageSize.top = -halfHeight; pageSize.bottom = halfHeight; multiplayerPages.size = pageSize; } 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) { startConnectionStatus("server"); - Engine.ConfigDB_CreateAndWriteValueToFile("user", "playername.multiplayer", playername, "config/user.cfg"); + Engine.ConfigDB_CreateAndSaveValue("user", "playername.multiplayer", playername); - Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerhosting.port", port, "config/user.cfg"); + Engine.ConfigDB_CreateAndSaveValue("user", "multiplayerhosting.port", port); let hostFeedback = Engine.GetGUIObjectByName("hostFeedback"); // Disallow identically named games in the multiplayer lobby if (Engine.HasXmppClient() && Engine.GetGameList().some(game => game.name == servername)) { cancelSetup(); hostFeedback.caption = translate("Game name already in use."); return false; } let useSTUN = Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked; try { Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, useSTUN, password, true); } catch (e) { cancelSetup(); messageBox( 400, 200, sprintf(translate("Cannot host game: %(message)s."), { "message": e.message }), translate("Error") ); return false; } g_ServerName = servername; g_ServerHasPassword = !!password; if (Engine.HasXmppClient()) Engine.LobbySetPlayerPresence("playing"); return true; } /** * Connect via direct IP (used by the 'simple' MP screen) */ function startJoin(playername, ip, port) { try { Engine.StartNetworkJoin(playername, ip, port, true); } catch (e) { cancelSetup(); messageBox( 400, 200, sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }), translate("Error") ); return false; } 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_CreateAndWriteValueToFile("user", "playername.multiplayer", playername, "config/user.cfg"); - Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerserver", ip, "config/user.cfg"); - Engine.ConfigDB_CreateAndWriteValueToFile("user", "multiplayerjoining.port", port, "config/user.cfg"); + Engine.ConfigDB_CreateAndSaveValue("user", "playername.multiplayer", playername); + Engine.ConfigDB_CreateAndSaveValue("user", "multiplayerserver", ip); + Engine.ConfigDB_CreateAndSaveValue("user", "multiplayerjoining.port", port); return true; } /** * Connect via the lobby. */ function startJoinFromLobby(playername, hostJID, password) { if (!Engine.HasXmppClient()) { cancelSetup(); messageBox( 400, 200, sprintf("You cannot join a lobby game without logging in to the lobby."), translate("Error") ); return false; } try { Engine.StartNetworkJoinLobby(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), hostJID, password); } catch (e) { cancelSetup(); messageBox( 400, 200, sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }), translate("Error") ); return false; } startConnectionStatus("client"); Engine.LobbySetPlayerPresence("playing"); return true; } function getDefaultGameName() { return sprintf(translate("%(playername)s's game"), { "playername": multiplayerName() }); } function getDefaultPassword() { return ""; } Index: ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml (revision 27191) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml (revision 27192) @@ -1,168 +1,168 @@