Index: ps/trunk/binaries/data/mods/mod/gui/gui.rnc =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/gui.rnc (revision 24432) +++ ps/trunk/binaries/data/mods/mod/gui/gui.rnc (revision 24433) @@ -1,289 +1,291 @@ namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0" ## # NOTE: To modify this Relax NG grammar, edit the Relax NG Compact (.rnc) file # and use a converter tool like trang to generate the Relax NG XML (.rng) file ## start = object | objects | setup | sprites | styles ## # Types # ## # xsd:boolean could be used instead of this definition, # though it considers "1" & "0" as valid values. bool = "true" | "false" align = "left" | "center" | "right" valign = "top" | "center" | "bottom" wrapmode = "repeat" | "mirrored_repeat" | "clamp_to_edge" coord = xsd:string { pattern = "-?\d*\.?\d+%?([\+\-]\d*\.?\d+%?)*" } clientarea = list { coord, coord, coord, coord } # color can be a name or "R G B A" format string rgba = list { xsd:integer { minInclusive = "0" maxInclusive = "255" }, xsd:integer { minInclusive = "0" maxInclusive = "255" }, xsd:integer { minInclusive = "0" maxInclusive = "255" }, xsd:integer { minInclusive = "0" maxInclusive = "255" }?} ccolor = rgba | xsd:string { pattern = "[A-Za-z]+" } size = list { xsd:decimal, xsd:decimal } pos = list { xsd:decimal, xsd:decimal } rect = list { xsd:decimal, xsd:decimal, xsd:decimal, xsd:decimal } ## # Defines # ## unique_settings = attribute name { text }?, [ a:defaultValue = "empty" ] attribute type { text }?, attribute style { text }? # This could probably be made more specific/strict # with more information regarding the use/meaning # of these attributes. base_settings = attribute absolute { bool }?& attribute enable { bool }?& attribute ghost { bool }?& attribute hidden { bool }?& attribute size { clientarea }?& attribute z { xsd:decimal }? # Defaults are not put in here, because it ruins the concept of styles. ex_settings = attribute anchor { valign }?& attribute auto_scroll { bool }?& attribute buffer_zone { xsd:decimal }?& attribute buffer_width { xsd:decimal }?& attribute button_width { xsd:decimal }?& attribute checked { bool }?& attribute clip { bool }?& attribute dropdown_size { xsd:decimal }?& attribute dropdown_buffer { xsd:decimal }?& attribute minimum_visible_items { xsd:nonNegativeInteger }?& attribute enabled { bool }?& attribute font { text }?& attribute format_x { text }?& attribute format_y { text }?& attribute fov_wedge_color { ccolor }?& attribute hotkey { text }?& attribute cell_id { xsd:integer }?& attribute independent { bool }?& attribute input_initvalue_destroyed_at_focus { bool }?& attribute mask { bool }?& attribute mask_char { xsd:string { minLength = "1" maxLength = "1" } }?& attribute max_length { xsd:nonNegativeInteger }?& attribute maxwidth { xsd:decimal }? & attribute multiline { bool }?& attribute offset { pos }?& + attribute placeholder_text { text }?& + attribute placeholder_color { ccolor }?& attribute readonly { bool }?& attribute scrollbar { bool }?& attribute scrollbar_style { text }?& attribute scroll_bottom { bool }?& attribute scroll_top { bool }?& attribute selected_column { text }?& attribute selected_column_order { text }?& attribute sortable { bool }?& attribute sound_closed { text }?& attribute sound_disabled { text }?& attribute sound_enter { text }?& attribute sound_leave { text }?& attribute sound_opened { text }?& attribute sound_pressed { text }?& attribute sound_selected { text }?& attribute sprite { text }?& attribute sprite2 { text }?& attribute sprite_asc { text }?& attribute sprite_heading { text }?& attribute sprite_bar { text }?& attribute sprite_background { text }?& attribute sprite_desc { text }?& attribute sprite_disabled { text }?& attribute sprite_list { text }?& attribute sprite2_disabled { text }?& attribute sprite_not_sorted { text }?& attribute sprite_over { text }?& attribute sprite2_over { text }?& attribute sprite_pressed { text }?& attribute sprite2_pressed { text }?& attribute sprite_selectarea { text }?& attribute square_side { xsd:decimal }?& attribute textcolor { ccolor }?& attribute textcolor_disabled { ccolor }?& attribute textcolor_over { ccolor }?& attribute textcolor_pressed { ccolor }?& attribute textcolor_selected { ccolor }?& attribute text_align { align }?& attribute text_valign { valign }?& attribute tooltip { text }?& attribute tooltip_style { text }? ## # Objects # ## objects = element objects { (script | object)* } script = element script { text & attribute file { text }? & attribute directory { text }? } object = element object { ((object | action | \attribute | column | \include | item | repeat | script | translatableAttribute)* | text), unique_settings, base_settings, ex_settings } action = element action { text, attribute on { text }, attribute file { text }? } \attribute = element attribute { (keep | translate)*, attribute id { text } } column = element column { translatableAttribute?, ( attribute id { text }& attribute color { ccolor }?& attribute heading { text }?& attribute width { text }?& attribute hidden { bool }? ) } \include = element include { attribute file { text }| attribute directory { text } } item = element item { text, attribute enabled { bool }? } keep = element keep { text } repeat = element repeat { object+, attribute count { xsd:nonNegativeInteger }, attribute var { text }? } translate = element translate { text } translatableAttribute = element translatableAttribute { text, ( attribute id { text }& attribute comment { text }?& attribute context { text }? ) } ## # Styles # ## styles = element styles { style* } style = element style { attribute name { text }, base_settings, ex_settings } ## # Setup # ## setup = element setup { (icon | scrollbar | tooltip | color)* } scrollbar = element scrollbar { attribute name { text }& attribute width { xsd:decimal }& attribute alwaysshown { bool }?& attribute maximum_bar_size { xsd:decimal }?& attribute minimum_bar_size { xsd:decimal }?& attribute scroll_wheel { bool }?& attribute show_edge_buttons { bool }?& attribute sprite_button_top { text }?& attribute sprite_button_top_pressed { text }?& attribute sprite_button_top_disabled { text }?& attribute sprite_button_top_over { text }?& attribute sprite_button_bottom { text }?& attribute sprite_button_bottom_pressed { text }?& attribute sprite_button_bottom_disabled { text }?& attribute sprite_button_bottom_over { text }?& attribute sprite_bar_vertical { text }?& attribute sprite_bar_vertical_over { text }?& attribute sprite_bar_vertical_pressed { text }?& attribute sprite_back_vertical { text }? } icon = element icon { attribute name { text }& attribute size { size }& attribute sprite { text }& attribute cell_id { text }? } tooltip = element tooltip { attribute name { text }& attribute sprite { text }?& attribute anchor { valign }?& attribute axis_color { ccolor }?& attribute axis_width { xsd:decimal { minInclusive = "0" } }?& attribute buffer_zone { xsd:decimal }?& attribute font { text }?& attribute maxwidth { xsd:decimal }?& attribute offset { pos }?& attribute textcolor { ccolor }?& attribute delay { xsd:integer }?& attribute use_object { text }?& attribute hide_object { bool }? } color = element color { rgba, attribute name { text } } ## # Sprites # ## sprites = element sprites { sprite* } sprite = element sprite { (effect?, image+), attribute name { text } } image = element image { effect?, ( attribute texture { text }?& attribute size { clientarea }?& attribute texture_size { clientarea }?& attribute real_texture_placement { rect }?& attribute cell_size { size }?& attribute backcolor { ccolor }?& attribute bordercolor { ccolor }?& attribute border { bool }?& attribute z_level { xsd:float }?& attribute fixed_h_aspect_ratio { xsd:decimal }?& attribute round_coordinates { bool }?& attribute wrap_mode { wrapmode }? ) } effect = element effect { attribute add_color { ccolor }?, attribute grayscale { empty }? } Index: ps/trunk/binaries/data/mods/mod/gui/gui.rng =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 24432) +++ ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 24433) @@ -1,883 +1,891 @@ true false left center right top center bottom repeat mirrored_repeat clamp_to_edge -?\d*\.?\d+%?([\+\-]\d*\.?\d+%?)* 0 255 0 255 0 255 0 255 [A-Za-z]+ 0 1 1 + + + + + + + + Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 24432) +++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 24433) @@ -1,442 +1,440 @@ /** * @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.24", * "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.24", "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 = []; /** * Name of the mods installed by the ModInstaller. */ var g_InstalledMods; 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 || []; initMods(); initGUIButtons(data); } function initMods() { loadMods(); loadEnabledMods(); validateMods(); initGUIFilters(); } function getHotloadData() { return { "installedMods": g_InstalledMods }; } function loadMods() { g_Mods = Engine.GetAvailableMods(); deepfreeze(g_Mods); } function loadEnabledMods() { g_ModsEnabled = Engine.ConfigDB_GetValue("user", "mod.enabledmods").split(/\s+/).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("modGenericFilter").caption = translate("Filter"); displayModLists(); } function initGUIButtons(data) { // Either get back to the previous page or quit if there is no previous page let cancelButton = !data || data.cancelbutton; Engine.GetGUIObjectByName("cancelButton").hidden = !cancelButton; Engine.GetGUIObjectByName("quitButton").hidden = cancelButton; 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"); } function startMods() { sortEnabledMods(); Engine.SetMods(["mod"].concat(g_ModsEnabled)); Engine.RestartEngine(); } function displayModLists() { g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled); g_ModsDisabledFiltered = displayModList("modsDisabledList", g_ModsDisabled); } function displayModList(listObjectName, folders) { let listObject = Engine.GetGUIObjectByName(listObjectName); if (listObjectName == "modsDisabledList") { let sortFolder = folder => String(g_Mods[folder][listObject.selected_column] || folder); folders.sort((folder1, folder2) => listObject.selected_column_order * sortFolder(folder1).localeCompare(sortFolder(folder2))); } folders = folders.filter(filterMod); listObject.list_name = folders.map(folder => g_Mods[folder].name).map(name => g_InstalledMods.indexOf(name) == -1 ? name : coloredText(name, "green")); listObject.list_folder = folders; listObject.list_label = folders.map(folder => g_Mods[folder].label); listObject.list_url = folders.map(folder => g_Mods[folder].url || ""); listObject.list_version = folders.map(folder => g_Mods[folder].version); listObject.list_dependencies = folders.map(folder => g_Mods[folder].dependencies.join(" ")); listObject.list = folders; return folders; } 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 || !areDependenciesMet(g_ModsDisabledFiltered[pos])) return; g_ModsEnabled.push(g_ModsDisabledFiltered.splice(pos, 1)[0]); reloadDisabledMods(); if (pos >= g_ModsDisabledFiltered.length) --pos; displayModLists(); 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; } 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])) { g_ModsDisabled.push(g_ModsEnabled.splice(i, 1)[0]); --i; } displayModLists(); modsEnabledList.selected = Math.min(pos, g_ModsEnabledFiltered.length - 1); } function applyFilters() { // Save selected rows let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList"); let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList"); let selectedDisabledFolder = modsDisabledList.list_folder[modsDisabledList.selected]; let selectedEnabledFolder = modsEnabledList.list_folder[modsEnabledList.selected]; // Remove selected rows to prevent a link to a non existing item modsDisabledList.selected = -1; modsEnabledList.selected = -1; displayModLists(); // Restore previously selected rows modsDisabledList.selected = modsDisabledList.list_folder.indexOf(selectedDisabledFolder); modsEnabledList.selected = modsEnabledList.list_folder.indexOf(selectedEnabledFolder); Engine.GetGUIObjectByName("globalModDescription").caption = ""; } function filterMod(folder) { let mod = g_Mods[folder]; let negateFilter = Engine.GetGUIObjectByName("negateFilter").checked; let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption; if (searchText && - searchText != translate("Filter") && 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", {}); } function areFilters() { let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption; return searchText && searchText != translate("Filter"); } /** * 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 (areFilters()) 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); obj.selected = idx2; } function areDependenciesMet(folder) { let guiObject = Engine.GetGUIObjectByName("message"); for (let dependency of g_Mods[folder].dependencies) { if (isDependencyMet(dependency)) continue; guiObject.caption = coloredText( sprintf(translate('Dependency not met: %(dep)s'), { "dep": dependency }), g_ColorDependenciesNotMet); return false; } guiObject.caption = coloredText(translate('All dependencies met'), g_ColorDependenciesMet); return true; } /** * @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 => g_Mods[folder].name == name && (!operator || versionSatisfied(g_Mods[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] = g_Mods[folder].dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]); g_ModsEnabled.sort((folder1, folder2) => dependencies[folder1].indexOf(g_Mods[folder2].name) != -1 ? 1 : dependencies[folder2].indexOf(g_Mods[folder1].name) != -1 ? -1 : 0); g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled); } function selectedMod(listObjectName) { let listObject = Engine.GetGUIObjectByName(listObjectName); let otherListObject = Engine.GetGUIObjectByName(listObjectName == "modsDisabledList" ? "modsEnabledList" : "modsDisabledList"); let toggleModButton = Engine.GetGUIObjectByName("toggleModButton"); let modSelected = listObject.selected != -1; if (modSelected) { otherListObject.selected = -1; toggleModButton.onPress = listObjectName == "modsDisabledList" ? enableMod : disableMod; } Engine.GetGUIObjectByName("visitWebButton").enabled = modSelected && !!getSelectedModUrl(); toggleModButton.caption = listObjectName == "modsDisabledList" ? translateWithContext("mod activation", "Enable") : translateWithContext("mod activation", "Disable"); toggleModButton.enabled = modSelected; Engine.GetGUIObjectByName("enabledModUp").enabled = modSelected && listObjectName == "modsEnabledList" && !areFilters(); Engine.GetGUIObjectByName("enabledModDown").enabled = modSelected && listObjectName == "modsEnabledList" && !areFilters(); Engine.GetGUIObjectByName("globalModDescription").caption = listObject.list[listObject.selected] ? g_Mods[listObject.list[listObject.selected]].description : '[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]'; } /** * @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_folder[list.selected]; return folder && g_Mods[folder] && g_Mods[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/mod/gui/modmod/modmod.xml =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 24432) +++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 24433) @@ -1,205 +1,207 @@