Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 20645)
+++ ps/trunk/binaries/data/config/default.cfg (revision 20646)
@@ -1,471 +1,472 @@
; Global Configuration Settings
;
; **************************************************************
; * DO NOT EDIT THIS FILE if you want personal customisations: *
; * create a text file called "local.cfg" instead, and copy *
; * the lines from this file that you want to change. *
; * *
; * If a setting is part of a section (for instance [hotkey]) *
; * you need to append the section name at the beginning of *
; * your custom line (for instance you need to write *
; * "hotkey.pause = Space" if you want to change the pausing *
; * hotkey to the spacebar). *
; * *
; * On Linux, create: *
; * $XDG_CONFIG_HOME/0ad/config/local.cfg *
; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) *
; * *
; * On OS X, create: *
; * ~/Library/Application\ Support/0ad/config/local.cfg *
; * *
; * On Windows, create: *
; * %appdata%\0ad\config\local.cfg *
; * *
; **************************************************************
; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.)
windowed = false
; Show detailed tooltips (Unit stats)
showdetailedtooltips = false
; Pause the game on window focus loss (Only applicable to single player mode)
pauseonfocusloss = true
; Persist settings after leaving the game setup screen
persistmatchsettings = true
; Default player name to use in multiplayer
; playername = "anonymous"
; Default server name or IP to use in multiplayer
multiplayerserver = "127.0.0.1"
; Force a particular resolution. (If these are 0, the default is
; to keep the current desktop resolution in fullscreen mode or to
; use 1024x768 in windowed mode.)
xres = 0
yres = 0
; Force a non-standard bit depth (if 0 then use the current desktop bit depth)
bpp = 0
; Preferred display (for multidisplay setups, only works with SDL 2.0)
display = 0
; Emulate right-click with Ctrl+Click on Mac mice
macmouse = false
; System settings:
; if false, actors won't be rendered but anything entity will be.
renderactors = true
watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly.
waterfancyeffects = false
waterrealdepth = true
waterrefraction = true
waterreflection = true
shadowsonwater = false
shadows = true
shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High)
; High values can crash the game when using a graphics card with low memory!
shadowpcf = true
vsync = false
particles = true
fog = true
silhouettes = true
showsky = true
nos3tc = false
noautomipmap = true
novbo = false
noframebufferobject = false
; Disable hardware cursors
nohwcursor = false
; Linux only: Set the driconf force_s3tc_enable option at startup,
; for compressed texture support
force_s3tc_enable = true
; Specify the render path. This can be one of:
; default Automatically select one of the below, depending on system capabilities
; fixed Only use OpenGL fixed function pipeline
; shader Use vertex/fragment shaders for transform and lighting where possible
; Using 'fixed' instead of 'default' may work around some graphics-related problems,
; but will reduce performance and features when a modern graphics card is available.
renderpath = default
;;;;; EXPERIMENTAL ;;;;;
; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects.
preferglsl = false
; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk
gpuskinning = false
; Use smooth LOS interpolation
smoothlos = false
; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath.
postproc = false
; Quality level of shader effects (set to 10 to display all effects)
materialmgr.quality = 2.0
; Maximum distance to display parallax effect. Set to 0 to disable parallax.
materialmgr.PARALLAX_DIST.max = 150
; Maximum distance to display high quality parallax effect.
materialmgr.PARALLAX_HQ_DIST.max = 75
; Maximum distance to display very high quality parallax effect. Set to 30 to enable.
materialmgr.PARALLAX_VHQ_DIST.max = 0
;;;;;;;;;;;;;;;;;;;;;;;;
; Replace alpha-blending with alpha-testing, for performance experiments
forcealphatest = false
; Color of the sky (in "r g b" format)
skycolor = "0 0 0"
[adaptivefps]
session = 60 ; Throttle FPS in running games (prevents 100% CPU workload).
menu = 30 ; Throttle FPS in menus only.
[hotkey]
; Each one of the specified keys will trigger the action on the left
; for multiple-key combinations, separate keys with '+'.
; See keys.txt for the list of key names.
; > SYSTEM SETTINGS
exit = "Ctrl+Break", "Super+Q" ; Exit to desktop
cancel = Escape ; Close or cancel the current dialog box/popup
leave = Escape ; End current game or Exit
confirm = Return ; Confirm the current command
pause = Pause ; Pause/unpause game
screenshot = F2 ; Take PNG screenshot
bigscreenshot = "Shift+F2" ; Take large BMP screenshot
togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode
screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots
wireframe = "Alt+Shift+W" ; Toggle wireframe mode
silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes
showsky = "Alt+Z" ; Toggle sky
summary = "Ctrl+Tab" ; Toggle in-game summary
; > CLIPBOARD CONTROLS
copy = "Ctrl+C" ; Copy to clipboard
paste = "Ctrl+V" ; Paste from clipboard
cut = "Ctrl+X" ; Cut selected text and copy to the clipboard
; > CONSOLE SETTINGS
console.toggle = BackQuote, F9 ; Open/close console
; > OVERLAY KEYS
fps.toggle = "Alt+F" ; Toggle frame counter
realtime.toggle = "Alt+T" ; Toggle current display of computer time
session.devcommands.toggle = "Alt+D" ; Toggle developer commands panel
timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter
session.showstatusbars = Tab ; Toggle display of status bars
session.highlightguarding = PgDn ; Toggle highlight of guarding units
session.highlightguarded = PgUp ; Toggle highlight of guarded units
session.toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures
session.toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures
session.togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units
; > HOTKEYS ONLY
chat = Return ; Toggle chat window
teamchat = "T" ; Toggle chat window in team chat mode
privatechat = "L" ; Toggle chat window and select the previous private chat partner
; > QUICKSAVE
quicksave = "Shift+F5"
quickload = "Shift+F8"
[hotkey.camera]
reset = "R" ; Reset camera rotation to default.
follow = "F" ; Follow the first unit in the selection
rallypointfocus = unused ; Focus the camera on the rally point of the selected building
zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control)
zoom.out = Minus, NumMinus ; Zoom camera out (continuous control)
zoom.wheel.in = WheelUp ; Zoom camera in (stepped control)
zoom.wheel.out = WheelDown ; Zoom camera out (stepped control)
rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards
rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards
rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain
rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain
rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control)
rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control)
pan = MouseMiddle ; Enable scrolling by moving mouse
left = A, LeftArrow ; Scroll or rotate left
right = D, RightArrow ; Scroll or rotate right
up = W, UpArrow ; Scroll or rotate up/forwards
down = S, DownArrow ; Scroll or rotate down/backwards
scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed
scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed
rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed
rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed
zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed
zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed
[hotkey.camera.jump]
1 = F5 ; Jump to position N
2 = F6
3 = F7
4 = F8
;5 =
;6 =
;7 =
;8 =
;9 =
;10 =
[hotkey.camera.jump.set]
1 = "Ctrl+F5" ; Set jump position N
2 = "Ctrl+F6"
3 = "Ctrl+F7"
4 = "Ctrl+F8"
;5 =
;6 =
;7 =
;8 =
;9 =
;10 =
[hotkey.profile]
toggle = "F11" ; Enable/disable real-time profiler
save = "Shift+F11" ; Save current profiler data to logs/profile.txt
[hotkey.profile2]
toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler
[hotkey.selection]
add = Shift ; Add units to selection
milonly = Alt ; Add only military units to selection
idleonly = "I" ; Select only idle units
woundedonly = "O" ; Select only wounded units
remove = Ctrl ; Remove units from selection
cancel = Esc ; Un-select all units and cancel building placement
idleworker = Period ; Select next idle worker
idlewarrior = ForwardSlash ; Select next idle warrior
idleunit = BackSlash ; Select next idle unit
offscreen = Alt ; Include offscreen units in selection
[hotkey.selection.group.add]
0 = "Shift+0"
1 = "Shift+1"
2 = "Shift+2"
3 = "Shift+3"
4 = "Shift+4"
5 = "Shift+5"
6 = "Shift+6"
7 = "Shift+7"
8 = "Shift+8"
9 = "Shift+9"
[hotkey.selection.group.save]
0 = "Ctrl+0"
1 = "Ctrl+1"
2 = "Ctrl+2"
3 = "Ctrl+3"
4 = "Ctrl+4"
5 = "Ctrl+5"
6 = "Ctrl+6"
7 = "Ctrl+7"
8 = "Ctrl+8"
9 = "Ctrl+9"
[hotkey.selection.group.select]
0 = 0
1 = 1
2 = 2
3 = 3
4 = 4
5 = 5
6 = 6
7 = 7
8 = 8
9 = 9
[hotkey.session]
kill = Delete ; Destroy selected units
stop = "H" ; Stop the current action
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
move = unused ; Modifier to move to a point instead of another action (e.g. gather)
attack = Ctrl ; Modifier to attack instead of another action (e.g. capture)
attackmove = Ctrl ; Modifier to attackmove when clicking on a point
attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys)
garrison = Ctrl ; Modifier to garrison when clicking on building
autorallypoint = Ctrl ; Modifier to set the rally point on the building itself
guard = "G" ; Modifier to escort/guard when clicking on unit/building
patrol = "P" ; Modifier to patrol a unit
repair = "J" ; Modifier to repair when clicking on building/mechanical unit
queue = Shift ; Modifier to queue unit orders instead of replacing
orderone = Alt ; Modifier to order only one entity in selection.
batchtrain = Shift ; Modifier to train units in batches
massbarter = Shift ; Modifier to barter bunch of resources
masstribute = Shift ; Modifier to tribute bunch of resources
noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit
fulltradeswap = Shift ; Modifier to put the desired trade resource to 100%
unloadtype = Shift ; Modifier to unload all units of type
deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting
rotate.cw = RightBracket ; Rotate building placement preview clockwise
rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
[hotkey.session.gui]
toggle = "Alt+G" ; Toggle visibility of session GUI
menu.toggle = "F10" ; Toggle in-game menu
barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page
[hotkey.session.savedgames]
delete = Delete ; Delete the selected saved game asking confirmation
noconfirmation = Shift ; Do not ask confirmation when deleting a game
[hotkey.session.queueunit] ; > UNIT TRAINING
1 = "Z" ; add first unit type to queue
2 = "X" ; add second unit type to queue
3 = "C" ; add third unit type to queue
4 = "V" ; add fourth unit type to queue
5 = "B" ; add fivth unit type to queue
6 = "N" ; add sixth unit type to queue
7 = "M" ; add seventh unit type to queue
8 = Comma ; add eighth unit type to queue
[hotkey.session.timewarp]
fastforward = Space ; If timewarp mode enabled, speed up the game
rewind = Backspace ; If timewarp mode enabled, go back to earlier point in the game
[hotkey.tab]
next = "Alt+S" ; Show the next tab
prev = "Alt+W" ; Show the previous tab
[hotkey.text] ; > GUI TEXTBOX HOTKEYS
delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor
delete.right = "Ctrl+Del" ; Delete word to the right of cursor
move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor
move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor
[gui]
cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking)
scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays
[gui.gamesetup]
enabletips = true ; Enable/Disable tips during gamesetup (for newcomers)
assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled.
aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest)
+aibehavior = "random" ; Default behavior of the AI (random, generalist, aggressive or defensive)
[gui.session]
camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one?
timeelapsedcounter = false ; Show the game duration in the top right corner
batchtrainingsize = 5 ; Number of units to be trained per batch (when pressing the hotkey)
woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number
attackrange = true ; Display attack range overlays of selected defensive structures
aurasrange = true ; Display aura range overlays of selected units and structures
healrange = true ; Display heal range overlays of selected units
[gui.session.minimap]
blinkduration = 1.7 ; The blink duration while pinging
pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification
[gui.session.notifications]
attack = true ; Show a chat notification if you are attacked by another player
tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode
barter = true ; Show a chat notification to observers when a player bartered resources
phase = completed ; Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode. Possible values: none, completed, all.
[gui.splashscreen]
enable = true ; Enable/disable the splashscreen
version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch
[joystick] ; EXPERIMENTAL: joystick/gamepad settings
enable = false
deadzone = 8192
[joystick.camera]
pan.x = 0
pan.y = 1
rotate.x = 3
rotate.y = 2
zoom.in = 5
zoom.out = 4
[chat]
timestamp = true ; Show at which time chat messages have been sent
[chat.session]
extended = true ; Whether to display the chat history
[lobby]
history = 0 ; Number of past messages to display on join
room = "arena23" ; Default MUC room to join
server = "lobby.wildfiregames.com" ; Address of lobby server
xpartamupp = "wfgbot23" ; Name of the server-side xmpp client that manage games
buddies = "," ; Comma separated list of playernames that the current user has marked as buddies
[lobby.columns]
gamerating = false ; Show the average rating of the participating players in a column of the gamelist
[lobby.stun]
enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router.
; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding.
server = "lobby.wildfiregames.com" ; Address of the STUN server.
port = 3478 ; Port of the STUN server.
delay = 200 ; Duration in milliseconds that is waited between STUN messages.
; Smaller numbers speed up joins but also become less stable.
[mod]
enabledmods = "mod public"
[network]
duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join.
lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled.
observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached
[overlay]
fps = "false" ; Show frames per second in top right corner
realtime = "false" ; Show current system time in top right corner
netwarnings = "true" ; Show warnings if the network connection is bad
[profiler2]
autoenable = false ; Enable HTTP server output at startup (default off for security/performance)
gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available
gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available
gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available
[sound]
mastergain = 0.9
musicgain = 0.2
ambientgain = 0.6
actiongain = 0.7
uigain = 0.7
[sound.notify]
nick = true ; Play a sound when someone mentions your name in the lobby or game
[tinygettext]
debug = false ; Print error messages each time a translation for an English string is not found.
[userreport] ; Opt-in online user reporting system
url = "http://feedback.wildfiregames.com/report/upload/v1/"
[view] ; Camera control settings
scroll.speed = 120.0
scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed
rotate.x.speed = 1.2
rotate.x.min = 28.0
rotate.x.max = 60.0
rotate.x.default = 35.0
rotate.y.speed = 2.0
rotate.y.speed.wheel = 0.45
rotate.y.default = 0.0
rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed
drag.speed = 0.5
zoom.speed = 256.0
zoom.speed.wheel = 32.0
zoom.min = 50.0
zoom.max = 200.0
zoom.default = 120.0
zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed
pos.smoothness = 0.1
zoom.smoothness = 0.4
rotate.x.smoothness = 0.5
rotate.y.smoothness = 0.3
near = 2.0 ; Near plane distance
far = 4096.0 ; Far plane distance
fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide
height.smoothness = 0.5
height.min = 16
Index: ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.js (revision 20646)
@@ -1,53 +1,68 @@
+var g_AIBehaviorList = [{
+ "Name": "random",
+ "Title": translateWithContext("AI Behavior", "Random")
+}].concat(g_Settings.AIBehaviors);
+
var g_PlayerSlot;
-const g_AIDescriptions = [{
+var g_AIDescriptions = [{
"id": "",
"data": {
"name": translateWithContext("ai", "None"),
"description": translate("AI will be disabled for this player.")
}
}].concat(g_Settings.AIDescriptions);
+var g_AIControls = {
+ "aiSelection": {
+ "labels": g_AIDescriptions.map(ai => ai.data.name),
+ "selected": settings => g_AIDescriptions.findIndex(ai => ai.id == settings.id)
+ },
+ "aiDifficulty": {
+ "labels": prepareForDropdown(g_Settings.AIDifficulties).Title,
+ "selected": settings => settings.difficulty
+ },
+ "aiBehavior": {
+ "labels": prepareForDropdown(g_AIBehaviorList).Title,
+ "selected": settings => g_AIBehaviorList.findIndex(b => b.Name == settings.behavior)
+ }
+};
+
function init(settings)
{
// Remember the player ID that we change the AI settings for
g_PlayerSlot = settings.playerSlot;
- let aiSelection = Engine.GetGUIObjectByName("aiSelection");
- aiSelection.list = g_AIDescriptions.map(ai => ai.data.name);
- aiSelection.selected = g_AIDescriptions.findIndex(ai => ai.id == settings.id);
- aiSelection.hidden = !settings.isController;
-
- let aiSelectionText = Engine.GetGUIObjectByName("aiSelectionText");
- aiSelectionText.caption = aiSelection.list[aiSelection.selected];
- aiSelectionText.hidden = settings.isController;
-
- let aiDiff = Engine.GetGUIObjectByName("aiDifficulty");
- aiDiff.list = prepareForDropdown(g_Settings.AIDifficulties).Title;
- aiDiff.selected = settings.difficulty;
- aiDiff.hidden = !settings.isController;
-
- let aiDiffText = Engine.GetGUIObjectByName("aiDifficultyText");
- aiDiffText.caption = aiDiff.list[aiDiff.selected];
- aiDiffText.hidden = settings.isController;
+ for (let name in g_AIControls)
+ {
+ let control = Engine.GetGUIObjectByName(name);
+ control.list = g_AIControls[name].labels;
+ control.selected = g_AIControls[name].selected(settings);
+ control.hidden = !settings.isController;
+
+ let label = Engine.GetGUIObjectByName(name + "Text");
+ label.caption = control.list[control.selected];
+ label.hidden = settings.isController;
+ }
}
function selectAI(idx)
{
Engine.GetGUIObjectByName("aiDescription").caption = g_AIDescriptions[idx].data.description;
}
function returnAI(save = true)
{
let idx = Engine.GetGUIObjectByName("aiSelection").selected;
// Pop the page before calling the callback, so the callback runs
// in the parent GUI page's context
Engine.PopGuiPageCB({
"save": save,
"id": g_AIDescriptions[idx].id,
"name": g_AIDescriptions[idx].data.name,
"difficulty": Engine.GetGUIObjectByName("aiDifficulty").selected,
+ "behavior": g_AIBehaviorList[Engine.GetGUIObjectByName("aiBehavior").selected].Name,
"playerSlot": g_PlayerSlot
});
}
Index: ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.xml (revision 20645)
+++ ps/trunk/binaries/data/mods/public/gui/aiconfig/aiconfig.xml (revision 20646)
@@ -1,48 +1,56 @@
-
Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 20646)
@@ -1,429 +1,430 @@
/**
* Highlights the victory condition in the game-description.
*/
var g_DescriptionHighlight = "orange";
/**
* The rating assigned to lobby players who didn't complete a ranked 1v1 yet.
*/
var g_DefaultLobbyRating = 1200;
/**
* XEP-0172 doesn't restrict nicknames, but our lobby policy does.
* So use this human readable delimiter to separate buddy names in the config file.
*/
var g_BuddyListDelimiter = ",";
/**
* Returns the nickname without the lobby rating.
*/
function splitRatingFromNick(playerName)
{
let result = /^(\S+)\ \((\d+)\)$/g.exec(playerName);
if (!result)
return [playerName, ""];
return [result[1], +result[2]];
}
/**
* Array of playernames that the current user has marked as buddies.
*/
var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter);
/**
* Denotes which players are a lobby buddy of the current user.
*/
var g_BuddySymbol = '•';
/**
* Returns map description and preview image or placeholder.
*/
function getMapDescriptionAndPreview(mapType, mapName)
{
let mapData;
if (mapType == "random" && mapName == "random")
mapData = { "settings": { "Description": translate("A randomly selected map.") } };
else if (mapType == "random" && Engine.FileExists(mapName + ".json"))
mapData = Engine.ReadJSONFile(mapName + ".json");
else if (Engine.FileExists(mapName + ".xml"))
mapData = Engine.LoadMapSettings(mapName + ".xml");
return deepfreeze({
"description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."),
"preview": mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png"
});
}
/**
* Sets the mappreview image correctly.
* It needs to be cropped as the engine only allows loading square textures.
*
* @param {string} guiObject
* @param {string} filename
*/
function setMapPreviewImage(guiObject, filename)
{
Engine.GetGUIObjectByName(guiObject).sprite =
"cropped:" + 400 / 512 + "," + 300 / 512 + ":" +
"session/icons/mappreview/" + filename;
}
/**
* Returns a formatted string describing the player assignments.
* Needs g_CivData to translate!
*
* @param {object} playerDataArray - As known from gamesetup and simstate.
* @param {(string[]|false)} playerStates - One of "won", "defeated", "active" for each player.
* @returns {string}
*/
function formatPlayerInfo(playerDataArray, playerStates)
{
let playerDescriptions = {};
let playerIdx = 0;
for (let playerData of playerDataArray)
{
if (playerData == null || playerData.Civ && playerData.Civ == "gaia")
continue;
++playerIdx;
let teamIdx = playerData.Team;
let isAI = playerData.AI && playerData.AI != "";
let playerState = playerStates && playerStates[playerIdx] || playerData.State;
let isActive = !playerState || playerState == "active";
let playerDescription;
if (isAI)
{
if (playerData.Civ)
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
- playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)");
+ playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIbehavior)s %(AIname)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
- playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, %(state)s)");
+ playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIbehavior)s %(AIname)s, %(state)s)");
}
else
{
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
- playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s)");
+ playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIbehavior)s %(AIname)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
- playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIname)s, %(state)s)");
+ playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIbehavior)s %(AIname)s, %(state)s)");
}
}
else
{
if (playerData.Offline)
{
// Can only occur in the lobby for now, so no strings with civ needed
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (OFFLINE)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)");
}
else
{
if (playerData.Civ)
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s)");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)");
else
if (isActive)
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s");
else
// Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu
playerDescription = translate("%(playerName)s (%(state)s)");
}
}
// Sort player descriptions by team
if (!playerDescriptions[teamIdx])
playerDescriptions[teamIdx] = [];
playerDescriptions[teamIdx].push(sprintf(playerDescription, {
"playerName":
coloredText(
(g_Buddies.indexOf(splitRatingFromNick(playerData.Name)[0]) != -1 ? g_BuddySymbol + " " : "") +
escapeText(playerData.Name),
(typeof getPlayerColor == 'function' ?
(isAI ? "white" : getPlayerColor(splitRatingFromNick(playerData.Name)[0])) :
rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color))),
"civ":
!playerData.Civ ?
translate("Unknown Civilization") :
g_CivData && g_CivData[playerData.Civ] && g_CivData[playerData.Civ].Name ?
translate(g_CivData[playerData.Civ].Name) :
playerData.Civ,
"state":
playerState == "defeated" ?
translateWithContext("playerstate", "defeated") :
translateWithContext("playerstate", "won"),
"AIname": isAI ? translateAIName(playerData.AI) : "",
- "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : ""
+ "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "",
+ "AIbehavior": isAI ? translateAIBehavior(playerData.AIBehavior) : ""
}));
}
let teams = Object.keys(playerDescriptions);
if (teams.indexOf("observer") > -1)
teams.splice(teams.indexOf("observer"), 1);
let teamDescription = [];
// If there are no teams, merge all playersDescriptions
if (teams.length == 1)
teamDescription.push(playerDescriptions[teams[0]].join("\n"));
// If there are teams, merge "Team N:" + playerDescriptions
else
teamDescription = teams.map(team => {
let teamCaption = team == -1 ?
translate("No Team") :
sprintf(translate("Team %(team)s"), { "team": +team + 1 });
// Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby
return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + teamCaption + "[/font]",
"playerDescriptions": playerDescriptions[team].join("\n")
});
});
if (playerDescriptions.observer)
teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), {
"team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]",
"playerDescriptions": playerDescriptions.observer.join("\n")
}));
return teamDescription.join("\n\n");
}
/**
* Sets an additional map label, map preview image and describes the chosen gamesettings more closely.
*
* Requires g_GameAttributes and g_VictoryConditions.
*/
function getGameDescription(extended = false)
{
let titles = [];
let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || g_VictoryConditions.Default);
if (victoryIdx != -1)
{
let title = g_VictoryConditions.Title[victoryIdx];
if (g_VictoryConditions.Name[victoryIdx] == "wonder")
title = sprintf(
translatePluralWithContext(
"victory condition",
"Wonder (%(min)s minute)",
"Wonder (%(min)s minutes)",
g_GameAttributes.settings.WonderDuration
),
{ "min": g_GameAttributes.settings.WonderDuration }
);
let isCaptureTheRelic = g_VictoryConditions.Name[victoryIdx] == "capture_the_relic";
if (isCaptureTheRelic)
title = sprintf(
translatePluralWithContext(
"victory condition",
"Capture the Relic (%(min)s minute)",
"Capture the Relic (%(min)s minutes)",
g_GameAttributes.settings.RelicDuration
),
{ "min": g_GameAttributes.settings.RelicDuration }
);
titles.push({
"label": title,
"value": g_VictoryConditions.Description[victoryIdx]
});
if (isCaptureTheRelic)
titles.push({
"label": translate("Relic Count"),
"value": g_GameAttributes.settings.RelicCount
});
if (g_VictoryConditions.Name[victoryIdx] == "regicide")
if (g_GameAttributes.settings.RegicideGarrison)
titles.push({
"label": translate("Hero Garrison"),
"value": translate("Heroes can be garrisoned.")
});
else
titles.push({
"label": translate("Exposed Heroes"),
"value": translate("Heroes cannot be garrisoned, and they are vulnerable to raids.")
});
}
if (g_GameAttributes.settings.RatingEnabled &&
g_GameAttributes.settings.PlayerData.length == 2)
titles.push({
"label": translate("Rated game"),
"value": translate("When the winner of this match is determined, the lobby score will be adapted.")
});
if (g_GameAttributes.settings.LockTeams)
titles.push({
"label": translate("Locked Teams"),
"value": translate("Players can't change the initial teams.")
});
else
titles.push({
"label": translate("Diplomacy"),
"value": translate("Players can make alliances and declare war on allies.")
});
if (g_GameAttributes.settings.LastManStanding)
titles.push({
"label": translate("Last Man Standing"),
"value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.")
});
else
titles.push({
"label": translate("Allied Victory"),
"value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.")
});
if (extended)
{
titles.push({
"label": translate("Ceasefire"),
"value":
g_GameAttributes.settings.Ceasefire == 0 ?
translate("disabled") :
sprintf(translatePlural(
"For the first minute, other players will stay neutral.",
"For the first %(min)s minutes, other players will stay neutral.",
g_GameAttributes.settings.Ceasefire),
{ "min": g_GameAttributes.settings.Ceasefire })
});
titles.push({
"label": translate("Map Name"),
"value": translate(g_GameAttributes.settings.Name)
});
titles.push({
"label": translate("Map Type"),
"value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)]
});
if (g_GameAttributes.mapType == "random")
{
let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)];
if (mapSize)
titles.push({
"label": translate("Map Size"),
"value": mapSize
});
}
}
titles.push({
"label": translate("Map Description"),
"value":
g_GameAttributes.map == "random" ?
translate("Randomly selects a map from the list") :
g_GameAttributes.settings.Description ?
translate(g_GameAttributes.settings.Description) :
translate("Sorry, no description available.")
});
if (g_GameAttributes.settings.Biome)
{
let biome = g_Settings.Biomes.find(b => b.Id == g_GameAttributes.settings.Biome);
titles.push({
"label": translate("Biome"),
"value": biome ? biome.Title : translateWithContext("biome", "Random")
});
}
if (extended)
{
titles.push({
"label": translate("Starting Resources"),
"value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), {
"startingResourcesTitle":
g_StartingResources.Title[
g_StartingResources.Resources.indexOf(
g_GameAttributes.settings.StartingResources)],
"amount": g_GameAttributes.settings.StartingResources
})
});
titles.push({
"label": translate("Population Limit"),
"value":
g_PopulationCapacities.Title[
g_PopulationCapacities.Population.indexOf(
g_GameAttributes.settings.PopulationCap)]
});
titles.push({
"label": translate("Treasures"),
"value": g_GameAttributes.settings.DisableTreasures ?
translateWithContext("treasures", "Disabled") :
translateWithContext("treasures", "As defined by the map.")
});
titles.push({
"label": translate("Revealed Map"),
"value": g_GameAttributes.settings.RevealMap
});
titles.push({
"label": translate("Explored Map"),
"value": g_GameAttributes.settings.ExploreMap
});
titles.push({
"label": translate("Cheats"),
"value": g_GameAttributes.settings.CheatsEnabled
});
}
return titles.map(title => sprintf(translate("%(label)s %(details)s"), {
"label": coloredText(title.label, g_DescriptionHighlight),
"details":
title.value === true ? translateWithContext("gamesetup option", "enabled") :
title.value || translateWithContext("gamesetup option", "disabled")
})).join("\n");
}
/**
* Sets the win/defeat icon to indicate current player's state.
* @param {string} state - The current in-game state of the player.
* @param {string} imageID - The name of the XML image object to update.
*/
function setOutcomeIcon(state, imageID)
{
let image = Engine.GetGUIObjectByName(imageID);
if (state == "won")
{
image.sprite = "stretched:session/icons/victory.png";
image.tooltip = translate("Victorious");
}
else if (state == "defeated")
{
image.sprite = "stretched:session/icons/defeat.png";
image.tooltip = translate("Defeated");
}
}
Index: ps/trunk/binaries/data/mods/public/gui/common/settings.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 20646)
@@ -1,405 +1,436 @@
/**
* The maximum number of players that the engine supports.
* TODO: Maybe we can support more than 8 players sometime.
*/
const g_MaxPlayers = 8;
/**
* The maximum number of teams allowed.
*/
const g_MaxTeams = 4;
/**
* Directory containing all editable settings.
*/
const g_SettingsDirectory = "simulation/data/settings/";
/**
* Directory containing all biomes supported for random map scripts.
*/
const g_BiomesDirectory = "maps/random/rmbiome/biomes/";
/**
* An object containing all values given by setting name.
* Used by lobby, gamesetup, session, summary screen and replay menu.
*/
const g_Settings = loadSettingsValues();
/**
* Loads and translates all values of all settings which
* can be configured by dropdowns in the gamesetup.
*
* @returns {Object|undefined}
*/
function loadSettingsValues()
{
var settings = {
"AIDescriptions": loadAIDescriptions(),
"AIDifficulties": loadAIDifficulties(),
+ "AIBehaviors": loadAIBehaviors(),
"Ceasefire": loadCeasefire(),
"VictoryDurations": loadVictoryDuration(),
"GameSpeeds": loadSettingValuesFile("game_speeds.json"),
"MapTypes": loadMapTypes(),
"MapSizes": loadSettingValuesFile("map_sizes.json"),
"Biomes": loadBiomes(),
"PlayerDefaults": loadPlayerDefaults(),
"PopulationCapacities": loadPopulationCapacities(),
"StartingResources": loadSettingValuesFile("starting_resources.json"),
"VictoryConditions": loadVictoryConditions()
};
if (Object.keys(settings).some(key => settings[key] === undefined))
return undefined;
return deepfreeze(settings);
}
/**
* Returns an array of objects reflecting all possible values for a given setting.
*
* @param {string} filename
* @see simulation/data/settings/
* @returns {Array|undefined}
*/
function loadSettingValuesFile(filename)
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + filename);
if (!json || !json.Data)
{
error("Could not load " + filename + "!");
return undefined;
}
if (json.TranslatedKeys)
{
let keyContext = json.TranslatedKeys;
if (json.TranslationContext)
{
keyContext = {};
for (let key of json.TranslatedKeys)
keyContext[key] = json.TranslationContext;
}
translateObjectKeys(json.Data, keyContext);
}
return json.Data;
}
/**
* Loads the descriptions as defined in simulation/ai/.../data.json and loaded by ICmpAIManager.cpp.
*
* @returns {Array}
*/
function loadAIDescriptions()
{
var ais = Engine.GetAIs();
translateObjectKeys(ais, ["name", "description"]);
return ais.sort((a, b) => a.data.name.localeCompare(b.data.name));
}
/**
* Hardcoded, as modding is not supported without major changes.
* Notice the AI code parses the difficulty level by the index, not by name.
*
* @returns {Array}
*/
function loadAIDifficulties()
{
return [
{
"Name": "sandbox",
"Title": translateWithContext("aiDiff", "Sandbox")
},
{
"Name": "very easy",
"Title": translateWithContext("aiDiff", "Very Easy")
},
{
"Name": "easy",
"Title": translateWithContext("aiDiff", "Easy")
},
{
"Name": "medium",
"Title": translateWithContext("aiDiff", "Medium"),
"Default": true
},
{
"Name": "hard",
"Title": translateWithContext("aiDiff", "Hard")
},
{
"Name": "very hard",
"Title": translateWithContext("aiDiff", "Very Hard")
}
];
}
+function loadAIBehaviors()
+{
+ return [
+ {
+ "Name": "generalist",
+ "Title": translateWithContext("aiBehavior", "Generalist"),
+ "Default": true
+ },
+ {
+ "Name": "defensive",
+ "Title": translateWithContext("aiBehavior", "Defensive")
+ },
+ {
+ "Name": "aggressive",
+ "Title": translateWithContext("aiBehavior", "Aggressive")
+ }
+ ];
+}
+
/**
* Loads available victory times for victory conditions like Wonder and Capture the Relic.
*/
function loadVictoryDuration()
{
var jsonFile = "victory_times.json";
var json = Engine.ReadJSONFile(g_SettingsDirectory + jsonFile);
if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times))
{
error("Could not load " + jsonFile);
return undefined;
}
return json.Times.map(duration => ({
"Duration": duration,
"Default": duration == json.Default,
"Title": sprintf(translatePluralWithContext("victory duration", "%(min)s minute", "%(min)s minutes", duration), { "min": duration })
}));
}
/**
* Loads available ceasefire settings.
*
* @returns {Array|undefined}
*/
function loadCeasefire()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "ceasefire.json");
if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times))
{
error("Could not load ceasefire.json");
return undefined;
}
return json.Times.map(timeout => ({
"Duration": timeout,
"Default": timeout == json.Default,
"Title": timeout == 0 ? translateWithContext("ceasefire", "No ceasefire") :
sprintf(translatePluralWithContext("ceasefire", "%(minutes)s minute", "%(minutes)s minutes", timeout), { "minutes": timeout })
}));
}
/**
* Hardcoded, as modding is not supported without major changes.
*
* @returns {Array}
*/
function loadMapTypes()
{
return [
{
"Name": "skirmish",
"Title": translateWithContext("map", "Skirmish"),
"Description": translate("A map with a predefined landscape and number of players. Freely select the other gamesettings."),
"Default": true
},
{
"Name": "random",
"Title": translateWithContext("map", "Random"),
"Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams.")
},
{
"Name": "scenario",
"Title": translateWithContext("map", "Scenario"),
"Description": translate("A map with a predefined landscape and matchsettings.")
}
];
}
function loadBiomes()
{
return Engine.ListDirectoryFiles(g_BiomesDirectory, "*.json", false).map(file => {
let description = Engine.ReadJSONFile(file).Description;
return {
"Id": file.substr(g_BiomesDirectory.length).slice(0, -".json".length),
"Title": translateWithContext("biome definition", description.Title),
"Description": translateWithContext("biome definition", description.Description)
};
});
}
/**
* Loads available gametypes.
*
* @returns {Array|undefined}
*/
function loadVictoryConditions()
{
let subdir = "victory_conditions/";
let files = Engine.ListDirectoryFiles(g_SettingsDirectory + subdir, "*.json", false).map(
file => file.substr(g_SettingsDirectory.length));
let victoryConditions = files.map(file => {
let vc = loadSettingValuesFile(file);
if (vc)
vc.Name = file.substr(subdir.length, file.length - (subdir + ".json").length);
return vc;
});
if (victoryConditions.some(vc => vc == undefined))
return undefined;
// TODO: We might support enabling victory conditions separately sometime.
// Until then, we supplement the endless gametype here.
victoryConditions.push({
"Name": "endless",
"Title": translateWithContext("victory condition", "None"),
"Description": translate("Endless game."),
"Scripts": []
});
return victoryConditions;
}
/**
* Loads the default player settings (like civs and colors).
*
* @returns {Array|undefined}
*/
function loadPlayerDefaults()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "player_defaults.json");
if (!json || !json.PlayerData)
{
error("Could not load player_defaults.json");
return undefined;
}
return json.PlayerData;
}
/**
* Loads available population capacities.
*
* @returns {Array|undefined}
*/
function loadPopulationCapacities()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "population_capacities.json");
if (!json || json.Default === undefined || !json.PopulationCapacities || !Array.isArray(json.PopulationCapacities))
{
error("Could not load population_capacities.json");
return undefined;
}
return json.PopulationCapacities.map(population => ({
"Population": population,
"Default": population == json.Default,
"Title": population < 10000 ? population : translate("Unlimited")
}));
}
/**
* Creates an object with all values of that property of the given setting and
* finds the index of the default value.
*
* This allows easy copying of setting values to dropdown lists.
*
* @param {Array} settingValues
* @returns {Object|undefined}
*/
function prepareForDropdown(settingValues)
{
if (!settingValues)
return undefined;
let settings = { "Default": 0 };
for (let index in settingValues)
{
for (let property in settingValues[index])
{
if (property == "Default")
continue;
if (!settings[property])
settings[property] = [];
// Switch property and index
settings[property][index] = settingValues[index][property];
}
// Copy default value
if (settingValues[index].Default)
settings.Default = +index;
}
return deepfreeze(settings);
}
function getGameSpeedChoices(allowFastForward)
{
return prepareForDropdown(g_Settings.GameSpeeds.filter(speed => !speed.FastForward || allowFastForward));
}
/**
* Returns title or placeholder.
*
* @param {string} aiName - for example "petra"
*/
function translateAIName(aiName)
{
let description = g_Settings.AIDescriptions.find(ai => ai.id == aiName);
return description ? translate(description.data.name) : translateWithContext("AI name", "Unknown");
}
/**
* Returns title or placeholder.
*
* @param {Number} index - index of AIDifficulties
*/
function translateAIDifficulty(index)
{
let difficulty = g_Settings.AIDifficulties[index];
return difficulty ? difficulty.Title : translateWithContext("AI difficulty", "Unknown");
}
/**
* Returns title or placeholder.
*
+ * @param {string} aiBehavior - for example "defensive"
+ */
+function translateAIBehavior(aiBehavior)
+{
+ let behavior = g_Settings.AIBehaviors.find(b => b.Name == aiBehavior);
+ return behavior ? behavior.Title : translateWithContext("AI behavior", "Default");
+}
+
+/**
+ * Returns title or placeholder.
+ *
* @param {string} mapType - for example "skirmish"
* @returns {string}
*/
function translateMapType(mapType)
{
let type = g_Settings.MapTypes.find(t => t.Name == mapType);
return type ? type.Title : translateWithContext("map type", "Unknown");
}
/**
* Returns title or placeholder "Default".
*
* @param {Number} mapSize - tilecount
* @returns {string}
*/
function translateMapSize(tiles)
{
let mapSize = g_Settings.MapSizes.find(size => size.Tiles == +tiles);
return mapSize ? mapSize.Name : translateWithContext("map size", "Default");
}
/**
* Returns title or placeholder.
*
* @param {Number} population - for example 300
* @returns {string}
*/
function translatePopulationCapacity(population)
{
let popCap = g_Settings.PopulationCapacities.find(p => p.Population == population);
return popCap ? popCap.Title : translateWithContext("population capacity", "Unknown");
}
/**
* Returns title or placeholder.
*
* @param {string} gameType - for example "conquest"
* @returns {string}
*/
function translateVictoryCondition(gameType)
{
let victoryCondition = g_Settings.VictoryConditions.find(vc => vc.Name == gameType);
return victoryCondition ? victoryCondition.Title : translateWithContext("victory condition", "Unknown");
}
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 20646)
@@ -1,2448 +1,2457 @@
const g_MatchSettings_SP = "config/matchsettings.json";
const g_MatchSettings_MP = "config/matchsettings.mp.json";
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
var g_GameSpeeds = getGameSpeedChoices(false);
/**
* Offer users to select playable civs only.
* Load unselectable civs as they could appear in scenario maps.
*/
const g_CivData = loadCivData(false, false);
/**
* Highlight the "random" dropdownlist item.
*/
var g_ColorRandom = "orange";
/**
* Color for regular dropdownlist items.
*/
var g_ColorRegular = "white";
/**
* Color for "Unassigned"-placeholder item in the dropdownlist.
*/
var g_PlayerAssignmentColors = {
"player": g_ColorRegular,
"observer": "170 170 250",
"unassigned": "140 140 140",
"AI": "70 150 70"
};
/**
* Used for highlighting the sender of chat messages.
*/
var g_SenderFont = "sans-bold-13";
/**
* This yields [1, 2, ..., MaxPlayers].
*/
var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1);
/**
* Used for generating the botnames.
*/
var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
var g_PlayerTeamList = prepareForDropdown([{
"label": translateWithContext("team", "None"),
"id": -1
}].concat(
Array(g_MaxTeams).fill(0).map((v, i) => ({
"label": i + 1,
"id": i
}))
)
);
/**
* Number of relics: [1, ..., NumCivs]
*/
var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1);
var g_PlayerCivList = g_CivData && prepareForDropdown([{
"name": translateWithContext("civilization", "Random"),
"tooltip": translate("Picks one civilization at random when the game starts."),
"color": g_ColorRandom,
"code": "random"
}].concat(
Object.keys(g_CivData).filter(
civ => g_CivData[civ].SelectableInGameSetup
).map(civ => ({
"name": g_CivData[civ].Name,
"tooltip": g_CivData[civ].History,
"color": g_ColorRegular,
"code": civ
})).sort(sortNameIgnoreCase)
)
);
/**
* All selectable playercolors except gaia.
*/
var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
/**
* Directory containing all maps of the given type.
*/
var g_MapPath = {
"random": "maps/random/",
"scenario": "maps/scenarios/",
"skirmish": "maps/skirmishes/"
};
/**
* Containing the colors to highlight the ready status of players,
* the chat ready messages and
* the tooltips and captions for the ready button
*/
var g_ReadyData = [
{
"color": g_ColorRegular,
"chat": translate("* %(username)s is not ready."),
"caption": translate("I'm ready"),
"tooltip": translate("State that you are ready to play.")
},
{
"color": "green",
"chat": translate("* %(username)s is ready!"),
"caption": translate("Stay ready"),
"tooltip": translate("Stay ready even when the game settings change.")
},
{
"color": "150 150 250",
"chat": "",
"caption": translate("I'm not ready!"),
"tooltip": translate("State that you are not ready to play.")
}
];
/**
* Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
*/
var g_NetMessageTypes = {
"netstatus": msg => handleNetStatusMessage(msg),
"netwarn": msg => addNetworkWarning(msg),
"gamesetup": msg => handleGamesetupMessage(msg),
"players": msg => handlePlayerAssignmentMessage(msg),
"ready": msg => handleReadyMessage(msg),
"start": msg => handleGamestartMessage(msg),
"kicked": msg => addChatMessage({
"type": msg.banned ? "banned" : "kicked",
"username": msg.username
}),
"chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text }),
};
var g_FormatChatMessage = {
"system": (msg, user) => systemMessage(msg.text),
"settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
"connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
"disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
"kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
"banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
"chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
"username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
"message": escapeText(msg.text || "")
}),
"ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }),
"clientlist": (msg, user) => getUsernameList(),
};
var g_MapFilters = [
{
"id": "default",
"name": translateWithContext("map filter", "Default"),
"tooltip": translateWithContext("map filter", "All maps except naval and demo maps."),
"filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1),
"Default": true
},
{
"id": "naval",
"name": translate("Naval Maps"),
"tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."),
"filter": mapKeywords => mapKeywords.indexOf("naval") != -1
},
{
"id": "demo",
"name": translate("Demo Maps"),
"tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."),
"filter": mapKeywords => mapKeywords.indexOf("demo") != -1
},
{
"id": "new",
"name": translate("New Maps"),
"tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."),
"filter": mapKeywords => mapKeywords.indexOf("new") != -1
},
{
"id": "trigger",
"name": translate("Trigger Maps"),
"tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."),
"filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
},
{
"id": "all",
"name": translate("All Maps"),
"tooltip": translateWithContext("map filter", "Every map of the chosen maptype."),
"filter": mapKeywords => true
},
];
/**
* This contains only filters that have at least one map matching them.
*/
var g_MapFilterList;
/**
* Array of biome identifiers supported by the currently selected map.
*/
var g_BiomeList;
/**
* Whether this is a single- or multiplayer match.
*/
var g_IsNetworked;
/**
* Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame).
*/
var g_IsController;
/**
* Whether this is a tutorial.
*/
var g_IsTutorial;
/**
* To report the game to the lobby bot.
*/
var g_ServerName;
var g_ServerPort;
/**
* IP address and port of the STUN endpoint.
*/
var g_StunEndpoint;
/**
* Current username. Cannot contain whitespace.
*/
var g_Username = Engine.LobbyGetNick();
/**
* States whether the GUI is currently updated in response to network messages instead of user input
* and therefore shouldn't send further messages to the network.
*/
var g_IsInGuiUpdate = false;
/**
* Whether the current player is ready to start the game.
* 0 - not ready
* 1 - ready
* 2 - stay ready
*/
var g_IsReady = 0;
/**
* Ignore duplicate ready commands on init.
*/
var g_ReadyInit = true;
/**
* If noone has changed the ready status, we have no need to spam the settings changed message.
*
* <=0 - Suppressed settings message
* 1 - Will show settings message
* 2 - Host's initial ready, suppressed settings message
*/
var g_ReadyChanged = 2;
/**
* Used to prevent calling resetReadyData when starting a game.
*/
var g_GameStarted = false;
/**
* Selectable options (player, AI, unassigned) in the player assignment dropdowns and
* their colorized, textual representation.
*/
var g_PlayerAssignmentList = {};
/**
* Remembers which clients are assigned to which player slots and whether they are ready.
* The keys are guids or "local" in Singleplayer.
*/
var g_PlayerAssignments = {};
var g_DefaultPlayerData = [];
var g_GameAttributes = { "settings": {} };
/**
* List of translated words that can be used to autocomplete titles of settings
* and their values (for example playernames).
*/
var g_Autocomplete = [];
/**
* Array of strings formatted as displayed, including playername.
*/
var g_ChatMessages = [];
/**
* Filename and translated title of all maps, given the currently selected
* maptype and filter. Sorted by title, shown in the dropdown.
*/
var g_MapSelectionList = [];
/**
* Cache containing the mapsettings. Just-in-time loading.
*/
var g_MapData = {};
/**
* Wait one tick before initializing the GUI objects and
* don't process netmessages prior to that.
*/
var g_LoadingState = 0;
/**
* Send the current gamesettings to the lobby bot if the settings didn't change for this number of seconds.
*/
var g_GameStanzaTimeout = 2;
/**
* Index of the GUI timer.
*/
var g_GameStanzaTimer;
/**
* Only send a lobby update if something actually changed.
*/
var g_LastGameStanza;
/**
* Remembers if the current player viewed the AI settings of some playerslot.
*/
var g_LastViewedAIPlayer = -1;
/**
* Total number of units that the engine can run with smoothly.
* It means a 4v4 with 150 population can still run nicely, but more than that might "lag".
*/
var g_PopulationCapacityRecommendation = 1200;
/**
* Order in which the GUI elements will be shown.
* All valid options are required to appear here.
* The ones under "map" are shown in the map selection panel,
* the others in the "more options" dialog.
*/
var g_OptionOrderGUI = {
"map": [
"mapType",
"mapFilter",
"mapSelection",
"numPlayers",
"mapSize"
],
"more": [
"biome",
"gameSpeed",
"victoryCondition",
"relicCount",
"relicDuration",
"wonderDuration",
"populationCap",
"startingResources",
"ceasefire",
"regicideGarrison",
"exploreMap",
"revealMap",
"disableTreasures",
"disableSpies",
"lockTeams",
"lastManStanding",
"enableCheats",
"enableRating"
]
};
/**
* Contains the logic of all multiple-choice gamesettings.
*
* Logic
* ids - Array of identifier strings that indicate the selected value.
* default - Returns the index of the default value (not the value itself).
* defined - Whether a value for the setting is actually specified.
* get - The identifier of the currently selected value.
* select - Saves and processes the value of the selected index of the ids array.
*
* GUI
* title - The caption shown in the label.
* tooltip - A description shown when hovering the dropdown or a specific item.
* labels - Array of translated strings selectable for this dropdown.
* colors - Optional array of colors to tint the according dropdown items with.
* hidden - If hidden, both the label and dropdown won't be visible. (default: false)
* enabled - Only the label will be shown if the setting is disabled. (default: true)
* autocomplete - Marks whether to autocomplete translated values of the string. (default: undefined)
* If not undefined, must be a number that denotes the priority (higher numbers come first).
* If undefined, still autocompletes the translated title of the setting.
* initOrder - Options with lower values will be initialized first.
*/
var g_Dropdowns = {
"mapType": {
"title": () => translate("Map Type"),
"tooltip": (hoverIdx) => g_MapTypes.Description[hoverIdx] || translate("Select a map type."),
"labels": () => g_MapTypes.Title,
"ids": () => g_MapTypes.Name,
"default": () => g_MapTypes.Default,
"defined": () => g_GameAttributes.mapType !== undefined,
"get": () => g_GameAttributes.mapType,
"select": (itemIdx) => {
g_MapData = {};
g_GameAttributes.mapType = g_MapTypes.Name[itemIdx];
g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType];
delete g_GameAttributes.map;
if (g_GameAttributes.mapType != "scenario")
g_GameAttributes.settings = {
"PlayerData": clone(g_DefaultPlayerData.slice(0, 4))
};
reloadMapFilterList();
},
"autocomplete": 0,
"initOrder": 1
},
"mapFilter": {
"title": () => translate("Map Filter"),
"tooltip": (hoverIdx) => g_MapFilterList.tooltip[hoverIdx] || translate("Select a map filter."),
"labels": () => g_MapFilterList.name,
"ids": () => g_MapFilterList.id,
"default": () => g_MapFilterList.Default,
"defined": () => g_MapFilterList.id.indexOf(g_GameAttributes.mapFilter || "") != -1,
"get": () => g_GameAttributes.mapFilter,
"select": (itemIdx) => {
g_GameAttributes.mapFilter = g_MapFilterList.id[itemIdx];
delete g_GameAttributes.map;
reloadMapList();
},
"autocomplete": 0,
"initOrder": 2
},
"mapSelection": {
"title": () => translate("Select Map"),
"tooltip": (hoverIdx) => g_MapSelectionList.description[hoverIdx] || translate("Select a map to play on."),
"labels": () => g_MapSelectionList.name,
"colors": () => g_MapSelectionList.color,
"ids": () => g_MapSelectionList.file,
"default": () => 0,
"defined": () => g_GameAttributes.map !== undefined,
"get": () => g_GameAttributes.map,
"select": (itemIdx) => {
selectMap(g_MapSelectionList.file[itemIdx]);
},
"autocomplete": 0,
"initOrder": 3
},
"mapSize": {
"title": () => translate("Map Size"),
"tooltip": (hoverIdx) => g_MapSizes.Tooltip[hoverIdx] || translate("Select map size. (Larger sizes may reduce performance.)"),
"labels": () => g_MapSizes.Name,
"ids": () => g_MapSizes.Tiles,
"default": () => g_MapSizes.Default,
"defined": () => g_GameAttributes.settings.Size !== undefined,
"get": () => g_GameAttributes.settings.Size,
"select": (itemIdx) => {
g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx];
},
"hidden": () => g_GameAttributes.mapType != "random",
"autocomplete": 0,
"initOrder": 1000
},
"biome": {
"title": () => translate("Biome"),
"tooltip": (hoverIdx) => g_BiomeList && g_BiomeList.Description && g_BiomeList.Description[hoverIdx] || translate("Select the flora and fauna."),
"labels": () => g_BiomeList ? g_BiomeList.Title : [],
"colors": (itemIdx) => g_BiomeList ? g_BiomeList.Color : [],
"ids": () => g_BiomeList ? g_BiomeList.Id : [],
"default": () => 0,
"defined": () => g_GameAttributes.settings.Biome !== undefined,
"get": () => g_GameAttributes.settings.Biome,
"select": (itemIdx) => {
g_GameAttributes.settings.Biome = g_BiomeList && g_BiomeList.Id[itemIdx];
},
"hidden": () => !g_BiomeList,
"autocomplete": 0,
"initOrder": 1000
},
"numPlayers": {
"title": () => translate("Number of Players"),
"tooltip": (hoverIdx) => translate("Select number of players."),
"labels": () => g_NumPlayersList,
"ids": () => g_NumPlayersList,
"default": () => g_MaxPlayers - 1,
"defined": () => g_GameAttributes.settings.PlayerData !== undefined,
"get": () => g_GameAttributes.settings.PlayerData.length,
"enabled": () => g_GameAttributes.mapType == "random",
"select": (itemIdx) => {
let num = itemIdx + 1;
let pData = g_GameAttributes.settings.PlayerData;
g_GameAttributes.settings.PlayerData =
num > pData.length ?
pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) :
pData.slice(0, num);
unassignInvalidPlayers(num);
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
},
"initOrder": 1000
},
"populationCap": {
"title": () => translate("Population Cap"),
"tooltip": (hoverIdx) => {
let popCap = g_PopulationCapacities.Population[hoverIdx];
let players = g_GameAttributes.settings.PlayerData.length;
if (hoverIdx == -1 || popCap * players <= g_PopulationCapacityRecommendation)
return translate("Select population limit.");
return coloredText(
sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), {
"players": players,
"popCap": popCap
}),
"orange");
},
"labels": () => g_PopulationCapacities.Title,
"ids": () => g_PopulationCapacities.Population,
"default": () => g_PopulationCapacities.Default,
"defined": () => g_GameAttributes.settings.PopulationCap !== undefined,
"get": () => g_GameAttributes.settings.PopulationCap,
"select": (itemIdx) => {
g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"startingResources": {
"title": () => translate("Starting Resources"),
"tooltip": (hoverIdx) => {
return hoverIdx >= 0 ?
sprintf(translate("Initial amount of each resource: %(resources)s."), {
"resources": g_StartingResources.Resources[hoverIdx]
}) :
translate("Select the game's starting resources.");
},
"labels": () => g_StartingResources.Title,
"ids": () => g_StartingResources.Resources,
"default": () => g_StartingResources.Default,
"defined": () => g_GameAttributes.settings.StartingResources !== undefined,
"get": () => g_GameAttributes.settings.StartingResources,
"select": (itemIdx) => {
g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx];
},
"hidden": () => g_GameAttributes.mapType == "scenario",
"autocomplete": 0,
"initOrder": 1000
},
"ceasefire": {
"title": () => translate("Ceasefire"),
"tooltip": (hoverIdx) => translate("Set time where no attacks are possible."),
"labels": () => g_Ceasefire.Title,
"ids": () => g_Ceasefire.Duration,
"default": () => g_Ceasefire.Default,
"defined": () => g_GameAttributes.settings.Ceasefire !== undefined,
"get": () => g_GameAttributes.settings.Ceasefire,
"select": (itemIdx) => {
g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"victoryCondition": {
"title": () => translate("Victory Condition"),
"tooltip": (hoverIdx) => g_VictoryConditions.Description[hoverIdx] || translate("Select victory condition."),
"labels": () => g_VictoryConditions.Title,
"ids": () => g_VictoryConditions.Name,
"default": () => g_VictoryConditions.Default,
"defined": () => g_GameAttributes.settings.GameType !== undefined,
"get": () => g_GameAttributes.settings.GameType,
"select": (itemIdx) => {
g_GameAttributes.settings.GameType = g_VictoryConditions.Name[itemIdx];
g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[itemIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"autocomplete": 0,
"initOrder": 1000
},
"relicCount": {
"title": () => translate("Relic Count"),
"tooltip": (hoverIdx) => translate("Total number of relics spawned on the map. Relic victory is most realistic with only one or two relics. With greater numbers, the relics are important to capture to receive aura bonuses."),
"labels": () => g_RelicCountList,
"ids": () => g_RelicCountList,
"default": () => g_RelicCountList.indexOf(2),
"defined": () => g_GameAttributes.settings.RelicCount !== undefined,
"get": () => g_GameAttributes.settings.RelicCount,
"select": (itemIdx) => {
g_GameAttributes.settings.RelicCount = g_RelicCountList[itemIdx];
},
"hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic",
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"relicDuration": {
"title": () => translate("Relic Duration"),
"tooltip": (hoverIdx) => translate("Minutes until the player has achieved Relic Victory."),
"labels": () => g_VictoryDurations.Title,
"ids": () => g_VictoryDurations.Duration,
"default": () => g_VictoryDurations.Default,
"defined": () => g_GameAttributes.settings.RelicDuration !== undefined,
"get": () => g_GameAttributes.settings.RelicDuration,
"select": (itemIdx) => {
g_GameAttributes.settings.RelicDuration = g_VictoryDurations.Duration[itemIdx];
},
"hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic",
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"wonderDuration": {
"title": () => translate("Wonder Duration"),
"tooltip": (hoverIdx) => translate("Minutes until the player has achieved Wonder Victory."),
"labels": () => g_VictoryDurations.Title,
"ids": () => g_VictoryDurations.Duration,
"default": () => g_VictoryDurations.Default,
"defined": () => g_GameAttributes.settings.WonderDuration !== undefined,
"get": () => g_GameAttributes.settings.WonderDuration,
"select": (itemIdx) => {
g_GameAttributes.settings.WonderDuration = g_VictoryDurations.Duration[itemIdx];
},
"hidden": () => g_GameAttributes.settings.GameType != "wonder",
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"gameSpeed": {
"title": () => translate("Game Speed"),
"tooltip": (hoverIdx) => translate("Select game speed."),
"labels": () => g_GameSpeeds.Title,
"ids": () => g_GameSpeeds.Speed,
"default": () => g_GameSpeeds.Default,
"defined": () =>
g_GameAttributes.gameSpeed !== undefined &&
g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) != -1,
"get": () => g_GameAttributes.gameSpeed,
"select": (itemIdx) => {
g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[itemIdx];
},
"initOrder": 1000
},
};
/**
* These dropdowns provide a setting that is repeated once for each player
* (where playerIdx is starting from 0 for player 1).
*/
var g_PlayerDropdowns = {
"playerAssignment": {
"labels": (playerIdx) => g_PlayerAssignmentList.Name || [],
"colors": (playerIdx) => g_PlayerAssignmentList.Color || [],
"ids": (playerIdx) => g_PlayerAssignmentList.Choice || [],
"default": (playerIdx) => "ai:petra",
"defined": (playerIdx) => playerIdx < g_GameAttributes.settings.PlayerData.length,
"get": (playerIdx) => {
for (let guid in g_PlayerAssignments)
if (g_PlayerAssignments[guid].player == playerIdx + 1)
return "guid:" + guid;
for (let ai of g_Settings.AIDescriptions)
if (g_GameAttributes.settings.PlayerData[playerIdx].AI == ai.id)
return "ai:" + ai.id;
return "unassigned";
},
"select": (selectedIdx, playerIdx) => {
let choice = g_PlayerAssignmentList.Choice[selectedIdx];
if (choice == "unassigned" || choice.startsWith("ai:"))
{
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerIdx+1, "");
else if (g_PlayerAssignments.local.player == playerIdx+1)
g_PlayerAssignments.local.player = -1;
g_GameAttributes.settings.PlayerData[playerIdx].AI = choice.startsWith("ai:") ? choice.substr(3) : "";
}
else
swapPlayers(choice.substr("guid:".length), playerIdx);
},
"autocomplete": 100,
},
"playerTeam": {
"labels": (playerIdx) => g_PlayerTeamList.label,
"ids": (playerIdx) => g_PlayerTeamList.id,
"default": (playerIdx) => 0,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team !== undefined,
"get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team,
"select": (selectedIdx, playerIdx) => {
g_GameAttributes.settings.PlayerData[playerIdx].Team = selectedIdx - 1;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
},
"playerCiv": {
"tooltip": (hoverIdx, playerIdx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Chose the civilization for this player"),
"labels": (playerIdx) => g_PlayerCivList.name,
"colors": (playerIdx) => g_PlayerCivList.color,
"ids": (playerIdx) => g_PlayerCivList.code,
"default": (playerIdx) => 0,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ !== undefined,
"get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ,
"select": (selectedIdx, playerIdx) => {
g_GameAttributes.settings.PlayerData[playerIdx].Civ = g_PlayerCivList.code[selectedIdx];
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"autocomplete": 0,
},
"playerColorPicker": {
"labels": (playerIdx) => g_PlayerColorPickerList.map(color => "■"),
"colors": (playerIdx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)),
"ids": (playerIdx) => g_PlayerColorPickerList.map((color, index) => index),
"default": (playerIdx) => playerIdx,
"defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Color !== undefined,
"get": (playerIdx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[playerIdx].Color),
"select": (selectedIdx, playerIdx) => {
let playerData = g_GameAttributes.settings.PlayerData;
// If someone else has that color, give that player the old color
let sameColorPData = playerData.find(pData => sameColor(g_PlayerColorPickerList[selectedIdx], pData.Color));
if (sameColorPData)
sameColorPData.Color = playerData[playerIdx].Color;
playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx];
ensureUniquePlayerColors(playerData);
},
"enabled": () => g_GameAttributes.mapType != "scenario",
},
};
/**
* Contains the logic of all boolean gamesettings.
*/
var g_Checkboxes = {
"regicideGarrison": {
"title": () => translate("Hero Garrison"),
"tooltip": () => translate("Toggle whether heroes can be garrisoned."),
"default": () => false,
"defined": () => g_GameAttributes.settings.RegicideGarrison !== undefined,
"get": () => g_GameAttributes.settings.RegicideGarrison,
"set": checked => {
g_GameAttributes.settings.RegicideGarrison = checked;
},
"hidden": () => g_GameAttributes.settings.GameType != "regicide",
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"revealMap": {
"title": () =>
// Translation: Make sure to differentiate between the revealed map and explored map options!
translate("Revealed Map"),
"tooltip":
// Translation: Make sure to differentiate between the revealed map and explored map options!
() => translate("Toggle revealed map (see everything)."),
"default": () => false,
"defined": () => g_GameAttributes.settings.RevealMap !== undefined,
"get": () => g_GameAttributes.settings.RevealMap,
"set": checked => {
g_GameAttributes.settings.RevealMap = checked;
if (checked)
g_Checkboxes.exploreMap.set(true);
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"exploreMap": {
"title":
// Translation: Make sure to differentiate between the revealed map and explored map options!
() => translate("Explored Map"),
"tooltip":
// Translation: Make sure to differentiate between the revealed map and explored map options!
() => translate("Toggle explored map (see initial map)."),
"default": () => false,
"defined": () => g_GameAttributes.settings.ExploreMap !== undefined,
"get": () => g_GameAttributes.settings.ExploreMap,
"set": checked => {
g_GameAttributes.settings.ExploreMap = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap,
"initOrder": 1000
},
"disableTreasures": {
"title": () => translate("Disable Treasures"),
"tooltip": () => translate("Disable all treasures on the map."),
"default": () => false,
"defined": () => g_GameAttributes.settings.DisableTreasures !== undefined,
"get": () => g_GameAttributes.settings.DisableTreasures,
"set": checked => {
g_GameAttributes.settings.DisableTreasures = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"disableSpies": {
"title": () => translate("Disable Spies"),
"tooltip": () => translate("Disable spies during the game."),
"default": () => false,
"defined": () => g_GameAttributes.settings.DisableSpies !== undefined,
"get": () => g_GameAttributes.settings.DisableSpies,
"set": checked => {
g_GameAttributes.settings.DisableSpies = checked;
},
"enabled": () => g_GameAttributes.mapType != "scenario",
"initOrder": 1000
},
"lockTeams": {
"title": () => translate("Teams Locked"),
"tooltip": () => translate("Toggle locked teams."),
"default": () => Engine.HasXmppClient(),
"defined": () => g_GameAttributes.settings.LockTeams !== undefined,
"get": () => g_GameAttributes.settings.LockTeams,
"set": checked => {
g_GameAttributes.settings.LockTeams = checked;
g_GameAttributes.settings.LastManStanding = false;
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.RatingEnabled,
"initOrder": 1000
},
"lastManStanding": {
"title": () => translate("Last Man Standing"),
"tooltip": () => translate("Toggle whether the last remaining player or the last remaining set of allies wins."),
"default": () => false,
"defined": () => g_GameAttributes.settings.LastManStanding !== undefined,
"get": () => g_GameAttributes.settings.LastManStanding,
"set": checked => {
g_GameAttributes.settings.LastManStanding = checked;
},
"enabled": () =>
g_GameAttributes.mapType != "scenario" &&
!g_GameAttributes.settings.LockTeams,
"initOrder": 1000
},
"enableCheats": {
"title": () => translate("Cheats"),
"tooltip": () => translate("Toggle the usability of cheats."),
"default": () => !g_IsNetworked,
"hidden": () => !g_IsNetworked,
"defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined,
"get": () => g_GameAttributes.settings.CheatsEnabled,
"set": checked => {
g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked ||
checked && !g_GameAttributes.settings.RatingEnabled;
},
"enabled": () => !g_GameAttributes.settings.RatingEnabled,
"initOrder": 1000
},
"enableRating": {
"title": () => translate("Rated Game"),
"tooltip": () => translate("Toggle if this game will be rated for the leaderboard."),
"default": () => Engine.HasXmppClient(),
"hidden": () => !Engine.HasXmppClient(),
"defined": () => g_GameAttributes.settings.RatingEnabled !== undefined,
"get": () => !!g_GameAttributes.settings.RatingEnabled,
"set": checked => {
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined;
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
if (checked)
{
g_Checkboxes.lockTeams.set(true);
g_Checkboxes.enableCheats.set(false);
}
},
"initOrder": 1000
},
};
/**
* For setting up arbitrary GUI objects.
*/
var g_MiscControls = {
"chatPanel": {
"hidden": () => !g_IsNetworked,
},
"chatInput": {
"tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete playernames or settings.")),
},
"cheatWarningText": {
"hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled,
},
"cancelGame": {
"tooltip": () =>
Engine.HasXmppClient() ?
translate("Return to the lobby.") :
translate("Return to the main menu."),
},
"startGame": {
"caption": () =>
g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption,
"tooltip": (hoverIdx) =>
!g_IsController ?
g_ReadyData[g_IsReady].tooltip :
!g_IsNetworked || Object.keys(g_PlayerAssignments).every(guid =>
g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1) ?
translate("Start a new game with the current settings.") :
translate("Start a new game with the current settings (disabled until all players are ready)"),
"enabled": () => !g_IsController ||
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status ||
g_PlayerAssignments[guid].player == -1 ||
guid == Engine.GetPlayerGUID() && g_IsController),
"hidden": () =>
!g_PlayerAssignments[Engine.GetPlayerGUID()] ||
g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController,
},
"civResetButton": {
"hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
},
"teamResetButton": {
"hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
},
// Display these after having hidden every GUI object in the "More Options" dialog
"moreOptionsLabel": {
"hidden": () => false,
},
"hideMoreOptions": {
"hidden": () => false,
},
};
/**
* Contains gui elements that are repeated for every player.
*/
var g_PlayerMiscElements = {
"playerBox": {
"size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "),
},
"playerName": {
"caption": (playerIdx) => {
let pData = g_GameAttributes.settings.PlayerData[playerIdx];
let assignedGUID = Object.keys(g_PlayerAssignments).find(
guid => g_PlayerAssignments[guid].player == playerIdx + 1);
let name = translate(pData.Name || g_DefaultPlayerData[playerIdx].Name);
if (g_IsNetworked)
name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color);
return name;
},
},
"playerColor": {
"sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color) + " 100",
},
"playerConfig": {
"hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI,
"onPress": (playerIdx) => function() {
openAIConfig(playerIdx);
},
- "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(name)s - %(difficulty)s."), {
+ "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(difficulty)s %(behavior)s %(name)s."), {
"name": translateAIName(g_GameAttributes.settings.PlayerData[playerIdx].AI),
- "difficulty": translateAIDifficulty(g_GameAttributes.settings.PlayerData[playerIdx].AIDiff)
+ "difficulty": translateAIDifficulty(g_GameAttributes.settings.PlayerData[playerIdx].AIDiff),
+ "behavior": translateAIBehavior(g_GameAttributes.settings.PlayerData[playerIdx].AIBehavior),
}),
},
};
/**
* Initializes some globals without touching the GUI.
*
* @param {Object} attribs - context data sent by the lobby / mainmenu
*/
function init(attribs)
{
if (!g_Settings)
{
cancelSetup();
return;
}
if (["offline", "server", "client"].indexOf(attribs.type) == -1)
{
error("Unexpected 'type' in gamesetup init: " + attribs.type);
cancelSetup();
return;
}
g_IsNetworked = attribs.type != "offline";
g_IsController = attribs.type != "client";
g_IsTutorial = !!attribs.tutorial;
g_ServerName = attribs.serverName;
g_ServerPort = attribs.serverPort;
g_StunEndpoint = attribs.stunEndpoint;
if (!g_IsNetworked)
g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
// Replace empty playername when entering a singleplayermatch for the first time
if (!g_IsNetworked)
{
Engine.ConfigDB_CreateValue("user", "playername.singleplayer", singleplayerName());
Engine.ConfigDB_WriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg");
}
initDefaults();
supplementDefaults();
setTimeout(displayGamestateNotifications, 1000);
}
function initDefaults()
{
// Remove gaia from both arrays
g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1));
let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty");
+ let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior");
// Don't change the underlying defaults file, as Atlas uses that file too
for (let i in g_DefaultPlayerData)
{
g_DefaultPlayerData[i].Civ = "random";
g_DefaultPlayerData[i].Team = -1;
g_DefaultPlayerData[i].AIDiff = aiDifficulty;
+ g_DefaultPlayerData[i].AIBehavior = aiBehavior;
}
deepfreeze(g_DefaultPlayerData);
}
/**
* Sets default values for all g_GameAttribute settings which don't have a value set.
*/
function supplementDefaults()
{
for (let dropdown in g_Dropdowns)
if (!g_Dropdowns[dropdown].defined())
g_Dropdowns[dropdown].select(g_Dropdowns[dropdown].default());
for (let checkbox in g_Checkboxes)
if (!g_Checkboxes[checkbox].defined())
g_Checkboxes[checkbox].set(g_Checkboxes[checkbox].default());
for (let dropdown in g_PlayerDropdowns)
for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
if (!isControlArrayElementHidden(i) && !g_PlayerDropdowns[dropdown].defined(i))
g_PlayerDropdowns[dropdown].select(g_PlayerDropdowns[dropdown].default(i), i);
}
/**
* Called after the first tick.
*/
function initGUIObjects()
{
// Copy all initOrder values into one object
let initOrder = {};
for (let dropdown in g_Dropdowns)
initOrder[dropdown] = g_Dropdowns[dropdown].initOrder;
for (let checkbox in g_Checkboxes)
initOrder[checkbox] = g_Checkboxes[checkbox].initOrder;
// Sort the object on initOrder so we can init the options in an arbitrary order
for (let option of Object.keys(initOrder).sort((a, b) => initOrder[a] - initOrder[b]))
if (g_Dropdowns[option])
initDropdown(option);
else if (g_Checkboxes[option])
initCheckbox(option);
else
warn('The option "' + option + '" is not defined.');
for (let dropdown in g_PlayerDropdowns)
initPlayerDropdowns(dropdown);
resizeMoreOptionsWindow();
initSPTips();
loadPersistMatchSettings();
updateGameAttributes();
sendRegisterGameStanzaImmediate();
if (g_IsTutorial)
{
launchTutorial();
return;
}
// Don't lift the curtain until the controls are updated the first time
if (!g_IsNetworked)
hideLoadingWindow();
}
function hideLoadingWindow()
{
let loadingWindow = Engine.GetGUIObjectByName("loadingWindow");
if (loadingWindow.hidden)
return;
loadingWindow.hidden = true;
Engine.GetGUIObjectByName("setupWindow").hidden = false;
Engine.GetGUIObjectByName("chatInput").focus();
}
/**
* Options in the "More Options" or "Map" panel use a generic name.
* Player settings use custom names.
*/
function getGUIObjectNameFromSetting(name)
{
for (let panel in g_OptionOrderGUI)
{
let idx = g_OptionOrderGUI[panel].indexOf(name);
if (idx != -1)
return [
panel + "Option",
g_Dropdowns[name] ? "Dropdown" : "Checkbox",
"[" + idx + "]"
];
}
// Assume there is a GUI object with exactly that setting name
return [name, "", ""];
}
function initDropdown(name, playerIdx)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let data = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
dropdown.list = data.labels(playerIdx).map((label, id) =>
data.colors && data.colors(playerIdx) ?
coloredText(label, data.colors(playerIdx)[id]) :
label);
dropdown.list_data = data.ids(playerIdx);
dropdown.onSelectionChange = function() {
if (!g_IsController ||
g_IsInGuiUpdate ||
!this.list_data[this.selected] ||
data.hidden && data.hidden(playerIdx) ||
data.enabled && !data.enabled(playerIdx))
return;
data.select(this.selected, playerIdx);
supplementDefaults();
updateGameAttributes();
};
if (data.tooltip)
dropdown.onHoverChange = function() {
this.tooltip = data.tooltip(this.hovered, playerIdx);
};
}
function initPlayerDropdowns(name)
{
for (let i = 0; i < g_MaxPlayers; ++i)
initDropdown(name, i);
}
function initCheckbox(name)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
Engine.GetGUIObjectByName(guiName + guiType + guiIdx).onPress = function() {
let obj = g_Checkboxes[name];
if (!g_IsController ||
g_IsInGuiUpdate ||
obj.enabled && !obj.enabled() ||
obj.hidden && obj.hidden())
return;
obj.set(this.checked);
supplementDefaults();
updateGameAttributes();
};
}
function initSPTips()
{
if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true")
return;
Engine.GetGUIObjectByName("spTips").hidden = false;
Engine.GetGUIObjectByName("displaySPTips").checked = true;
Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile("gui/gamesetup/ai.txt"));
}
function saveSPTipsSetting()
{
let enabled = String(Engine.GetGUIObjectByName("displaySPTips").checked);
Engine.ConfigDB_CreateValue("user", "gui.gamesetup.enabletips", enabled);
Engine.ConfigDB_WriteValueToFile("user", "gui.gamesetup.enabletips", enabled, "config/user.cfg");
}
function verticallyDistributeGUIObjects(parent, objectHeight, ignore)
{
let yPos;
let parentObject = Engine.GetGUIObjectByName(parent);
for (let child of parentObject.children)
{
if (ignore.indexOf(child.name) != -1)
continue;
let childSize = child.size;
yPos = yPos || childSize.top;
if (child.hidden)
continue;
childSize.top = yPos;
childSize.bottom = yPos + objectHeight - 2;
child.size = childSize;
yPos += objectHeight;
}
return yPos;
}
/**
* Remove empty space in case of hidden options (like cheats, rating or victory duration)
*/
function resizeMoreOptionsWindow()
{
verticallyDistributeGUIObjects("mapOptions", 32, []);
let yPos = verticallyDistributeGUIObjects("moreOptions", 32, ["moreOptionsLabel"]);
// Resize the vertically centered window containing the options
let moreOptions = Engine.GetGUIObjectByName("moreOptions");
let mSize = moreOptions.size;
mSize.bottom = mSize.top + yPos + 20;
moreOptions.size = mSize;
}
/**
* Called when the client disconnects.
* The other cases from NetClient should never occur in the gamesetup.
*/
function handleNetStatusMessage(message)
{
if (message.status != "disconnected")
{
error("Unrecognised netstatus type " + message.status);
return;
}
cancelSetup();
reportDisconnect(message.reason, true);
}
/**
* Called whenever a client clicks on ready (or not ready).
*/
function handleReadyMessage(message)
{
--g_ReadyChanged;
if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
addChatMessage({
"type": "ready",
"status": message.status,
"guid": message.guid
});
g_PlayerAssignments[message.guid].status = message.status;
updateGUIObjects();
}
/**
* Called after every player is ready and the host decided to finally start the game.
*/
function handleGamestartMessage(message)
{
// Immediately inform the lobby server instead of waiting for the load to finish
if (g_IsController && Engine.HasXmppClient())
{
sendRegisterGameStanzaImmediate();
let clients = formatClientsForStanza();
Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked": g_IsNetworked,
"playerAssignments": g_PlayerAssignments,
"isController": g_IsController
});
}
/**
* Called whenever the host changed any setting.
*/
function handleGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
if (!!g_GameAttributes.settings.RatingEnabled)
{
g_GameAttributes.settings.CheatsEnabled = false;
g_GameAttributes.settings.LockTeams = true;
g_GameAttributes.settings.LastManStanding = false;
}
Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
resetReadyData();
updateGUIObjects();
hideLoadingWindow();
}
/**
* Called whenever a client joins/leaves or any gamesetting is changed.
*/
function handlePlayerAssignmentMessage(message)
{
let playerChange = false;
for (let guid in message.newAssignments)
if (!g_PlayerAssignments[guid])
{
onClientJoin(guid, message.newAssignments);
playerChange = true;
}
for (let guid in g_PlayerAssignments)
if (!message.newAssignments[guid])
{
onClientLeave(guid);
playerChange = true;
}
g_PlayerAssignments = message.newAssignments;
sanitizePlayerData(g_GameAttributes.settings.PlayerData);
updateGUIObjects();
if (playerChange)
sendRegisterGameStanzaImmediate();
else
sendRegisterGameStanza();
}
function onClientJoin(newGUID, newAssignments)
{
let playername = newAssignments[newGUID].name;
addChatMessage({
"type": "connect",
"guid": newGUID,
"username": playername
});
let isRejoiningPlayer = newAssignments[newGUID].player != -1;
// Assign the client (or only buddies if prefered) to an unused playerslot and rejoining players to their old slot
if (!isRejoiningPlayer && playername != newAssignments[Engine.GetPlayerGUID()].name)
{
let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers");
if (assignOption == "disabled" ||
assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername)[0]) == -1)
return;
}
let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v, i) =>
Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i + 1)
);
// Client is not and cannot become assigned as player
if (!isRejoiningPlayer && freeSlot == -1)
return;
// Assign the joining client to the free slot
if (g_IsController && !isRejoiningPlayer)
Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
resetReadyData();
}
function onClientLeave(guid)
{
addChatMessage({
"type": "disconnect",
"guid": guid
});
if (g_PlayerAssignments[guid].player != -1)
resetReadyData();
}
/**
* Doesn't translate, so that lobby clients can do that locally
* (even if they don't have that map).
*/
function getMapDisplayName(map)
{
if (map == "random")
return map;
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Name)
return map;
return mapData.settings.Name;
}
function getMapPreview(map)
{
let mapData = loadMapData(map);
if (!mapData || !mapData.settings || !mapData.settings.Preview)
return "nopreview.png";
return mapData.settings.Preview;
}
/**
* Filter maps with filterFunc and by chosen map type.
*
* @param {function} filterFunc - Filter function that should be applied.
* @return {Array} the maps that match the filterFunc and the chosen map type.
*/
function getFilteredMaps(filterFunc)
{
if (!g_MapPath[g_GameAttributes.mapType])
{
error("Unexpected map type: " + g_GameAttributes.mapType);
return [];
}
let mapFiles = g_GameAttributes.mapType == "random" ?
getJSONFileList(g_GameAttributes.mapPath) :
getXMLFileList(g_GameAttributes.mapPath);
let maps = [];
// TODO: Should verify these are valid maps before adding to list
for (let mapFile of mapFiles)
{
let file = g_GameAttributes.mapPath + mapFile;
let mapData = loadMapData(file);
if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || []))
continue;
maps.push({
"file": file,
"name": translate(getMapDisplayName(file)),
"color": g_ColorRegular,
"description": translate(mapData.settings.Description)
});
}
return maps;
}
/**
* Initialize the dropdown containing all map filters for the selected maptype.
*/
function reloadMapFilterList()
{
g_MapFilterList = prepareForDropdown(g_MapFilters.filter(
mapFilter => getFilteredMaps(mapFilter.filter).length
));
initDropdown("mapFilter");
reloadMapList();
}
/**
* Initialize the dropdown containing all maps for the selected maptype and mapfilter.
*/
function reloadMapList()
{
let filterID = g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter);
let filterFunc = g_MapFilterList.filter[filterID];
let mapList = getFilteredMaps(filterFunc).sort(sortNameIgnoreCase);
if (g_GameAttributes.mapType == "random")
mapList.unshift({
"file": "random",
"name": translateWithContext("map selection", "Random"),
"color": g_ColorRandom,
"description": translate("Pick any of the given maps at random.")
});
g_MapSelectionList = prepareForDropdown(mapList);
initDropdown("mapSelection");
}
function reloadBiomeList()
{
let biomeList;
if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
{
if (g_GameAttributes.settings.SupportedBiomes === true)
biomeList = g_Settings.Biomes;
else
biomeList = g_Settings.Biomes.filter(
biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
}
g_BiomeList = biomeList && prepareForDropdown(
[{
"Id": "random",
"Title": translateWithContext("biome", "Random"),
"Description": translate("Pick a biome at random."),
"Color": g_ColorRandom
}].concat(biomeList.map(biome => ({
"Id": biome.Id,
"Title": biome.Title,
"Description": biome.Description,
"Color": g_ColorRegular
}))));
initDropdown("biome");
}
function reloadGameSpeedChoices()
{
g_GameSpeeds = getGameSpeedChoices(Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player == -1));
initDropdown("gameSpeed");
supplementDefaults();
}
function loadMapData(name)
{
if (!name || !g_MapPath[g_GameAttributes.mapType])
return undefined;
if (name == "random")
return { "settings": { "Name": "", "Description": "" } };
if (!g_MapData[name])
g_MapData[name] = g_GameAttributes.mapType == "random" ?
Engine.ReadJSONFile(name + ".json") :
Engine.LoadMapSettings(name);
return g_MapData[name];
}
/**
* Sets the gameattributes the way they were the last time the user left the gamesetup.
*/
function loadPersistMatchSettings()
{
if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial)
return;
let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
if (!Engine.FileExists(settingsFile))
return;
let attrs = Engine.ReadJSONFile(settingsFile);
if (!attrs || !attrs.settings)
return;
g_IsInGuiUpdate = true;
let mapName = attrs.map || "";
let mapSettings = attrs.settings;
g_GameAttributes = attrs;
if (!g_IsNetworked)
mapSettings.CheatsEnabled = true;
// Replace unselectable civs with random civ
let playerData = mapSettings.PlayerData;
if (playerData && g_GameAttributes.mapType != "scenario")
for (let i in playerData)
if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
playerData[i].Civ = "random";
// Apply map settings
let newMapData = loadMapData(mapName);
if (newMapData && newMapData.settings)
{
for (let prop in newMapData.settings)
mapSettings[prop] = newMapData.settings[prop];
if (playerData)
mapSettings.PlayerData = playerData;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Reload, as the maptype or mapfilter might have changed
reloadMapFilterList();
reloadBiomeList();
g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
supplementDefaults();
g_IsInGuiUpdate = false;
}
function savePersistMatchSettings()
{
if (g_IsTutorial)
return;
let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {};
Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes);
}
function sanitizePlayerData(playerData)
{
// Remove gaia
if (playerData.length && !playerData[0])
playerData.shift();
playerData.forEach((pData, index) => {
// Use defaults if the map doesn't specify a value
for (let prop in g_DefaultPlayerData[index])
if (!(prop in pData))
pData[prop] = clone(g_DefaultPlayerData[index][prop]);
// Replace colors with the best matching color of PlayerDefaults
if (g_GameAttributes.mapType != "scenario")
{
let colorDistances = g_PlayerColorPickerList.map(color => colorDistance(color, pData.Color));
let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
pData.Color = g_PlayerColorPickerList.find(color => colorDistance(color, pData.Color) == smallestDistance);
}
// If there is a player in that slot, then there can't be an AI
if (Object.keys(g_PlayerAssignments).some(guid => g_PlayerAssignments[guid].player == index + 1))
pData.AI = "";
});
ensureUniquePlayerColors(playerData);
}
function cancelSetup()
{
if (g_IsController)
savePersistMatchSettings();
Engine.DisconnectNetworkGame();
if (Engine.HasXmppClient())
{
Engine.LobbySetPlayerPresence("available");
if (g_IsController)
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_lobby.xml");
}
else
Engine.SwitchGuiPage("page_pregame.xml");
}
/**
* Can't init the GUI before the first tick.
* Process netmessages afterwards.
*/
function onTick()
{
if (!g_Settings)
return;
// First tick happens before first render, so don't load yet
if (g_LoadingState == 0)
++g_LoadingState;
else if (g_LoadingState == 1)
{
initGUIObjects();
++g_LoadingState;
}
else if (g_LoadingState == 2)
handleNetMessages();
updateTimers();
}
/**
* Handles all pending messages sent by the net client.
*/
function handleNetMessages()
{
while (g_IsNetworked)
{
let message = Engine.PollNetworkClient();
if (!message)
break;
log("Net message: " + uneval(message));
if (g_NetMessageTypes[message.type])
g_NetMessageTypes[message.type](message);
else
error("Unrecognised net message type " + message.type);
}
}
/**
* Called when the map or the number of players changes.
*/
function unassignInvalidPlayers(maxPlayers)
{
if (g_IsNetworked)
// Remove invalid playerIDs from the servers playerassignments copy
for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID)
Engine.AssignNetworkPlayer(playerID, "");
else if (g_PlayerAssignments.local.player > maxPlayers)
g_PlayerAssignments.local.player = -1;
}
function ensureUniquePlayerColors(playerData)
{
for (let i = playerData.length - 1; i >= 0; --i)
// If someone else has that color, assign an unused color
if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
playerData[i].Color = g_PlayerColorPickerList.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
}
function selectMap(name)
{
// Reset some map specific properties which are not necessarily redefined on each map
for (let prop of ["TriggerScripts", "CircularMap", "Garrison", "DisabledTemplates", "Biome", "SupportedBiomes"])
g_GameAttributes.settings[prop] = undefined;
let mapData = loadMapData(name);
let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {};
// Reset victory conditions
if (g_GameAttributes.mapType != "random")
{
let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx];
g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx];
}
if (g_GameAttributes.mapType == "scenario")
{
delete g_GameAttributes.settings.RelicDuration;
delete g_GameAttributes.settings.WonderDuration;
delete g_GameAttributes.settings.LastManStanding;
delete g_GameAttributes.settings.RegicideGarrison;
}
if (mapSettings.PlayerData)
sanitizePlayerData(mapSettings.PlayerData);
// Copy any new settings
g_GameAttributes.map = name;
g_GameAttributes.script = mapSettings.Script;
if (g_GameAttributes.map !== "random")
for (let prop in mapSettings)
g_GameAttributes.settings[prop] = mapSettings[prop];
reloadBiomeList();
unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
supplementDefaults();
}
function isControlArrayElementHidden(playerIdx)
{
return playerIdx !== undefined && playerIdx >= g_GameAttributes.settings.PlayerData.length;
}
/**
* @param playerIdx - Only specified for dropdown arrays.
*/
function updateGUIDropdown(name, playerIdx = undefined)
{
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx + idxName);
let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx + idxName);
let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx + idxName);
if (guiType == "Dropdown")
Engine.GetGUIObjectByName(guiName + "Checkbox" + guiIdx).hidden = true;
let indexHidden = isControlArrayElementHidden(playerIdx);
let obj = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
let selected = indexHidden ? -1 : dropdown.list_data.indexOf(String(obj.get(playerIdx)));
let enabled = !indexHidden && (!obj.enabled || obj.enabled(playerIdx));
let hidden = indexHidden || obj.hidden && obj.hidden(playerIdx);
dropdown.hidden = !g_IsController || !enabled || hidden;
dropdown.selected = selected;
dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, playerIdx) : "";
if (frame)
frame.hidden = hidden;
if (title && obj.title && !indexHidden)
title.caption = sprintf(translate("%(option)s:"), { "option": obj.title(playerIdx) });
if (label && !indexHidden)
{
label.hidden = g_IsController && enabled || hidden;
label.caption = selected == -1 ? translateWithContext("option value", "Unknown") : dropdown.list[selected];
}
}
/**
* Not used for the player assignments, so playerCheckboxes are not implemented,
* hence no index.
*/
function updateGUICheckbox(name)
{
let obj = g_Checkboxes[name];
let checked = obj.get();
let hidden = obj.hidden && obj.hidden();
let enabled = !obj.enabled || obj.enabled();
let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
let checkbox = Engine.GetGUIObjectByName(guiName + guiType + guiIdx);
let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx);
let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx);
let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx);
if (guiType == "Checkbox")
Engine.GetGUIObjectByName(guiName + "Dropdown" + guiIdx).hidden = true;
checkbox.checked = checked;
checkbox.enabled = enabled;
checkbox.hidden = hidden || !g_IsController;
checkbox.tooltip = obj.tooltip ? obj.tooltip() : "";
label.caption = checked ? translate("Yes") : translate("No");
label.hidden = hidden || g_IsController;
if (frame)
frame.hidden = hidden;
if (title && obj.title)
title.caption = sprintf(translate("%(option)s:"), { "option": obj.title() });
}
function updateGUIMiscControl(name, playerIdx)
{
let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
let obj = (playerIdx === undefined ? g_MiscControls : g_PlayerMiscElements)[name];
let control = Engine.GetGUIObjectByName(name + idxName);
if (!control)
warn("No GUI object with name '" + name + "'");
let hide = isControlArrayElementHidden(playerIdx);
control.hidden = hide;
if (hide)
return;
for (let property in obj)
control[property] = obj[property](playerIdx);
}
function launchGame()
{
if (!g_IsController)
{
error("Only host can start game");
return;
}
if (!g_GameAttributes.map)
return;
savePersistMatchSettings();
// Select random map
if (g_GameAttributes.map == "random")
{
let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts;
let gameTypeSelected = g_GameAttributes.settings.GameType;
selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1)));
g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected;
g_GameAttributes.settings.GameType = gameTypeSelected;
}
if (g_GameAttributes.settings.Biome == "random")
g_GameAttributes.settings.Biome = pickRandom(
g_GameAttributes.settings.SupportedBiomes === true ?
g_BiomeList.Id.slice(1) :
g_GameAttributes.settings.SupportedBiomes);
g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
// Prevent reseting the readystate
g_GameStarted = true;
g_GameAttributes.settings.mapType = g_GameAttributes.mapType;
// Get a unique array of selectable cultures
let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
// Determine random civs and botnames
for (let i in g_GameAttributes.settings.PlayerData)
{
// Pick a random civ of a random culture
let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
if (chosenCiv == "random")
{
let culture = pickRandom(cultures);
chosenCiv = pickRandom(Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture));
}
g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
- // Pick one of the available botnames for the chosen civ
+ // Pick a random behavior and one of the available botnames for the chosen civ
if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
continue;
+ if (g_GameAttributes.settings.PlayerData[i].AIBehavior == "random")
+ g_GameAttributes.settings.PlayerData[i].AIBehavior = pickRandom(g_Settings.AIBehaviors).Name;
+
let chosenName = pickRandom(g_CivData[chosenCiv].AINames);
if (!g_IsNetworked)
chosenName = translate(chosenName);
// Count how many players use the chosenName
let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName :
sprintf(translate("%(playerName)s %(romanNumber)s"), {
"playerName": chosenName,
"romanNumber": g_RomanNumbers[usedName+1]
});
}
// Copy playernames for the purpose of replays
for (let guid in g_PlayerAssignments)
{
let player = g_PlayerAssignments[guid];
if (player.player > 0) // not observer or GAIA
g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
}
// Seed used for both map generation and simulation
g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32));
g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32));
// Used for identifying rated game reports for the lobby
g_GameAttributes.matchID = Engine.GetMatchID();
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
Engine.StartNetworkGame();
}
else
{
// Find the player ID which the user has been assigned to
let playerID = -1;
for (let i in g_GameAttributes.settings.PlayerData)
{
let assignBox = Engine.GetGUIObjectByName("playerAssignment[" + i + "]");
if (assignBox.list_data[assignBox.selected] == "guid:local")
playerID = +i + 1;
}
Engine.StartGame(g_GameAttributes, playerID);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": g_GameAttributes,
"isNetworked": g_IsNetworked,
"playerAssignments": g_PlayerAssignments
});
}
}
function launchTutorial()
{
g_GameAttributes.mapType = "scenario";
selectMap("maps/tutorials/starting_economy_walkthrough");
launchGame();
}
/**
* Don't set any attributes here, just show the changes in the GUI.
*
* Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
*/
function updateGUIObjects()
{
g_IsInGuiUpdate = true;
reloadMapFilterList();
reloadBiomeList();
reloadGameSpeedChoices();
reloadPlayerAssignmentChoices();
// Hide exceeding dropdowns and checkboxes
for (let panel in g_OptionOrderGUI)
for (let child of Engine.GetGUIObjectByName(panel + "Options").children)
child.hidden = true;
// Show the relevant ones
for (let name in g_Dropdowns)
updateGUIDropdown(name);
for (let name in g_Checkboxes)
updateGUICheckbox(name);
for (let i = 0; i < g_MaxPlayers; ++i)
{
for (let name in g_PlayerDropdowns)
updateGUIDropdown(name, i);
for (let name in g_PlayerMiscElements)
updateGUIMiscControl(name, i);
}
for (let name in g_MiscControls)
updateGUIMiscControl(name);
updateGameDescription();
resizeMoreOptionsWindow();
rightAlignCancelButton();
updateAutocompleteEntries();
g_IsInGuiUpdate = false;
// Refresh AI config page
if (g_LastViewedAIPlayer != -1)
{
Engine.PopGuiPage();
openAIConfig(g_LastViewedAIPlayer);
}
}
function rightAlignCancelButton()
{
let offset = 10;
let startGame = Engine.GetGUIObjectByName("startGame");
let right = startGame.hidden ? startGame.size.right : startGame.size.left - offset;
let cancelGame = Engine.GetGUIObjectByName("cancelGame");
let cancelGameSize = cancelGame.size;
let buttonWidth = cancelGameSize.right - cancelGameSize.left;
cancelGameSize.right = right;
right -= buttonWidth;
for (let element of ["cheatWarningText", "onscreenToolTip"])
{
let elementSize = Engine.GetGUIObjectByName(element).size;
elementSize.right = right - (cancelGameSize.left - elementSize.right);
Engine.GetGUIObjectByName(element).size = elementSize;
}
cancelGameSize.left = right;
cancelGame.size = cancelGameSize;
}
function updateGameDescription()
{
setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map));
Engine.GetGUIObjectByName("mapInfoName").caption =
translateMapTitle(getMapDisplayName(g_GameAttributes.map));
Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription();
}
/**
* Broadcast the changed settings to all clients and the lobbybot.
*/
function updateGameAttributes()
{
if (g_IsInGuiUpdate || !g_IsController)
return;
if (g_IsNetworked)
{
Engine.SetNetworkGameAttributes(g_GameAttributes);
if (g_LoadingState >= 2)
sendRegisterGameStanza();
resetReadyData();
}
else
updateGUIObjects();
}
function openAIConfig(playerSlot)
{
g_LastViewedAIPlayer = playerSlot;
Engine.PushGuiPage("page_aiconfig.xml", {
"callback": "AIConfigCallback",
"isController": g_IsController,
"playerSlot": playerSlot,
"id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
- "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff
+ "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
+ "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
});
}
/**
* Called after closing the dialog.
*/
function AIConfigCallback(ai)
{
g_LastViewedAIPlayer = -1;
if (!ai.save || !g_IsController)
return;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
+ g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
updateGameAttributes();
}
function reloadPlayerAssignmentChoices()
{
let playerChoices = sortGUIDsByPlayerID().map(guid => ({
"Choice": "guid:" + guid,
"Color": g_PlayerAssignments[guid].player == -1 ? g_PlayerAssignmentColors.observer : g_PlayerAssignmentColors.player,
"Name": g_PlayerAssignments[guid].name
}));
// Only display hidden AIs if the map preselects them
let aiChoices = g_Settings.AIDescriptions
.filter(ai => !ai.data.hidden || g_GameAttributes.settings.PlayerData.some(pData => pData.AI == ai.id))
.map(ai => ({
"Choice": "ai:" + ai.id,
"Name": sprintf(translate("AI: %(ai)s"), {
"ai": translate(ai.data.name)
}),
"Color": g_PlayerAssignmentColors.AI
}));
let unassignedSlot = [{
"Choice": "unassigned",
"Name": translate("Unassigned"),
"Color": g_PlayerAssignmentColors.unassigned
}];
g_PlayerAssignmentList = prepareForDropdown(playerChoices.concat(aiChoices).concat(unassignedSlot));
initPlayerDropdowns("playerAssignment");
}
function swapPlayers(guidToSwap, newSlot)
{
// Player slots are indexed from 0 as Gaia is omitted.
let newPlayerID = newSlot + 1;
let playerID = g_PlayerAssignments[guidToSwap].player;
// Attempt to swap the player or AI occupying the target slot,
// if any, into the slot this player is currently in.
if (playerID != -1)
{
for (let guid in g_PlayerAssignments)
{
// Move the player in the destination slot into the current slot.
if (g_PlayerAssignments[guid].player != newPlayerID)
continue;
if (g_IsNetworked)
Engine.AssignNetworkPlayer(playerID, guid);
else
g_PlayerAssignments[guid].player = playerID;
break;
}
// Transfer the AI from the target slot to the current slot.
g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff;
+ g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior;
// Swap civilizations and colors if they aren't fixed
if (g_GameAttributes.mapType != "scenario")
{
[g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] =
[g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ];
[g_GameAttributes.settings.PlayerData[playerID - 1].Color, g_GameAttributes.settings.PlayerData[newSlot].Color] =
[g_GameAttributes.settings.PlayerData[newSlot].Color, g_GameAttributes.settings.PlayerData[playerID - 1].Color];
}
}
if (g_IsNetworked)
Engine.AssignNetworkPlayer(newPlayerID, guidToSwap);
else
g_PlayerAssignments[guidToSwap].player = newPlayerID;
g_GameAttributes.settings.PlayerData[newSlot].AI = "";
}
function submitChatInput()
{
let input = Engine.GetGUIObjectByName("chatInput");
let text = input.caption;
if (!text.length)
return;
input.caption = "";
if (executeNetworkCommand(text))
return;
Engine.SendNetworkChat(text);
}
function senderFont(text)
{
return '[font="' + g_SenderFont + '"]' + text + '[/font]';
}
function systemMessage(message)
{
return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
}
function colorizePlayernameByGUID(guid, username = "")
{
// TODO: Maybe the host should have the moderator-prefix?
if (!username)
username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
let color = g_ColorRegular;
if (playerID > 0)
{
color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
// Enlighten playercolor to improve readability
let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
color = rgbToGuiColor({ "r": r, "g": g, "b": b });
}
return coloredText(username, color);
}
function addChatMessage(msg)
{
if (!g_FormatChatMessage[msg.type])
return;
if (msg.type == "chat")
{
let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
if (userName != g_PlayerAssignments[msg.guid].name &&
msg.text.toLowerCase().indexOf(splitRatingFromNick(userName)[0].toLowerCase()) != -1)
soundNotification("nick");
}
let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
let text = g_FormatChatMessage[msg.type](msg, user);
if (!text)
return;
if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
text = sprintf(translate("%(time)s %(message)s"), {
"time": sprintf(translate("\\[%(time)s]"), {
"time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm"))
}),
"message": text
});
g_ChatMessages.push(text);
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
function showMoreOptions(show)
{
Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show;
Engine.GetGUIObjectByName("moreOptions").hidden = !show;
}
function resetCivilizations()
{
for (let i in g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData[i].Civ = "random";
updateGameAttributes();
}
function resetTeams()
{
for (let i in g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData[i].Team = -1;
updateGameAttributes();
}
function toggleReady()
{
setReady((g_IsReady + 1) % 3, true);
}
function setReady(ready, sendMessage)
{
g_IsReady = ready;
if (sendMessage)
Engine.SendNetworkReady(g_IsReady);
updateGUIObjects();
}
function resetReadyData()
{
if (g_GameStarted)
return;
if (g_ReadyChanged < 1)
addChatMessage({ "type": "settings" });
else if (g_ReadyChanged == 2 && !g_ReadyInit)
return; // duplicate calls on init
else
g_ReadyInit = false;
g_ReadyChanged = 2;
if (!g_IsNetworked)
g_IsReady = 2;
else if (g_IsController)
{
Engine.ClearAllPlayerReady();
setReady(2, true);
}
else if (g_IsReady != 2)
setReady(0, false);
}
/**
* Send a list of playernames and distinct between players and observers.
* Don't send teams, AIs or anything else until the game was started.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function formatClientsForStanza()
{
let connectedPlayers = 0;
let playerData = [];
for (let guid in g_PlayerAssignments)
{
let pData = { "Name": g_PlayerAssignments[guid].name };
if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
++connectedPlayers;
else
pData.Team = "observer";
playerData.push(pData);
}
return {
"list": playerDataToStringifiedTeamList(playerData),
"connectedPlayers": connectedPlayers
};
}
/**
* Send the relevant gamesettings to the lobbybot immediately.
*/
function sendRegisterGameStanzaImmediate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (g_GameStanzaTimer !== undefined)
{
clearTimeout(g_GameStanzaTimer);
g_GameStanzaTimer = undefined;
}
let clients = formatClientsForStanza();
let stanza = {
"name": g_ServerName,
"port": g_ServerPort,
"hostUsername": g_Username,
"mapName": g_GameAttributes.map,
"niceMapName": getMapDisplayName(g_GameAttributes.map),
"mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
"mapType": g_GameAttributes.mapType,
"victoryCondition": g_GameAttributes.settings.GameType,
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
"stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "",
"stunPort": g_StunEndpoint ? g_StunEndpoint.port : "",
};
// Only send the stanza if the relevant settings actually changed
if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop]))
return;
g_LastGameStanza = stanza;
Engine.SendRegisterGame(stanza);
}
/**
* Send the relevant gamesettings to the lobbybot in a deferred manner.
*/
function sendRegisterGameStanza()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
if (g_GameStanzaTimer !== undefined)
clearTimeout(g_GameStanzaTimer);
g_GameStanzaTimer = setTimeout(sendRegisterGameStanzaImmediate, g_GameStanzaTimeout * 1000);
}
/**
* Figures out all strings that can be autocompleted and sorts
* them by priority (so that playernames are always autocompleted first).
*/
function updateAutocompleteEntries()
{
let autocomplete = { "0": [] };
for (let control of [g_Dropdowns, g_Checkboxes])
for (let name in control)
autocomplete[0] = autocomplete[0].concat(control[name].title());
for (let dropdown of [g_Dropdowns, g_PlayerDropdowns])
for (let name in dropdown)
{
let priority = dropdown[name].autocomplete;
if (priority === undefined)
continue;
autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels());
}
g_Autocomplete = Object.keys(autocomplete).sort().reverse().reduce((all, priority) => all.concat(autocomplete[priority]), []);
}
Index: ps/trunk/binaries/data/mods/public/gui/options/options.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 20645)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 20646)
@@ -1,453 +1,465 @@
[
{
"label": "General",
"options":
[
{
"type": "string",
"label": "Playername (Single Player)",
"tooltip": "How you want to be addressed in Single Player matches.",
"config": "playername.singleplayer"
},
{
"type": "string",
"label": "Playername (Multiplayer)",
"tooltip": "How you want to be addressed in Multiplayer matches (except lobby).",
"config": "playername.multiplayer"
},
{
"type": "boolean",
"label": "Windowed Mode",
"tooltip": "Start 0 A.D. in a window",
"config": "windowed"
},
{
"type": "boolean",
"label": "Background Pause",
"tooltip": "Pause single player games when window loses focus",
"config": "pauseonfocusloss"
},
{
"type": "boolean",
"label": "Enable Welcome Screen",
"tooltip": "If you disable it, the welcome screen will still appear once, each time a new version is available. You can always launch it from the main menu.",
"config": "gui.splashscreen.enable"
},
{
"type": "boolean",
"label": "Detailed Tooltips",
"tooltip": "Show detailed tooltips for trainable units in unit-producing buildings.",
"config": "showdetailedtooltips"
},
{
"type": "boolean",
"label": "Network Warnings",
"tooltip": "Show which player has a bad connection in multiplayer games.",
"config": "overlay.netwarnings"
},
{
"type": "boolean",
"label": "FPS Overlay",
"tooltip": "Show frames per second in top right corner.",
"config": "overlay.fps"
},
{
"type": "boolean",
"label": "Realtime Overlay",
"tooltip": "Show current system time in top right corner.",
"config": "overlay.realtime"
},
{
"type": "boolean",
"label": "Gametime Overlay",
"tooltip": "Show current simulation time in top right corner.",
"config": "gui.session.timeelapsedcounter"
},
{
"type": "boolean",
"label": "Ceasefire Time Overlay",
"tooltip": "Always show the remaining ceasefire time.",
"config": "gui.session.ceasefirecounter"
},
{
"type": "dropdown",
"label": "Late Observer Joins",
"tooltip": "Allow everybody or buddies only to join the game as observer after it started",
"config": "network.lateobservers",
"list": [
{ "value": "everyone", "label": "Everyone" },
{ "value": "buddies", "label": "Buddies" },
{ "value": "disabled", "label": "Disabled" }
]
},
{
"type": "number",
"label": "Observer Limit",
"tooltip": "Prevent further observers from joining if the limit is reached",
"config": "network.observerlimit",
"min": 0,
"max": 32
},
{
"type": "number",
"label": "Batch Training Size",
"tooltip": "Number of units trained per batch",
"config": "gui.session.batchtrainingsize",
"min": 1,
"max": 20
},
{
"type": "slider",
"label": "Wounded Unit Health",
"tooltip": "The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number",
"config": "gui.session.woundedunithotkeythreshold",
"min": 0,
"max": 100
},
{
"type": "boolean",
"label": "Chat Timestamp",
"tooltip": "Show time that messages are posted in the lobby, gamesetup and ingame chat.",
"config": "chat.timestamp"
},
{
"type": "boolean",
"label": "Attack Range Visualization",
"tooltip": "Display the attack range of selected defensive structures (can also be toggled in-game with the hotkey).",
"config": "gui.session.attackrange"
},
{
"type": "boolean",
"label": "Aura Range Visualization",
"tooltip": "Display the range of auras of selected units and structures (can also be toggled in-game with the hotkey).",
"config": "gui.session.aurarange"
},
{
"type": "boolean",
"label": "Heal Range Visualization",
"tooltip": "Display the healing range of selected units (can also be toggled in-game with the hotkey).",
"config": "gui.session.healrange"
}
]
},
{
"label": "Graphics",
"tooltip": "Set the balance between performance and visual appearance.",
"options":
[
{
"type": "boolean",
"label": "Prefer GLSL",
"tooltip": "Use OpenGL 2.0 shaders (recommended)",
"config": "preferglsl",
"function": "Renderer_SetPreferGLSLEnabled"
},
{
"type": "boolean",
"label": "Fog",
"tooltip": "Enable Fog",
"dependencies": ["preferglsl"],
"config": "fog",
"function": "Renderer_SetFogEnabled"
},
{
"type": "boolean",
"label": "Post Processing",
"tooltip": "Use screen-space postprocessing filters (HDR, Bloom, DOF, etc)",
"config": "postproc",
"function": "Renderer_SetPostprocEnabled"
},
{
"type": "slider",
"label": "Shader Effects",
"tooltip": "Number of shader effects. REQUIRES GAME RESTART",
"config": "materialmgr.quality",
"min": 0,
"max": 10
},
{
"type": "boolean",
"label": "Shadows",
"tooltip": "Enable shadows",
"config": "shadows",
"function": "Renderer_SetShadowsEnabled"
},
{
"type": "dropdown",
"label": "Shadow Quality",
"tooltip": "Shadow map resolution. High values can crash the game when using a graphics card with low memory!",
"dependencies": ["shadows"],
"config": "shadowquality",
"function": "Renderer_RecreateShadowMap",
"list": [
{ "value": -2, "label": "Very Low" },
{ "value": -1, "label": "Low" },
{ "value": 0, "label": "Medium" },
{ "value": 1, "label": "High" },
{ "value": 2, "label": "Very High" }
]
},
{
"type": "boolean",
"label": "Shadow Filtering",
"tooltip": "Smooth shadows",
"dependencies": ["shadows"],
"config": "shadowpcf",
"function": "Renderer_SetShadowPCFEnabled"
},
{
"type": "boolean",
"label": "Unit Silhouettes",
"tooltip": "Show outlines of units behind buildings",
"config": "silhouettes",
"function": "Renderer_SetSilhouettesEnabled"
},
{
"type": "boolean",
"label": "Particles",
"tooltip": "Enable particles",
"config": "particles",
"function": "Renderer_SetParticlesEnabled"
},
{
"type": "boolean",
"label": "Water Effects",
"tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.",
"config": "watereffects",
"function": "Renderer_SetWaterEffectsEnabled"
},
{
"type": "boolean",
"label": "HQ Water Effects",
"tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.",
"dependencies": ["watereffects"],
"config": "waterfancyeffects",
"function": "Renderer_SetWaterFancyEffectsEnabled"
},
{
"type": "boolean",
"label": "Real Water Depth",
"tooltip": "Use actual water depth in rendering calculations",
"dependencies": ["watereffects"],
"config": "waterrealdepth",
"function": "Renderer_SetWaterRealDepthEnabled"
},
{
"type": "boolean",
"label": "Water Reflections",
"tooltip": "Allow water to reflect a mirror image",
"dependencies": ["watereffects"],
"config": "waterreflection",
"function": "Renderer_SetWaterReflectionEnabled"
},
{
"type": "boolean",
"label": "Water Refraction",
"tooltip": "Use a real water refraction map and not transparency",
"dependencies": ["watereffects"],
"config": "waterrefraction",
"function": "Renderer_SetWaterRefractionEnabled"
},
{
"type": "boolean",
"label": "Shadows on Water",
"tooltip": "Cast shadows on water",
"dependencies": ["watereffects"],
"config": "watershadows",
"function": "Renderer_SetWaterShadowsEnabled"
},
{
"type": "boolean",
"label": "Smooth LOS",
"tooltip": "Lift darkness and fog-of-war smoothly",
"config": "smoothlos",
"function": "Renderer_SetSmoothLOSEnabled"
},
{
"type": "boolean",
"label": "Show Sky",
"tooltip": "Render Sky",
"config": "showsky",
"function": "Renderer_SetShowSkyEnabled"
},
{
"type": "boolean",
"label": "VSync",
"tooltip": "Run vertical sync to fix screen tearing. REQUIRES GAME RESTART",
"config": "vsync"
},
{
"type": "slider",
"label": "FPS Throttling in Menus",
"tooltip": "To save CPU workload, throttle render frequency in all menus. Set to maximum to disable throttling.",
"config": "adaptivefps.menu",
"min": 20,
"max": 100
},
{
"type": "slider",
"label": "FPS Throttling in Games",
"tooltip": "To save CPU workload, throttle render frequency in running games. Set to maximum to disable throttling.",
"config": "adaptivefps.session",
"min": 20,
"max": 100
}
]
},
{
"label": "Sound",
"options":
[
{
"type": "slider",
"label": "Master Volume",
"tooltip": "Master audio gain",
"config": "sound.mastergain",
"function": "SetMasterGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Music Volume",
"tooltip": "In game music gain",
"config": "sound.musicgain",
"function": "SetMusicGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Ambient Volume",
"tooltip": "In game ambient sound gain",
"config": "sound.ambientgain",
"function": "SetAmbientGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Action Volume",
"tooltip": "In game unit action sound gain",
"config": "sound.actiongain",
"function": "SetActionGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "UI Volume",
"tooltip": "UI sound gain",
"config": "sound.uigain",
"function": "SetUIGain",
"min": 0,
"max": 2
},
{
"type": "boolean",
"label": "Nick Notification",
"tooltip": "Receive audio notification when someone types your nick",
"config": "sound.notify.nick"
}
]
},
{
"label": "Game Setup",
"options":
[
{
"type": "boolean",
"label": "Enable Game Setting Tips",
"tooltip": "Show tips when setting up a game.",
"config": "gui.gamesetup.enabletips"
},
{
"type": "boolean",
"label": "Persist Match Settings",
"tooltip": "Save and restore match settings for quick reuse when hosting another game",
"config": "persistmatchsettings"
},
{
"type": "dropdown",
- "label": "Default AI difficulty",
+ "label": "Default AI Difficulty",
"tooltip": "Default difficulty of the AI.",
"config": "gui.gamesetup.aidifficulty",
"list": [
{ "value": 0, "label": "Sandbox" },
{ "value": 1, "label": "Very Easy" },
{ "value": 2, "label": "Easy" },
{ "value": 3, "label": "Medium" },
{ "value": 4, "label": "Hard" },
{ "value": 5, "label": "Very Hard" }
]
},
{
"type": "dropdown",
+ "label": "Default AI Behavior",
+ "tooltip": "Default behavior of the AI.",
+ "config": "gui.gamesetup.aibehavior",
+ "list": [
+ { "value": "Random", "label": "Random" },
+ { "value": "Generalist", "label": "Generalist" },
+ { "value": "Aggressive", "label": "Aggressive" },
+ { "value": "Defensive", "label": "Defensive" }
+ ]
+ },
+ {
+ "type": "dropdown",
"label": "Assign Players",
"tooltip": "Automatically assign joining clients to free player slots during the match setup.",
"config": "gui.gamesetup.assignplayers",
"list": [
{ "value": "everyone", "label": "Everyone" },
{ "value": "buddies", "label": "Buddies" },
{ "value": "disabled", "label": "Disabled" }
]
}
]
},
{
"label": "Lobby",
"tooltip": "These settings only affect the multiplayer.",
"options":
[
{
"type": "number",
"label": "Chat Backlog",
"tooltip": "Number of backlogged messages to load when joining the lobby",
"config": "lobby.history",
"min": "0"
},
{
"type": "boolean",
"label": "Game Rating Column",
"tooltip": "Show the average rating of the participating players in a column of the gamelist.",
"config": "lobby.columns.gamerating"
}
]
},
{
"label": "Chat Notifications",
"tooltip": "Regulate the verbosity of chat notifications.",
"options":
[
{
"type": "boolean",
"label": "Attack",
"tooltip": "Show a chat notification if you are attacked by another player",
"config": "gui.session.notifications.attack"
},
{
"type": "boolean",
"label": "Tribute",
"tooltip": "Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode",
"config": "gui.session.notifications.tribute"
},
{
"type": "boolean",
"label": "Barter",
"tooltip": "Show a chat notification to observers when a player bartered resources",
"config": "gui.session.notifications.barter"
},
{
"type": "dropdown",
"label": "Phase",
"tooltip": "Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode",
"config": "gui.session.notifications.phase",
"list": [
{ "value": "none", "label": "Disable" },
{ "value": "completed", "label": "Completed" },
{ "value": "all", "label": "All displayed" }
]
}
]
}
]
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/_petrabot.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/_petrabot.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/_petrabot.js (revision 20646)
@@ -1,170 +1,170 @@
Engine.IncludeModule("common-api");
var PETRA = (function() {
var m = {};
m.PetraBot = function PetraBot(settings)
{
API3.BaseAI.call(this, settings);
this.playedTurn = 0;
this.elapsedTime = 0;
this.uniqueIDs = {
"armies": 1, // starts at 1 to allow easier tests on armies ID existence
"bases": 1, // base manager ID starts at one because "0" means "no base" on the map
"plans": 0, // training/building/research plans
"transports": 1 // transport plans start at 1 because 0 might be used as none
};
- this.Config = new m.Config(settings.difficulty);
+ this.Config = new m.Config(settings.difficulty, settings.behavior);
this.savedEvents = {};
};
m.PetraBot.prototype = new API3.BaseAI();
m.PetraBot.prototype.CustomInit = function(gameState)
{
if (this.isDeserialized)
{
// WARNING: the deserializations should not modify the metadatas infos inside their init functions
this.turn = this.data.turn;
this.playedTurn = this.data.playedTurn;
this.elapsedTime = this.data.elapsedTime;
this.savedEvents = this.data.savedEvents;
for (let key in this.savedEvents)
{
for (let i in this.savedEvents[key])
{
if (!this.savedEvents[key][i].entityObj)
continue;
let evt = this.savedEvents[key][i];
let evtmod = {};
for (let keyevt in evt)
{
evtmod[keyevt] = evt[keyevt];
evtmod.entityObj = new API3.Entity(gameState.sharedScript, evt.entityObj);
this.savedEvents[key][i] = evtmod;
}
}
}
this.Config.Deserialize(this.data.config);
this.queueManager = new m.QueueManager(this.Config, {});
this.queueManager.Deserialize(gameState, this.data.queueManager);
this.queues = this.queueManager.queues;
this.HQ = new m.HQ(this.Config);
this.HQ.init(gameState, this.queues);
this.HQ.Deserialize(gameState, this.data.HQ);
this.uniqueIDs = this.data.uniqueIDs;
this.isDeserialized = false;
this.data = undefined;
// initialisation needed after the completion of the deserialization
this.HQ.postinit(gameState);
}
else
{
this.Config.setConfig(gameState);
// this.queues can only be modified by the queue manager or things will go awry.
this.queues = {};
for (let i in this.Config.priorities)
this.queues[i] = new m.Queue();
this.queueManager = new m.QueueManager(this.Config, this.queues);
this.HQ = new m.HQ(this.Config);
this.HQ.init(gameState, this.queues);
// Analyze our starting position and set a strategy
this.HQ.gameAnalysis(gameState);
}
};
m.PetraBot.prototype.OnUpdate = function(sharedScript)
{
if (this.gameFinished)
return;
for (let i in this.events)
{
if (i == "AIMetadata") // not used inside petra
continue;
if(this.savedEvents[i] !== undefined)
this.savedEvents[i] = this.savedEvents[i].concat(this.events[i]);
else
this.savedEvents[i] = this.events[i];
}
// Run the update every n turns, offset depending on player ID to balance the load
this.elapsedTime = this.gameState.getTimeElapsed() / 1000;
if (!this.playedTurn || (this.turn + this.player) % 8 == 5)
{
Engine.ProfileStart("PetraBot bot (player " + this.player +")");
this.playedTurn++;
if (this.gameState.getOwnEntities().length === 0)
{
Engine.ProfileStop();
return; // With no entities to control the AI cannot do anything
}
this.HQ.update(this.gameState, this.queues, this.savedEvents);
this.queueManager.update(this.gameState);
for (let i in this.savedEvents)
this.savedEvents[i] = [];
Engine.ProfileStop();
}
this.turn++;
};
m.PetraBot.prototype.Serialize = function()
{
let savedEvents = {};
for (let key in this.savedEvents)
{
savedEvents[key] = this.savedEvents[key].slice();
for (let i in savedEvents[key])
{
if (!savedEvents[key][i].entityObj)
continue;
let evt = savedEvents[key][i];
let evtmod = {};
for (let keyevt in evt)
evtmod[keyevt] = evt[keyevt];
evtmod.entityObj = evt.entityObj._entity;
savedEvents[key][i] = evtmod;
}
}
return {
"uniqueIDs": this.uniqueIDs,
"turn": this.turn,
"playedTurn": this.playedTurn,
"elapsedTime": this.elapsedTime,
"savedEvents": savedEvents,
"config": this.Config.Serialize(),
"queueManager": this.queueManager.Serialize(),
"HQ": this.HQ.Serialize()
};
};
m.PetraBot.prototype.Deserialize = function(data, sharedScript)
{
this.isDeserialized = true;
this.data = data;
};
return m;
}());
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 20646)
@@ -1,778 +1,779 @@
var PETRA = function(m)
{
/** Attack Manager */
m.AttackManager = function(Config)
{
this.Config = Config;
this.totalNumber = 0;
this.attackNumber = 0;
this.rushNumber = 0;
this.raidNumber = 0;
this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] };
this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] };
this.bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start
this.debugTime = 0;
this.maxRushes = 0;
this.rushSize = [];
this.currentEnemyPlayer = undefined; // enemy player we are currently targeting
this.defeated = {};
};
/** More initialisation for stuff that needs the gameState */
m.AttackManager.prototype.init = function(gameState)
{
this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1));
this.outOfPlan.registerUpdates();
};
m.AttackManager.prototype.setRushes = function(allowed)
{
if (this.Config.personality.aggressive > 0.8 && allowed > 2)
{
this.maxRushes = 3;
this.rushSize = [ 16, 20, 24 ];
}
else if (this.Config.personality.aggressive > 0.6 && allowed > 1)
{
this.maxRushes = 2;
this.rushSize = [ 18, 22 ];
}
else if (this.Config.personality.aggressive > 0.3 && allowed > 0)
{
this.maxRushes = 1;
this.rushSize = [ 20 ];
}
};
m.AttackManager.prototype.checkEvents = function(gameState, events)
{
for (let evt of events.PlayerDefeated)
this.defeated[evt.playerId] = true;
let answer = "decline";
let other;
let targetPlayer;
for (let evt of events.AttackRequest)
{
if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player))
continue;
targetPlayer = evt.player;
let available = 0;
for (let attackType in this.upcomingAttacks)
{
for (let attack of this.upcomingAttacks[attackType])
{
if (attack.state === "completing")
{
if (attack.targetPlayer === targetPlayer)
available += attack.unitCollection.length;
else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer)
other = attack.targetPlayer;
continue;
}
attack.targetPlayer = targetPlayer;
if (attack.unitCollection.length > 2)
available += attack.unitCollection.length;
}
}
if (available > 12) // launch the attack immediately
{
for (let attackType in this.upcomingAttacks)
{
for (let attack of this.upcomingAttacks[attackType])
{
if (attack.state === "completing" ||
attack.targetPlayer !== targetPlayer ||
attack.unitCollection.length < 3)
continue;
attack.forceStart();
attack.requested = true;
}
}
answer = "join";
}
else if (other !== undefined)
answer = "other";
break; // take only the first attack request into account
}
if (targetPlayer !== undefined)
m.chatAnswerRequestAttack(gameState, targetPlayer, answer, other);
for (let evt of events.EntityRenamed) // take care of packing units in bombing attacks
{
for (let [targetId, unitIds] of this.bombingAttacks)
{
if (targetId == evt.entity)
{
this.bombingAttacks.set(evt.newentity, unitIds);
this.bombingAttacks.delete(evt.entity);
}
else if (unitIds.has(evt.entity))
{
unitIds.add(evt.newentity);
unitIds.delete(evt.entity);
}
}
}
};
/**
* Check for any structure in range from within our territory, and bomb it
*/
m.AttackManager.prototype.assignBombers = function(gameState)
{
// First some cleaning of current bombing attacks
for (let [targetId, unitIds] of this.bombingAttacks)
{
let target = gameState.getEntityById(targetId);
if (!target || !gameState.isPlayerEnemy(target.owner()))
this.bombingAttacks.delete(targetId);
else
{
for (let entId of unitIds.values())
{
let ent = gameState.getEntityById(entId);
if (ent && ent.owner() == PlayerID)
{
let plan = ent.getMetadata(PlayerID, "plan");
let orders = ent.unitAIOrderData();
let lastOrder = orders && orders.length ? orders[orders.length-1] : null;
if (lastOrder && lastOrder.target && lastOrder.target == targetId && plan != -2 && plan != -3)
continue;
}
unitIds.delete(entId);
}
if (!unitIds.size)
this.bombingAttacks.delete(targetId);
}
}
let bombers = gameState.updatingCollection("bombers", API3.Filters.byClassesOr(["BoltShooter", "Catapult"]), gameState.getOwnUnits());
for (let ent of bombers.values())
{
if (!ent.position() || !ent.isIdle() || !ent.attackRange("Ranged"))
continue;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
continue;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1)
{
let subrole = ent.getMetadata(PlayerID, "subrole");
if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking"))
continue;
}
let alreadyBombing = false;
for (let unitIds of this.bombingAttacks.values())
{
if (!unitIds.has(ent.id()))
continue;
alreadyBombing = true;
break;
}
if (alreadyBombing)
break;
let range = ent.attackRange("Ranged").max;
let entPos = ent.position();
let access = gameState.ai.accessibility.getAccessValue(entPos);
for (let struct of gameState.getEnemyStructures().values())
{
let structPos = struct.position();
let x;
let z;
if (struct.hasClass("Field"))
{
if (!struct.resourceSupplyNumGatherers() ||
!gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos)))
continue;
}
let dist = API3.VectorDistance(entPos, structPos);
if (dist > range)
{
let safety = struct.footprintRadius() + 30;
x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist;
z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist;
let owner = gameState.ai.HQ.territoryMap.getOwner([x, z]);
if (owner != 0 && gameState.isPlayerEnemy(owner))
continue;
x = structPos[0] + (entPos[0] - structPos[0]) * range / dist;
z = structPos[1] + (entPos[1] - structPos[1]) * range / dist;
if (gameState.ai.HQ.territoryMap.getOwner([x, z]) != PlayerID ||
gameState.ai.accessibility.getAccessValue([x, z]) != access)
continue;
}
let attackingUnits;
for (let [targetId, unitIds] of this.bombingAttacks)
{
if (targetId != struct.id())
continue;
attackingUnits = unitIds;
break;
}
if (attackingUnits && attackingUnits.size > 4)
continue; // already enough units against that target
if (!attackingUnits)
{
attackingUnits = new Set();
this.bombingAttacks.set(struct.id(), attackingUnits);
}
attackingUnits.add(ent.id());
if (dist > range)
ent.move(x, z);
ent.attack(struct.id(), false, dist > range);
break;
}
}
};
/**
* Some functions are run every turn
* Others once in a while
*/
m.AttackManager.prototype.update = function(gameState, queues, events)
{
if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60)
{
this.debugTime = gameState.ai.elapsedTime;
API3.warn(" upcoming attacks =================");
for (let attackType in this.upcomingAttacks)
for (let attack of this.upcomingAttacks[attackType])
API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length);
API3.warn(" started attacks ==================");
for (let attackType in this.startedAttacks)
for (let attack of this.startedAttacks[attackType])
API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length);
API3.warn(" ==================================");
}
this.checkEvents(gameState, events);
let unexecutedAttacks = {"Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0};
for (let attackType in this.upcomingAttacks)
{
for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i)
{
let attack = this.upcomingAttacks[attackType][i];
attack.checkEvents(gameState, events);
if (attack.isStarted())
API3.warn("Petra problem in attackManager: attack in preparation has already started ???");
let updateStep = attack.updatePreparation(gameState);
// now we're gonna check if the preparation time is over
if (updateStep === 1 || attack.isPaused() )
{
// just chillin'
if (attack.state === "unexecuted")
++unexecutedAttacks[attackType];
}
else if (updateStep === 0)
{
if (this.Config.debug > 1)
API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted.");
attack.Abort(gameState);
this.upcomingAttacks[attackType].splice(i--,1);
}
else if (updateStep === 2)
{
if (attack.StartAttack(gameState))
{
if (this.Config.debug > 1)
API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName());
if (this.Config.chat)
m.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType());
this.startedAttacks[attackType].push(attack);
}
else
attack.Abort(gameState);
this.upcomingAttacks[attackType].splice(i--,1);
}
}
}
for (let attackType in this.startedAttacks)
{
for (let i = 0; i < this.startedAttacks[attackType].length; ++i)
{
let attack = this.startedAttacks[attackType][i];
attack.checkEvents(gameState, events);
// okay so then we'll update the attack.
if (attack.isPaused())
continue;
let remaining = attack.update(gameState, events);
if (!remaining)
{
if (this.Config.debug > 1)
API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining);
attack.Abort(gameState);
this.startedAttacks[attackType].splice(i--,1);
}
}
}
// creating plans after updating because an aborted plan might be reused in that case.
let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length;
- if (this.rushNumber < this.maxRushes && barracksNb >= 1)
+ if (this.rushNumber < this.maxRushes && barracksNb >= 1 && this.Config.behavior != "defensive")
{
if (unexecutedAttacks.Rush === 0)
{
// we have a barracks and we want to rush, rush.
let data = { "targetSize": this.rushSize[this.rushNumber] };
let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data);
if (!attackPlan.failed)
{
if (this.Config.debug > 1)
API3.warn("Military Manager: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes);
this.totalNumber++;
attackPlan.init(gameState);
this.upcomingAttacks.Rush.push(attackPlan);
}
this.rushNumber++;
}
}
else if (unexecutedAttacks.Attack == 0 && unexecutedAttacks.HugeAttack == 0 &&
this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100)) &&
- (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12))
+ (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12) &&
+ (this.Config.behavior != "defensive" || gameState.ai.HQ.defenseManager.targetList.length))
{
if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) ||
!gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack
{
let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack";
let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, type);
if (attackPlan.failed)
this.attackPlansEncounteredWater = true; // hack
else
{
if (this.Config.debug > 1)
API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber);
this.totalNumber++;
attackPlan.init(gameState);
this.upcomingAttacks[type].push(attackPlan);
}
this.attackNumber++;
}
}
if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length)
{
let target;
for (let targetId of gameState.ai.HQ.defenseManager.targetList)
{
target = gameState.getEntityById(targetId);
if (!target)
continue;
if (gameState.isPlayerEnemy(target.owner()))
break;
target = undefined;
}
if (target) // prepare a raid against this target
this.raidTargetEntity(gameState, target);
}
// Check if we have some unused ranged siege unit which could do something useful while waiting
if (this.Config.difficulty > 1 && gameState.ai.playedTurn % 5 == 0)
this.assignBombers(gameState);
};
m.AttackManager.prototype.getPlan = function(planName)
{
for (let attackType in this.upcomingAttacks)
{
for (let attack of this.upcomingAttacks[attackType])
if (attack.getName() == planName)
return attack;
}
for (let attackType in this.startedAttacks)
{
for (let attack of this.startedAttacks[attackType])
if (attack.getName() == planName)
return attack;
}
return undefined;
};
m.AttackManager.prototype.pausePlan = function(planName)
{
let attack = this.getPlan(planName);
if (attack)
attack.setPaused(true);
};
m.AttackManager.prototype.unpausePlan = function(planName)
{
let attack = this.getPlan(planName);
if (attack)
attack.setPaused(false);
};
m.AttackManager.prototype.pauseAllPlans = function()
{
for (let attackType in this.upcomingAttacks)
for (let attack of this.upcomingAttacks[attackType])
attack.setPaused(true);
for (let attackType in this.startedAttacks)
for (let attack of this.startedAttacks[attackType])
attack.setPaused(true);
};
m.AttackManager.prototype.unpauseAllPlans = function()
{
for (let attackType in this.upcomingAttacks)
for (let attack of this.upcomingAttacks[attackType])
attack.setPaused(false);
for (let attackType in this.startedAttacks)
for (let attack of this.startedAttacks[attackType])
attack.setPaused(false);
};
m.AttackManager.prototype.getAttackInPreparation = function(type)
{
if (!this.upcomingAttacks[type].length)
return undefined;
return this.upcomingAttacks[type][0];
};
/**
* determine which player should be attacked: when called when starting the attack,
* attack.targetPlayer is undefined and in that case, we keep track of the chosen target
* for future attacks.
*/
m.AttackManager.prototype.getEnemyPlayer = function(gameState, attack)
{
let enemyPlayer;
// first check if there is a preferred enemy based on our victory conditions
if (gameState.getGameType() === "wonder")
{
let moreAdvanced;
let enemyWonder;
let wonders = gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder"));
for (let wonder of wonders.values())
{
if (wonder.owner() === 0)
continue;
let progress = wonder.foundationProgress();
if (progress === undefined)
{
enemyWonder = wonder;
break;
}
if (enemyWonder && moreAdvanced > progress)
continue;
enemyWonder = wonder;
moreAdvanced = progress;
}
if (enemyWonder)
{
enemyPlayer = enemyWonder.owner();
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
}
}
else if (gameState.getGameType() === "capture_the_relic")
{
// Target the player with the most relics (including gaia)
let allRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic"));
let maxRelicsOwned = 0;
for (let i = 0; i < gameState.sharedScript.playersData.length; ++i)
{
if (!gameState.isPlayerEnemy(i) || this.defeated[i] ||
i === 0 && !gameState.ai.HQ.gameTypeManager.tryCaptureGaiaRelic)
continue;
let relicsCount = allRelics.filter(relic => relic.owner() === i).length;
if (relicsCount <= maxRelicsOwned)
continue;
maxRelicsOwned = relicsCount;
enemyPlayer = i;
}
if (enemyPlayer !== undefined)
{
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
if (enemyPlayer === 0)
gameState.ai.HQ.gameTypeManager.resetCaptureGaiaRelic(gameState);
return enemyPlayer;
}
}
let veto = {};
for (let i in this.defeated)
veto[i] = true;
// No rush if enemy too well defended (i.e. iberians)
if (attack.type === "Rush")
{
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
{
if (!gameState.isPlayerEnemy(i) || veto[i])
continue;
if (this.defeated[i])
continue;
let enemyDefense = 0;
for (let ent of gameState.getEnemyStructures(i).values())
if (ent.hasClass("Tower") || ent.hasClass("Fortress"))
enemyDefense++;
if (enemyDefense > 6)
veto[i] = true;
}
}
// then if not a huge attack, continue attacking our previous target as long as it has some entities,
// otherwise target the most accessible one
if (attack.type !== "HugeAttack")
{
if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined &&
!this.defeated[this.currentEnemyPlayer] &&
gameState.isPlayerEnemy(this.currentEnemyPlayer) &&
gameState.getEntities(this.currentEnemyPlayer).hasEntities())
return this.currentEnemyPlayer;
let distmin;
let ccmin;
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
for (let ourcc of ccEnts.values())
{
if (ourcc.owner() !== PlayerID)
continue;
let ourPos = ourcc.position();
let access = gameState.ai.accessibility.getAccessValue(ourPos);
for (let enemycc of ccEnts.values())
{
if (veto[enemycc.owner()])
continue;
if (!gameState.isPlayerEnemy(enemycc.owner()))
continue;
let enemyPos = enemycc.position();
if (access !== gameState.ai.accessibility.getAccessValue(enemyPos))
continue;
let dist = API3.SquareVectorDistance(ourPos, enemyPos);
if (distmin && dist > distmin)
continue;
ccmin = enemycc;
distmin = dist;
}
}
if (ccmin)
{
enemyPlayer = ccmin.owner();
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
}
}
// then let's target our strongest enemy (basically counting enemies units)
// with priority to enemies with civ center
let max = 0;
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
{
if (veto[i])
continue;
if (!gameState.isPlayerEnemy(i))
continue;
let enemyCount = 0;
let enemyCivCentre = false;
for (let ent of gameState.getEntities(i).values())
{
enemyCount++;
if (ent.hasClass("CivCentre"))
enemyCivCentre = true;
}
if (enemyCivCentre)
enemyCount += 500;
if (!enemyCount || enemyCount < max)
continue;
max = enemyCount;
enemyPlayer = i;
}
if (attack.targetPlayer === undefined)
this.currentEnemyPlayer = enemyPlayer;
return enemyPlayer;
};
/** f.e. if we have changed diplomacy with another player. */
m.AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player)
{
for (let attackType in this.upcomingAttacks)
for (let attack of this.upcomingAttacks[attackType])
if (attack.targetPlayer === player)
attack.targetPlayer = undefined;
for (let attackType in this.startedAttacks)
for (let i = 0; i < this.startedAttacks[attackType].length; ++i)
{
let attack = this.startedAttacks[attackType][i];
if (attack.targetPlayer === player)
{
attack.Abort(gameState);
this.startedAttacks[attackType].splice(i--, 1);
}
}
};
m.AttackManager.prototype.raidTargetEntity = function(gameState, ent)
{
let data = { "target": ent };
let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data);
if (!attackPlan.failed)
{
if (this.Config.debug > 1)
API3.warn("Military Manager: Raiding plan " + this.totalNumber);
this.totalNumber++;
attackPlan.init(gameState);
this.upcomingAttacks.Raid.push(attackPlan);
}
this.raidNumber++;
};
/**
* Return the number of units from any of our attacking armies around this position
*/
m.AttackManager.prototype.numAttackingUnitsAround = function(pos, dist)
{
let num = 0;
for (let attackType in this.startedAttacks)
for (let attack of this.startedAttacks[attackType])
if (API3.SquareVectorDistance(pos, attack.position) < dist*dist)
num += attack.unitCollection.length;
return num;
};
/**
* Switch defense armies into an attack one against the given target
* data.range: transform all defense armies inside range of the target into a new attack
* data.armyID: transform only the defense army ID into a new attack
* data.uniqueTarget: the attack will stop when the target is destroyed or captured
*/
m.AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data)
{
if (!target || !target.position())
return false;
if (!data.range && !data.armyID)
{
API3.warn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data));
return false;
}
let attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined;
let pos = target.position();
let attackType = "Attack";
let attackPlan = new m.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData);
if (attackPlan.failed)
return false;
this.totalNumber++;
attackPlan.init(gameState);
this.startedAttacks[attackType].push(attackPlan);
for (let army of gameState.ai.HQ.defenseManager.armies)
{
if (data.range)
{
army.recalculatePosition(gameState);
if (API3.SquareVectorDistance(pos, army.foePosition) > data.range * data.range)
continue;
}
else if (army.ID != +data.armyID)
continue;
while (army.foeEntities.length > 0)
army.removeFoe(gameState, army.foeEntities[0]);
while (army.ownEntities.length > 0)
{
let unitId = army.ownEntities[0];
army.removeOwn(gameState, unitId);
let unit = gameState.getEntityById(unitId);
if (unit && attackPlan.isAvailableUnit(gameState, unit))
{
unit.setMetadata(PlayerID, "plan", attackPlan.name);
attackPlan.unitCollection.updateEnt(unit);
}
}
}
if (!attackPlan.unitCollection.hasEntities())
{
attackPlan.Abort(gameState);
return false;
}
attackPlan.targetPlayer = target.owner();
attackPlan.targetPos = pos;
attackPlan.target = target;
attackPlan.state = "arrived";
return true;
};
m.AttackManager.prototype.Serialize = function()
{
let properties = {
"totalNumber": this.totalNumber,
"attackNumber": this.attackNumber,
"rushNumber": this.rushNumber,
"raidNumber": this.raidNumber,
"debugTime": this.debugTime,
"maxRushes": this.maxRushes,
"rushSize": this.rushSize,
"currentEnemyPlayer": this.currentEnemyPlayer,
"defeated": this.defeated
};
let upcomingAttacks = {};
for (let key in this.upcomingAttacks)
{
upcomingAttacks[key] = [];
for (let attack of this.upcomingAttacks[key])
upcomingAttacks[key].push(attack.Serialize());
}
let startedAttacks = {};
for (let key in this.startedAttacks)
{
startedAttacks[key] = [];
for (let attack of this.startedAttacks[key])
startedAttacks[key].push(attack.Serialize());
}
return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks };
};
m.AttackManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
this.upcomingAttacks = {};
for (let key in data.upcomingAttacks)
{
this.upcomingAttacks[key] = [];
for (let dataAttack of data.upcomingAttacks[key])
{
let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name);
attack.Deserialize(gameState, dataAttack);
attack.init(gameState);
this.upcomingAttacks[key].push(attack);
}
}
this.startedAttacks = {};
for (let key in data.startedAttacks)
{
this.startedAttacks[key] = [];
for (let dataAttack of data.startedAttacks[key])
{
let attack = new m.AttackPlan(gameState, this.Config, dataAttack.properties.name);
attack.Deserialize(gameState, dataAttack);
attack.init(gameState);
this.startedAttacks[key].push(attack);
}
}
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 20646)
@@ -1,232 +1,235 @@
var PETRA = function(m)
{
-m.Config = function(difficulty)
+m.Config = function(difficulty, behavior)
{
// 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard.
this.difficulty = difficulty !== undefined ? difficulty : 3;
+ // for instance "generalist", "aggressive" or "defensive"
+ this.behavior = behavior || "generalist";
+
// debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug
this.debug = 0;
this.chat = true; // false to prevent AI's chats
this.popScaling = 1; // scale factor depending on the max population
this.Military = {
"towerLapseTime" : 90, // Time to wait between building 2 towers
"fortressLapseTime" : 390, // Time to wait between building 2 fortresses
"popForBarracks1" : 25,
"popForBarracks2" : 95,
"popForBlacksmith" : 65,
"numSentryTowers" : 1
};
this.Economy = {
"popPhase2" : 38, // How many units we want before aging to phase2.
"workPhase3" : 65, // How many workers we want before aging to phase3.
"workPhase4" : 80, // How many workers we want before aging to phase4 or higher.
"popForMarket" : 50,
"popForDock" : 25,
"targetNumWorkers" : 40,// dummy, will be changed later
"targetNumTraders" : 5, // Target number of traders
"targetNumFishers" : 1, // Target number of fishers per sea
"supportRatio" : 0.35, // fraction of support workers among the workforce
"provisionFields" : 2
};
// Note: attack settings are set directly in attack_plan.js
// defense
this.Defense =
{
"defenseRatio" : { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers.
"armyCompactSize" : 2000, // squared. Half-diameter of an army.
"armyBreakawaySize" : 3500, // squared.
"armyMergeSize" : 1400 // squared.
};
this.buildings =
{
"advanced": {
"default": [],
"athen": [ "structures/{civ}_gymnasion", "structures/{civ}_prytaneion",
"structures/{civ}_theatron", "structures/{civ}_royal_stoa" ],
"brit": [ "structures/{civ}_rotarymill" ],
"cart": [ "structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian",
"structures/{civ}_embassy_italiote" ],
"gaul": [ "structures/{civ}_rotarymill", "structures/{civ}_tavern" ],
"iber": [ "structures/{civ}_monument" ],
"mace": [ "structures/{civ}_library", "structures/{civ}_theatron" ],
"maur": [ "structures/{civ}_pillar_ashoka" ],
"pers": [ "structures/{civ}_apadana", "structures/{civ}_hall"],
"ptol": [ "structures/{civ}_library" ],
"rome": [ "structures/{civ}_army_camp" ],
"sele": [ "structures/{civ}_library" ],
"spart": [ "structures/{civ}_syssiton", "structures/{civ}_theatron",
"structures/{civ}_royal_stoa" ]
}
};
this.priorities =
{
"villager": 30, // should be slightly lower than the citizen soldier one to not get all the food
"citizenSoldier": 60,
"trader": 50,
"healer": 20,
"ships": 70,
"house": 350,
"dropsites": 200,
"field": 400,
"dock": 90,
"corral": 60,
"economicBuilding": 90,
"militaryBuilding": 130,
"defenseBuilding": 70,
"civilCentre": 950,
"majorTech": 700,
"minorTech": 40,
"wonder": 1000,
"emergency": 1000 // used only in emergency situations, should be the highest one
};
this.personality =
{
"aggressive": 0.5,
"cooperative": 0.5,
"defensive": 0.5
};
// See m.QueueManager.prototype.wantedGatherRates()
this.queues =
{
"firstTurn": {
"food": 10,
"wood": 10,
"default": 0
},
"short": {
"food": 200,
"wood": 200,
"default": 100
},
"medium": {
"default": 0
},
"long": {
"default": 0
}
};
this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 };
};
m.Config.prototype.setConfig = function(gameState)
{
// initialize personality traits
if (this.difficulty > 1)
{
- this.personality.aggressive = randFloat(0, 1);
+ this.personality.aggressive = this.behavior === "aggressive" ? randFloat(0.7, 1) : randFloat(0, 0.6);
this.personality.cooperative = randFloat(0, 1);
- this.personality.defensive = randFloat(0, 1);
+ this.personality.defensive = this.behavior === "defensive" ? randFloat(0.7, 1) : randFloat(0, 0.6);
}
else
{
this.personality.aggressive = 0.1;
this.personality.cooperative = 0.9;
}
if (gameState.playerData.teamsLocked)
this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30);
else if (gameState.getAlliedVictory())
this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15);
// changing settings based on difficulty or personality
if (this.difficulty < 2)
{
this.Economy.supportRatio = 0.5;
this.Economy.provisionFields = 1;
this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0;
}
else if (this.difficulty < 3)
{
this.Economy.supportRatio = 0.4;
this.Economy.provisionFields = 1;
this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0;
}
else
{
this.Military.towerLapseTime += Math.round(20*(this.personality.defensive - 0.5));
this.Military.fortressLapseTime += Math.round(60*(this.personality.defensive - 0.5));
if (this.difficulty == 3)
this.Military.numSentryTowers = 1;
else
this.Military.numSentryTowers = 2;
if (this.personality.defensive > 0.66)
++this.Military.numSentryTowers;
else if (this.personality.defensive < 0.33)
--this.Military.numSentryTowers;
if (this.personality.aggressive > 0.7)
{
this.Military.popForBarracks1 = 12;
this.Economy.popPhase2 = 50;
this.Economy.popForMarket = 60;
this.priorities.defenseBuilding = 60;
this.priorities.healer = 10;
}
}
let maxPop = gameState.getPopulationMax();
if (this.difficulty < 2)
this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop));
else if (this.difficulty < 3)
this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2)));
else
this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3)));
this.Economy.targetNumTraders = 2 + this.difficulty;
if (gameState.getGameType() === "wonder")
{
this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3);
this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4);
}
if (maxPop < 300)
{
this.popScaling = Math.sqrt(maxPop / 300);
this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5));
this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3));
this.Military.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * this.popScaling), 30), Math.floor(maxPop/2));
this.Economy.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2));
this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3));
this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), Math.floor(maxPop*2/3));
this.Economy.popForMarket = Math.min(Math.max(Math.floor(this.Economy.popForMarket * this.popScaling), 25), Math.floor(maxPop/2));
this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling);
}
this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popPhase2);
this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers);
this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers);
if (this.difficulty < 2)
this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase
if (this.debug < 2)
return;
API3.warn(" >>> Petra bot: personality = " + uneval(this.personality));
};
m.Config.prototype.Serialize = function()
{
var data = {};
for (let key in this)
if (this.hasOwnProperty(key) && key != "debug")
data[key] = this[key];
return data;
};
m.Config.prototype.Deserialize = function(data)
{
for (let key in data)
this[key] = data[key];
};
return m;
}(PETRA);
Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/player_defaults.json
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/data/settings/player_defaults.json (revision 20645)
+++ ps/trunk/binaries/data/mods/public/simulation/data/settings/player_defaults.json (revision 20646)
@@ -1,68 +1,77 @@
{
"PlayerData":
[
{
"Name": "Gaia",
"Civ": "gaia",
"Color": { "r": 255, "g": 255, "b": 255 },
"AI": "",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 1",
"Civ": "athen",
"Color": { "r": 21, "g": 55, "b": 149 },
"AI": "",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 2",
"Civ": "cart",
"Color": { "r": 150, "g": 20, "b": 20 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 3",
"Civ": "gaul",
"Color": { "r": 86 , "g": 180, "b": 31 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 4",
"Civ": "iber",
"Color": { "r": 231, "g": 200, "b": 5 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 5",
"Civ": "mace",
"Color": { "r": 50, "g": 170, "b": 170 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 6",
"Civ": "maur",
"Color": { "r": 160, "g": 80, "b": 200 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 7",
"Civ": "pers",
"Color": { "r": 220, "g": 115, "b": 16 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
},
{
"Name": "Player 8",
"Civ": "rome",
"Color": { "r": 64, "g": 64, "b": 64 },
"AI": "petra",
- "AIDiff": 3
+ "AIDiff": 3,
+ "AIBehavior": "generalist"
}
]
}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 20645)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 20646)
@@ -1,82 +1,82 @@
/**
* Called when the map has been loaded, but before the simulation has started.
* Only called when a new game is started, not when loading a saved game.
*/
function PreInitGame()
{
// We need to replace skirmish "default" entities with real ones.
// This needs to happen before AI initialization (in InitGame).
// And we need to flush destroyed entities otherwise the AI gets the wrong game state in
// the beginning and a bunch of "destroy" messages on turn 0, which just shouldn't happen.
Engine.BroadcastMessage(MT_SkirmishReplace, {});
Engine.FlushDestroyedEntities();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i) // ignore gaia
{
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.UpdateAutoResearch();
}
// Explore the map inside the players' territory borders
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.ExploreTerritories();
}
function InitGame(settings)
{
// No settings when loading a map in Atlas, so do nothing
if (!settings)
{
// Map dependent initialisations of components (i.e. garrisoned units)
Engine.BroadcastMessage(MT_InitGame, {});
return;
}
if (settings.ExploreMap)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let i = 1; i < settings.PlayerData.length; ++i)
cmpRangeManager.ExploreAllTiles(i);
}
// Sandbox, Very Easy, Easy, Medium, Hard, Very Hard
let rate = [ 0.50, 0.64, 0.80, 1.00, 1.25, 1.56 ];
let cmpAIManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIManager);
for (let i = 0; i < settings.PlayerData.length; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
cmpPlayer.SetCheatsEnabled(!!settings.CheatsEnabled);
if (settings.PlayerData[i] && settings.PlayerData[i].AI && settings.PlayerData[i].AI != "")
{
let AIDiff = +settings.PlayerData[i].AIDiff;
- cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, AIDiff);
+ cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, AIDiff, settings.PlayerData[i].AIBehavior);
cmpPlayer.SetAI(true);
AIDiff = Math.min(AIDiff, rate.length - 1);
cmpPlayer.SetGatherRateMultiplier(rate[AIDiff]);
cmpPlayer.SetTradeRateMultiplier(rate[AIDiff]);
}
if (settings.PopulationCap)
cmpPlayer.SetMaxPopulation(settings.PopulationCap);
if (settings.mapType !== "scenario" && settings.StartingResources)
{
let resourceCounts = cmpPlayer.GetResourceCounts();
let newResourceCounts = {};
for (let resouces in resourceCounts)
newResourceCounts[resouces] = settings.StartingResources;
cmpPlayer.SetResourceCounts(newResourceCounts);
}
}
// Map or player data (handicap...) dependent initialisations of components (i.e. garrisoned units)
Engine.BroadcastMessage(MT_InitGame, {});
let seed = settings.AISeed ? settings.AISeed : 0;
cmpAIManager.SetRNGSeed(seed);
cmpAIManager.TryLoadSharedComponent();
cmpAIManager.RunGamestateInit();
}
Engine.RegisterGlobal("PreInitGame", PreInitGame);
Engine.RegisterGlobal("InitGame", InitGame);
Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 20645)
+++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 20646)
@@ -1,1583 +1,1584 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "lib/app_hooks.h"
#include "lib/config2.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/file/common/file_stats.h"
#include "lib/res/h_mgr.h"
#include "lib/res/graphics/cursor.h"
#include "lib/sysdep/cursor.h"
#include "graphics/CinemaManager.h"
#include "graphics/FontMetrics.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/MapReader.h"
#include "graphics/MaterialManager.h"
#include "graphics/TerrainTextureManager.h"
#include "gui/GUI.h"
#include "gui/GUIManager.h"
#include "gui/scripting/ScriptFunctions.h"
#include "i18n/L10n.h"
#include "maths/MathUtil.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/HWDetect.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Joystick.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h" // psSetLogDir
#include "ps/scripting/JSInterface_Console.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/VisualReplay.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/VertexBufferManager.h"
#include "renderer/ModelRenderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/ScriptConversions.h"
#include "simulation2/Simulation2.h"
#include "lobby/IXmppClient.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "tools/atlas/GameInterface/View.h"
#if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets
#define MUST_INIT_X11 1
#include
#else
#define MUST_INIT_X11 0
#endif
extern void restart_engine();
#include
#include
#include
ERROR_GROUP(System);
ERROR_TYPE(System, SDLInitFailed);
ERROR_TYPE(System, VmodeFailed);
ERROR_TYPE(System, RequiredExtensionsMissing);
bool g_DoRenderGui = true;
bool g_DoRenderLogger = true;
bool g_DoRenderCursor = true;
shared_ptr g_ScriptRuntime;
static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code
bool g_InDevelopmentCopy;
bool g_CheckedIfInDevelopmentCopy = false;
static void SetTextureQuality(int quality)
{
int q_flags;
GLint filter;
retry:
// keep this in sync with SANE_TEX_QUALITY_DEFAULT
switch(quality)
{
// worst quality
case 0:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_NEAREST;
break;
// [perf] add bilinear filtering
case 1:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] no longer reduce resolution
case 2:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] add mipmaps
case 3:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_NEAREST_MIPMAP_LINEAR;
break;
// [perf] better filtering
case 4:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [vmem] no longer reduce bpp
case SANE_TEX_QUALITY_DEFAULT:
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [perf] add anisotropy
case 6:
// TODO: add anisotropic filtering
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// invalid
default:
debug_warn(L"SetTextureQuality: invalid quality");
quality = SANE_TEX_QUALITY_DEFAULT;
// careful: recursion doesn't work and we don't want to duplicate
// the "sane" default values.
goto retry;
}
ogl_tex_set_defaults(q_flags, filter);
}
//----------------------------------------------------------------------------
// GUI integration
//----------------------------------------------------------------------------
// display progress / description in loading screen
void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task)
{
g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, true);
g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, true);
g_GUI->GetActiveGUI()->SendEventToAll("progress");
}
void Render()
{
PROFILE3("render");
if (g_SoundManager)
g_SoundManager->IdleTask();
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameStart();
ogl_WarnIfError();
// prepare before starting the renderer frame
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->BeginFrame();
if (g_Game)
g_Renderer.SetSimulation(g_Game->GetSimulation2());
// start new frame
g_Renderer.BeginFrame();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->Render();
ogl_WarnIfError();
g_Renderer.RenderTextOverlays();
// If we're in Atlas game view, render special tools
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawCinemaPathTool();
ogl_WarnIfError();
}
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->GetCinema()->Render();
ogl_WarnIfError();
if (g_DoRenderGui)
g_GUI->Draw();
ogl_WarnIfError();
// If we're in Atlas game view, render special overlays (e.g. editor bandbox)
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawOverlays();
ogl_WarnIfError();
}
// Text:
glDisable(GL_DEPTH_TEST);
g_Console->Render();
ogl_WarnIfError();
if (g_DoRenderLogger)
g_Logger->Render();
ogl_WarnIfError();
// Profile information
g_ProfileViewer.RenderProfile();
ogl_WarnIfError();
// Draw the cursor (or set the Windows cursor, on Windows)
if (g_DoRenderCursor)
{
PROFILE3_GPU("cursor");
CStrW cursorName = g_CursorName;
if (cursorName.empty())
{
cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false);
}
else
{
bool forceGL = false;
CFG_GET_VAL("nohwcursor", forceGL);
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// set up transform for GL cursor
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
CMatrix3D transform;
transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
glLoadMatrixf(&transform._11);
#endif
#if OS_ANDROID
#warning TODO: cursors for Android
#else
if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0)
LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName));
#endif
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// restore transform
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
#endif
}
}
glEnable(GL_DEPTH_TEST);
g_Renderer.EndFrame();
PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls);
PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris);
PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris);
PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris);
PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris);
PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats);
PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles);
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameEnd();
ogl_WarnIfError();
}
ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags))
{
// If we're fullscreen, then sometimes (at least on some particular drivers on Linux)
// displaying the error dialog hangs the desktop since the dialog box is behind the
// fullscreen window. So we just force the game to windowed mode before displaying the dialog.
// (But only if we're in the main thread, and not if we're being reentrant.)
if (ThreadUtil::IsMainThread())
{
static bool reentering = false;
if (!reentering)
{
reentering = true;
g_VideoMode.SetFullscreen(false);
reentering = false;
}
}
// We don't actually implement the error display here, so return appropriately
return ERI_NOT_IMPLEMENTED;
}
const std::vector& GetMods(const CmdLineArgs& args, int flags)
{
const bool init_mods = (flags & INIT_MODS) == INIT_MODS;
const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod");
const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
if (!init_mods)
{
// Add the user mod if it should be present
if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user"))
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
g_modsLoaded = args.GetMultiple("mod");
if (add_public)
g_modsLoaded.insert(g_modsLoaded.begin(), "public");
g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
// Add the user mod if not explicitly disabled or we have a dev copy so
// that saved files end up in version control and not in the user mod.
if (add_user)
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
for (size_t i = 0; i < mods.size(); ++i)
{
size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0
size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE;
size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST;
OsPath modName(mods[i]);
if (InDevelopmentCopy())
{
// We are running a dev copy, so only mount mods in the user mod path
// if the mod does not exist in the data path.
if (DirectoryExists(modPath / modName/""))
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
else
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority);
}
else
{
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
// Ensure that user modified files are loaded, if they are present
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1);
}
}
}
static void InitVfs(const CmdLineArgs& args, int flags)
{
TIMER(L"InitVfs");
const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0;
const Paths paths(args);
OsPath logs(paths.Logs());
CreateDirectories(logs, 0700);
psSetLogDir(logs);
// desired location for crashlog is now known. update AppHooks ASAP
// (particularly before the following error-prone operations):
AppHooks hooks = {0};
hooks.bundle_logs = psBundleLogs;
hooks.get_log_dir = psLogDir;
if (setup_error)
hooks.display_error = psDisplayError;
app_hooks_update(&hooks);
g_VFS = CreateVfs();
const OsPath readonlyConfig = paths.RData()/"config"/"";
g_VFS->Mount(L"config/", readonlyConfig);
// Engine localization files.
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
MountMods(paths, GetMods(args, flags));
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1);
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads)
// note: don't bother with g_VFS->TextRepresentation - directories
// haven't yet been populated and are empty.
}
static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
{
// console
TIMER(L"ps_console");
g_Console->UpdateScreenSize(g_xres, g_yres);
// Calculate and store the line spacing
CFontMetrics font(CStrIntern(CONSOLE_FONT));
g_Console->m_iFontHeight = font.GetLineSpacing();
g_Console->m_iFontWidth = font.GetCharacterWidth(L'C');
g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth);
// Offset by an arbitrary amount, to make it fit more nicely
g_Console->m_iFontOffset = 7;
double blinkRate = 0.5;
CFG_GET_VAL("gui.cursorblinkrate", blinkRate);
g_Console->SetCursorBlinkRate(blinkRate);
}
// hotkeys
{
TIMER(L"ps_lang_hotkeys");
LoadHotkeys();
}
if (!setup_gui)
{
// We do actually need *some* kind of GUI loaded, so use the
// (currently empty) Atlas one
g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData);
return;
}
// GUI uses VFS, so this must come after VFS init.
g_GUI->SwitchPage(gui_page, srcScriptInterface, initData);
}
static void InitInput()
{
g_Joystick.Initialise();
// register input handlers
// This stack is constructed so the first added, will be the last
// one called. This is important, because each of the handlers
// has the potential to block events to go further down
// in the chain. I.e. the last one in the list added, is the
// only handler that can block all messages before they are
// processed.
in_add_handler(game_view_handler);
in_add_handler(CProfileViewer::InputThunk);
in_add_handler(conInputHandler);
in_add_handler(HotkeyInputHandler);
// gui_handler needs to be registered after (i.e. called before!) the
// hotkey handler so that input boxes can be typed in without
// setting off hotkeys.
in_add_handler(gui_handler);
in_add_handler(touch_input_handler);
// must be registered after (called before) the GUI which relies on these globals
in_add_handler(GlobalsInputHandler);
}
static void ShutdownPs()
{
SAFE_DELETE(g_GUI);
UnloadHotkeys();
// disable the special Windows cursor, or free textures for OGL cursors
cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false);
}
static void InitRenderer()
{
TIMER(L"InitRenderer");
if(g_NoGLS3TC)
ogl_tex_override(OGL_TEX_S3TC, OGL_TEX_DISABLE);
if(g_NoGLAutoMipmap)
ogl_tex_override(OGL_TEX_AUTO_MIPMAP_GEN, OGL_TEX_DISABLE);
// create renderer
new CRenderer;
// set renderer options from command line options - NOVBO must be set before opening the renderer
// and init them in the ConfigDB when needed
g_Renderer.SetOptionBool(CRenderer::OPT_NOVBO, g_NoGLVBO);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, g_Shadows);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadows", g_Shadows);
g_Renderer.SetOptionBool(CRenderer::OPT_WATEREFFECTS, g_WaterEffects);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "watereffects", g_WaterEffects);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS, g_WaterFancyEffects);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterfancyeffects", g_WaterFancyEffects);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREALDEPTH, g_WaterRealDepth);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrealdepth", g_WaterRealDepth);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFLECTION, g_WaterReflection);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterreflection", g_WaterReflection);
g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFRACTION, g_WaterRefraction);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrefraction", g_WaterRefraction);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWSONWATER, g_WaterShadows);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "watershadows", g_WaterShadows);
g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath));
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadowpcf", g_ShadowPCF);
g_Renderer.SetOptionBool(CRenderer::OPT_PARTICLES, g_Particles);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "particles", g_Particles);
g_Renderer.SetOptionBool(CRenderer::OPT_FOG, g_Fog);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "fog", g_Fog);
g_Renderer.SetOptionBool(CRenderer::OPT_SILHOUETTES, g_Silhouettes);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "silhouettes", g_Silhouettes);
g_Renderer.SetOptionBool(CRenderer::OPT_SHOWSKY, g_ShowSky);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "showsky", g_ShowSky);
g_Renderer.SetOptionBool(CRenderer::OPT_PREFERGLSL, g_PreferGLSL);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "preferglsl", g_PreferGLSL);
g_Renderer.SetOptionBool(CRenderer::OPT_POSTPROC, g_PostProc);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "postproc", g_PostProc);
g_Renderer.SetOptionBool(CRenderer::OPT_SMOOTHLOS, g_SmoothLOS);
g_ConfigDB.SetValueBool(CFG_SYSTEM, "smoothlos", g_SmoothLOS);
// create terrain related stuff
new CTerrainTextureManager;
g_Renderer.Open(g_xres, g_yres);
// Setup lighting environment. Since the Renderer accesses the
// lighting environment through a pointer, this has to be done before
// the first Frame.
g_Renderer.SetLightEnv(&g_LightEnv);
// I haven't seen the camera affecting GUI rendering and such, but the
// viewport has to be updated according to the video mode
SViewPort vp;
vp.m_X = 0;
vp.m_Y = 0;
vp.m_Width = g_xres;
vp.m_Height = g_yres;
g_Renderer.SetViewport(vp);
ColorActivateFastImpl();
ModelRenderer::Init();
}
static void InitSDL()
{
#if OS_LINUX
// In fullscreen mode when SDL is compiled with DGA support, the mouse
// sensitivity often appears to be unusably wrong (typically too low).
// (This seems to be reported almost exclusively on Ubuntu, but can be
// reproduced on Gentoo after explicitly enabling DGA.)
// Disabling the DGA mouse appears to fix that problem, and doesn't
// have any obvious negative effects.
setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0);
#endif
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0)
{
LOGERROR("SDL library initialization failed: %s", SDL_GetError());
throw PSERROR_System_SDLInitFailed();
}
atexit(SDL_Quit);
// Text input is active by default, disable it until it is actually needed.
SDL_StopTextInput();
#if OS_MACOSX
// Some Mac mice only have one button, so they can't right-click
// but SDL2 can emulate that with Ctrl+Click
bool macMouse = false;
CFG_GET_VAL("macmouse", macMouse);
SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0");
#endif
}
static void ShutdownSDL()
{
SDL_Quit();
sys_cursor_reset();
}
void EndGame()
{
const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled();
if (g_Game && g_Game->IsGameStarted() && !g_Game->IsVisualReplay() &&
g_AtlasGameLoop && !g_AtlasGameLoop->running && !nonVisual)
VisualReplay::SaveReplayMetadata(g_GUI->GetActiveGUI()->GetScriptInterface().get());
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_Game);
if (!nonVisual)
{
ISoundManager::CloseGame();
g_Renderer.ResetState();
}
}
void Shutdown(int flags)
{
const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled();
if ((flags & SHUTDOWN_FROM_CONFIG))
goto from_config;
EndGame();
SAFE_DELETE(g_XmppClient);
ShutdownPs();
TIMER_BEGIN(L"shutdown TexMan");
delete &g_TexMan;
TIMER_END(L"shutdown TexMan");
// destroy renderer if it was initialised
if (!nonVisual)
{
TIMER_BEGIN(L"shutdown Renderer");
delete &g_Renderer;
g_VBMan.Shutdown();
TIMER_END(L"shutdown Renderer");
}
g_Profiler2.ShutdownGPU();
// Free cursors before shutting down SDL, as they may depend on SDL.
cursor_shutdown();
TIMER_BEGIN(L"shutdown SDL");
ShutdownSDL();
TIMER_END(L"shutdown SDL");
if (!nonVisual)
g_VideoMode.Shutdown();
TIMER_BEGIN(L"shutdown UserReporter");
g_UserReporter.Deinitialize();
TIMER_END(L"shutdown UserReporter");
delete &g_L10n;
from_config:
TIMER_BEGIN(L"shutdown ConfigDB");
delete &g_ConfigDB;
TIMER_END(L"shutdown ConfigDB");
SAFE_DELETE(g_Console);
// This is needed to ensure that no callbacks from the JSAPI try to use
// the profiler when it's already destructed
g_ScriptRuntime.reset();
// resource
// first shut down all resource owners, and then the handle manager.
TIMER_BEGIN(L"resource modules");
ISoundManager::SetEnabled(false);
g_VFS.reset();
// this forcibly frees all open handles (thus preventing real leaks),
// and makes further access to h_mgr impossible.
h_mgr_shutdown();
file_stats_dump();
TIMER_END(L"resource modules");
TIMER_BEGIN(L"shutdown misc");
timer_DisplayClientTotals();
CNetHost::Deinitialize();
// should be last, since the above use them
SAFE_DELETE(g_Logger);
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
TIMER_END(L"shutdown misc");
}
#if OS_UNIX
static void FixLocales()
{
#if OS_MACOSX || OS_BSD
// OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle
// wide characters. Peculiarly the string "UTF-8" seems to be acceptable
// despite not being a real locale, and it's conveniently language-agnostic,
// so use that.
setlocale(LC_CTYPE, "UTF-8");
#endif
// On misconfigured systems with incorrect locale settings, we'll die
// with a C++ exception when some code (e.g. Boost) tries to use locales.
// To avoid death, we'll detect the problem here and warn the user and
// reset to the default C locale.
// For informing the user of the problem, use the list of env vars that
// glibc setlocale looks at. (LC_ALL is checked first, and LANG last.)
const char* const LocaleEnvVars[] = {
"LC_ALL",
"LC_COLLATE",
"LC_CTYPE",
"LC_MONETARY",
"LC_NUMERIC",
"LC_TIME",
"LC_MESSAGES",
"LANG"
};
try
{
// this constructor is similar to setlocale(LC_ALL, ""),
// but instead of returning NULL, it throws runtime_error
// when the first locale env variable found contains an invalid value
std::locale("");
}
catch (std::runtime_error&)
{
LOGWARNING("Invalid locale settings");
for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++)
{
if (char* envval = getenv(LocaleEnvVars[i]))
LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval);
else
LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]);
}
// We should set LC_ALL since it overrides LANG
if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1))
debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable.");
else
LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL"));
}
}
#else
static void FixLocales()
{
// Do nothing on Windows
}
#endif
void EarlyInit()
{
// If you ever want to catch a particular allocation:
//_CrtSetBreakAlloc(232647);
ThreadUtil::SetMainThread();
debug_SetThreadName("main");
// add all debug_printf "tags" that we are interested in:
debug_filter_add("TIMER");
timer_LatchStartTime();
// initialise profiler early so it can profile startup,
// but only after LatchStartTime
g_Profiler2.Initialise();
FixLocales();
// Because we do GL calls from a secondary thread, Xlib needs to
// be told to support multiple threads safely.
// This is needed for Atlas, but we have to call it before any other
// Xlib functions (e.g. the ones used when drawing the main menu
// before launching Atlas)
#if MUST_INIT_X11
int status = XInitThreads();
if (status == 0)
debug_printf("Error enabling thread-safety via XInitThreads\n");
#endif
// Initialise the low-quality rand function
srand(time(NULL)); // NOTE: this rand should *not* be used for simulation!
}
bool Autostart(const CmdLineArgs& args);
/**
* Returns true if the user has intended to start a visual replay from command line.
*/
bool AutostartVisualReplay(const std::string& replayFile);
bool Init(const CmdLineArgs& args, int flags)
{
h_mgr_init();
// Do this as soon as possible, because it chdirs
// and will mess up the error reporting if anything
// crashes before the working directory is set.
InitVfs(args, flags);
// This must come after VFS init, which sets the current directory
// (required for finding our output log files).
g_Logger = new CLogger;
new CProfileViewer;
new CProfileManager; // before any script code
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
// Set up the console early, so that debugging
// messages can be logged to it. (The console's size
// and fonts are set later in InitPs())
g_Console = new CConsole();
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
// Using a global object for the runtime is a workaround until Simulation and AI use
// their own threads and also their own runtimes.
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
// Special command-line mode to dump the entity schemas instead of running the game.
// (This must be done after loading VFS etc, but should be done before wasting time
// on anything else.)
if (args.Has("dumpSchema"))
{
CSimulation2 sim(NULL, g_ScriptRuntime, NULL);
sim.LoadDefaultScripts();
std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
f << sim.GenerateSchema();
std::cout << "Generated entity.rng\n";
exit(0);
}
CNetHost::Initialize();
#if CONFIG2_AUDIO
if (!args.Has("autostart-nonvisual"))
ISoundManager::CreateSoundManager();
#endif
// Check if there are mods specified on the command line,
// or if we already set the mods (~INIT_MODS),
// else check if there are mods that should be loaded specified
// in the config and load those (by aborting init and restarting
// the engine).
if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS)
{
CStr modstring;
CFG_GET_VAL("mod.enabledmods", modstring);
if (!modstring.empty())
{
std::vector mods;
boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
std::swap(g_modsLoaded, mods);
// Abort init and restart
restart_engine();
return false;
}
}
new L10n;
// Optionally start profiler HTTP output automatically
// (By default it's only enabled by a hotkey, for security/performance)
bool profilerHTTPEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable);
if (profilerHTTPEnable)
g_Profiler2.EnableHTTP();
if (!g_Quickstart)
g_UserReporter.Initialize(); // after config
PROFILE2_EVENT("Init finished");
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
if(setup_vmode)
{
InitSDL();
if (!g_VideoMode.InitSDL())
throw PSERROR_System_VmodeFailed(); // abort startup
}
RunHardwareDetection();
const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file
SetTextureQuality(quality);
ogl_WarnIfError();
// Optionally start profiler GPU timings automatically
// (By default it's only enabled by a hotkey, for performance/compatibility)
bool profilerGPUEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable);
if (profilerGPUEnable)
g_Profiler2.EnableGPU();
if(!g_Quickstart)
{
WriteSystemInfo();
// note: no longer vfs_display here. it's dog-slow due to unbuffered
// file output and very rarely needed.
}
if(g_DisableAudio)
ISoundManager::SetEnabled(false);
g_GUI = new CGUIManager();
// (must come after SetVideoMode, since it calls ogl_Init)
if (ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB
&& ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL
{
DEBUG_DISPLAY_ERROR(
L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders."
L" In the future, the game will not support pre-shader graphics cards."
L" You are advised to try installing newer drivers and/or upgrade your graphics card."
L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734"
);
// TODO: actually quit once fixed function support is dropped
}
const char* missing = ogl_HaveExtensions(0,
"GL_ARB_multitexture",
"GL_EXT_draw_range_elements",
"GL_ARB_texture_env_combine",
"GL_ARB_texture_env_dot3",
NULL);
if(missing)
{
wchar_t buf[500];
swprintf_s(buf, ARRAY_SIZE(buf),
L"The %hs extension doesn't appear to be available on your computer."
L" The game may still work, though - you are welcome to try at your own risk."
L" If not or it doesn't look right, upgrade your graphics card.",
missing
);
DEBUG_DISPLAY_ERROR(buf);
// TODO: i18n
}
if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar"))
{
DEBUG_DISPLAY_ERROR(
L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer."
L" Shadows are not available and overall graphics quality might suffer."
L" You are advised to try installing newer drivers and/or upgrade your graphics card.");
g_Shadows = false;
}
ogl_WarnIfError();
InitRenderer();
InitInput();
ogl_WarnIfError();
// TODO: Is this the best place for this?
if (VfsDirectoryExists(L"maps/"))
CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng");
try
{
if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args))
{
const bool setup_gui = ((flags & INIT_NO_GUI) == 0);
// We only want to display the splash screen at startup
shared_ptr scriptInterface = g_GUI->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue data(cx);
if (g_GUI)
{
scriptInterface->Eval("({})", &data);
scriptInterface->SetProperty(data, "isStartup", true);
}
InitPs(setup_gui, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), data);
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map Loading failed
// Start the engine so we have a GUI
InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue);
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
}
void InitNonVisual(const CmdLineArgs& args)
{
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
Autostart(args);
}
void RenderGui(bool RenderingState)
{
g_DoRenderGui = RenderingState;
}
void RenderLogger(bool RenderingState)
{
g_DoRenderLogger = RenderingState;
}
void RenderCursor(bool RenderingState)
{
g_DoRenderCursor = RenderingState;
}
/**
* Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON
* data from it.
* The scenario map format is used for scenario and skirmish map types (random
* games do not use a "map" (format) but a small JavaScript program which
* creates a map on the fly). It contains a section to initialize the game
* setup screen.
* @param mapPath Absolute path (from VFS root) to the map file to peek in.
* @return ScriptSettings in JSON format extracted from the map.
*/
CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath)
{
CXeromyces mapFile;
const char *pathToSettings[] =
{
"Scenario", "ScriptSettings", "" // Path to JSON data in map
};
Status loadResult = mapFile.Load(g_VFS, mapPath);
if (INFO::OK != loadResult)
{
LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8());
throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos.");
}
XMBElement mapElement = mapFile.GetRoot();
// Select the ScriptSettings node in the map file...
for (int i = 0; pathToSettings[i][0]; ++i)
{
int childId = mapFile.GetElementID(pathToSettings[i]);
XMBElementList nodes = mapElement.GetChildNodes();
auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) {
return child.GetNodeName() == childId;
});
if (it != nodes.end())
mapElement = *it;
}
// ... they contain a JSON document to initialize the game setup
// screen
return mapElement.GetText();
}
/*
* Command line options for autostart
* (keep synchronized with binaries/system/readme.txt):
*
* -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME;
* TYPEDIR is skirmishes, scenarios, or random
* -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random)
* -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra)
* -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI
* (0: sandbox, 5: very hard)
* -autostart-aiseed=AISEED sets the seed used for the AI random
* generator (default 0, use -1 for random)
* -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV
* (skirmish and random maps only)
* -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2).
* -autostart-nonvisual disable any graphics and sounds
* -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME
* located in simulation/data/settings/victory_conditions/
* -autostart-victoryduration=NUM sets the victory duration NUM for specific victory conditions
*
* Multiplayer:
* -autostart-playername=NAME sets local player NAME (default 'anonymous')
* -autostart-host sets multiplayer host mode
* -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer
* game (default 2)
* -autostart-client=IP sets multiplayer client to join host at
* given IP address
* Random maps only:
* -autostart-size=TILES sets random map size in TILES (default 192)
* -autostart-players=NUMBER sets NUMBER of players on random map
* (default 2)
*
* Examples:
* 1) "Bob" will host a 2 player game on the Arcadia map:
* -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob"
*
* 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot:
* -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra
*/
bool Autostart(const CmdLineArgs& args)
{
CStr autoStartName = args.Get("autostart");
if (autoStartName.empty())
return false;
const bool nonVisual = args.Has("autostart-nonvisual");
g_Game = new CGame(nonVisual, !nonVisual);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attrs(cx);
scriptInterface.Eval("({})", &attrs);
JS::RootedValue settings(cx);
scriptInterface.Eval("({})", &settings);
JS::RootedValue playerData(cx);
scriptInterface.Eval("([])", &playerData);
// The directory in front of the actual map name indicates which type
// of map is being loaded. Drawback of this approach is the association
// of map types and folders is hard-coded, but benefits are:
// - No need to pass the map type via command line separately
// - Prevents mixing up of scenarios and skirmish maps to some degree
Path mapPath = Path(autoStartName);
std::wstring mapDirectory = mapPath.Parent().Filename().string();
std::string mapType;
if (mapDirectory == L"random")
{
// Random map definition will be loaded from JSON file, so we need to parse it
std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json";
JS::RootedValue scriptData(cx);
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings))
{
// JSON loaded ok - copy script name over to game attributes
std::wstring scriptFile;
scriptInterface.GetProperty(settings, "Script", scriptFile);
scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename
}
else
{
// Problem with JSON file
LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
}
// Get optional map size argument (default 192)
uint mapSize = 192;
if (args.Has("autostart-size"))
{
CStr size = args.Get("autostart-size");
mapSize = size.ToUInt();
}
scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches)
// Get optional number of players (default 2)
size_t numPlayers = 2;
if (args.Has("autostart-players"))
{
CStr num = args.Get("autostart-players");
numPlayers = num.ToUInt();
}
// Set up player data
for (size_t i = 0; i < numPlayers; ++i)
{
JS::RootedValue player(cx);
scriptInterface.Eval("({})", &player);
// We could load player_defaults.json here, but that would complicate the logic
// even more and autostart is only intended for developers anyway
scriptInterface.SetProperty(player, "Civ", std::string("athen"));
scriptInterface.SetPropertyInt(playerData, i, player);
}
mapType = "random";
}
else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// Initialize general settings from the map data so some values
// (e.g. name of map) are always present, even when autostart is
// partially configured
CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml");
scriptInterface.ParseJSON(mapSettingsJSON, &settings);
// Initialize the playerData array being modified by autostart
// with the real map data, so sensible values are present:
scriptInterface.GetProperty(settings, "PlayerData", &playerData);
if (mapDirectory == L"scenarios")
mapType = "scenario";
else
mapType = "skirmish";
}
else
{
LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory));
throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types.");
}
scriptInterface.SetProperty(attrs, "mapType", mapType);
scriptInterface.SetProperty(attrs, "map", std::string("maps/" + autoStartName));
scriptInterface.SetProperty(settings, "mapType", mapType);
scriptInterface.SetProperty(settings, "CheatsEnabled", true);
// The seed is used for both random map generation and simulation
u32 seed = 0;
if (args.Has("autostart-seed"))
{
CStr seedArg = args.Get("autostart-seed");
if (seedArg == "-1")
seed = rand();
else
seed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "Seed", seed);
// Set seed for AIs
u32 aiseed = 0;
if (args.Has("autostart-aiseed"))
{
CStr seedArg = args.Get("autostart-aiseed");
if (seedArg == "-1")
aiseed = rand();
else
aiseed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "AISeed", aiseed);
// Set player data for AIs
// attrs.settings = { PlayerData: [ { AI: ... }, ... ] }
// or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set
int offset = 1;
JS::RootedValue player(cx);
if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull())
offset = 0;
// Set teams
if (args.Has("autostart-team"))
{
std::vector civArgs = args.GetMultiple("autostart-team");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
int teamID = civArgs[i].AfterFirst(":").ToInt() - 1;
scriptInterface.SetProperty(player, "Team", teamID);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
if (args.Has("autostart-ai"))
{
std::vector aiArgs = args.GetMultiple("autostart-ai");
for (size_t i = 0; i < aiArgs.size(); ++i)
{
int playerID = aiArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
CStr name = aiArgs[i].AfterFirst(":");
scriptInterface.SetProperty(player, "AI", std::string(name));
scriptInterface.SetProperty(player, "AIDiff", 3);
+ scriptInterface.SetProperty(player, "AIBehavior", std::string("generalist"));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set AI difficulty
if (args.Has("autostart-aidiff"))
{
std::vector civArgs = args.GetMultiple("autostart-aidiff");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
int difficulty = civArgs[i].AfterFirst(":").ToInt();
scriptInterface.SetProperty(player, "AIDiff", difficulty);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set player data for Civs
if (args.Has("autostart-civ"))
{
if (mapDirectory != L"scenarios")
{
std::vector civArgs = args.GetMultiple("autostart-civ");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID);
continue;
}
scriptInterface.Eval("({})", &player);
}
CStr name = civArgs[i].AfterFirst(":");
scriptInterface.SetProperty(player, "Civ", std::string(name));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
else
LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios");
}
// Add player data to map settings
scriptInterface.SetProperty(settings, "PlayerData", playerData);
// Add map settings to game attributes
scriptInterface.SetProperty(attrs, "settings", settings);
JS::RootedValue mpInitData(cx);
scriptInterface.Eval("({isNetworked:true, playerAssignments:{}})", &mpInitData);
scriptInterface.SetProperty(mpInitData, "attribs", attrs);
// Get optional playername
CStrW userName = L"anonymous";
if (args.Has("autostart-playername"))
userName = args.Get("autostart-playername").FromUTF8();
// Add additional scripts to the TriggerScripts property
std::vector triggerScriptsVector;
JS::RootedValue triggerScripts(cx);
if (scriptInterface.HasProperty(settings, "TriggerScripts"))
{
scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts);
FromJSVal_vector(cx, triggerScripts, triggerScriptsVector);
}
if (nonVisual)
{
CStr nonVisualScript = "scripts/NonVisualTrigger.js";
triggerScriptsVector.push_back(nonVisualScript.FromUTF8());
}
if (args.Has("autostart-victory"))
{
CStrW scriptName = args.Get("autostart-victory").FromUTF8();
CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + scriptName + L".json";
JS::RootedValue scriptData(cx);
JS::RootedValue data(cx);
JS::RootedValue victoryScripts(cx);
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined()
&& scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined())
{
std::vector victoryScriptsVector;
FromJSVal_vector(cx, victoryScripts, victoryScriptsVector);
triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end());
}
}
ToJSVal_vector(cx, &triggerScripts, triggerScriptsVector);
scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts);
if (args.Has("autostart-victoryduration"))
scriptInterface.SetProperty(settings, "VictoryDuration", args.Get("autostart-victoryduration").ToInt());
if (args.Has("autostart-host"))
{
InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData);
size_t maxPlayers = 2;
if (args.Has("autostart-host-players"))
maxPlayers = args.Get("autostart-host-players").ToUInt();
g_NetServer = new CNetServer(maxPlayers);
g_NetServer->UpdateGameAttributes(&attrs, scriptInterface);
bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT);
ENSURE(ok);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(userName);
g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT);
}
else if (args.Has("autostart-client"))
{
InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData);
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(userName);
CStr ip = args.Get("autostart-client");
if (ip.empty())
ip = "127.0.0.1";
bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT);
ENSURE(ok);
}
else
{
g_Game->SetPlayerID(1);
g_Game->StartGame(&attrs, "");
LDR_NonprogressiveLoad();
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
if (nonVisual)
return true;
InitPs(true, L"page_session.xml", NULL, JS::UndefinedHandleValue);
}
return true;
}
bool AutostartVisualReplay(const std::string& replayFile)
{
if (!FileExists(OsPath(replayFile)))
return false;
g_Game = new CGame(false, false);
g_Game->SetPlayerID(-1);
g_Game->StartVisualReplay(replayFile);
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
InitPs(true, L"page_session.xml", &scriptInterface, JS::UndefinedHandleValue);
return true;
}
void CancelLoad(const CStrW& message)
{
shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
JSContext* cx = pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, pScriptInterface->GetGlobalObject());
LDR_Cancel();
if (g_GUI &&
g_GUI->HasPages() &&
pScriptInterface->HasProperty(global, "cancelOnLoadGameError"))
pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message);
}
bool InDevelopmentCopy()
{
if (!g_CheckedIfInDevelopmentCopy)
{
g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK);
g_CheckedIfInDevelopmentCopy = true;
}
return g_InDevelopmentCopy;
}
Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 20645)
+++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 20646)
@@ -1,1214 +1,1220 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpAIManager.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Terrain.h"
#include "lib/timer.h"
#include "lib/tex/tex.h"
#include "lib/allocators/shared_ptr.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Profile.h"
#include "ps/scripting/JSInterface_VFS.h"
#include "ps/TemplateLoader.h"
#include "ps/Util.h"
#include "simulation2/components/ICmpAIInterface.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpDataTemplateManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/LongPathfinder.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "simulation2/serialization/SerializeTemplates.h"
extern void kill_mainloop();
/**
* @file
* Player AI interface.
* AI is primarily scripted, and the CCmpAIManager component defined here
* takes care of managing all the scripts.
*
* To avoid slow AI scripts causing jerky rendering, they are run in a background
* thread (maintained by CAIWorker) so that it's okay if they take a whole simulation
* turn before returning their results (though preferably they shouldn't use nearly
* that much CPU).
*
* CCmpAIManager grabs the world state after each turn (making use of AIInterface.js
* and AIProxy.js to decide what data to include) then passes it to CAIWorker.
* The AI scripts will then run asynchronously and return a list of commands to execute.
* Any attempts to read the command list (including indirectly via serialization)
* will block until it's actually completed, so the rest of the engine should avoid
* reading it for as long as possible.
*
* JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone.
*
* TODO: actually the thread isn't implemented yet, because performance hasn't been
* sufficiently problematic to justify the complexity yet, but the CAIWorker interface
* is designed to hopefully support threading when we want it.
*/
/**
* Implements worker thread for CCmpAIManager.
*/
class CAIWorker
{
private:
class CAIPlayer
{
NONCOPYABLE(CAIPlayer);
public:
- CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty,
+ CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior,
shared_ptr scriptInterface) :
- m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty),
+ m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior),
m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetJSRuntime())
{
}
bool Initialise()
{
// LoadScripts will only load each script once even though we call it for each player
if (!m_Worker.LoadScripts(m_AIName))
return false;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
OsPath path = L"simulation/ai/" + m_AIName + L"/data.json";
JS::RootedValue metadata(cx);
m_Worker.LoadMetadata(path, &metadata);
if (metadata.isUndefined())
{
LOGERROR("Failed to create AI player: can't find %s", path.string8());
return false;
}
// Get the constructor name from the metadata
std::string moduleName;
std::string constructor;
JS::RootedValue objectWithConstructor(cx); // object that should contain the constructor function
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->HasProperty(metadata, "moduleName"))
{
LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8());
return false;
}
m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName);
if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor)
|| objectWithConstructor.isUndefined())
{
LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName);
return false;
}
if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor))
{
LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8());
return false;
}
// Get the constructor function from the loaded scripts
if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor)
|| ctor.isNull())
{
LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor);
return false;
}
m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent);
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
m_ScriptInterface->Eval(L"({})", &settings);
m_ScriptInterface->SetProperty(settings, "player", m_Player, false);
m_ScriptInterface->SetProperty(settings, "difficulty", m_Difficulty, false);
+ m_ScriptInterface->SetProperty(settings, "behavior", m_Behavior, false);
+
if (!m_UseSharedComponent)
{
ENSURE(m_Worker.m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false);
}
JS::AutoValueVector argv(cx);
argv.append(settings.get());
m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj);
if (m_Obj.get().isNull())
{
LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor);
return false;
}
return true;
}
void Run(JS::HandleValue state, int playerID)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID);
}
// overloaded with a sharedAI part.
// javascript can handle both natively on the same function.
void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI);
}
void InitAI(JS::HandleValue state, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI);
}
CAIWorker& m_Worker;
std::wstring m_AIName;
player_id_t m_Player;
u8 m_Difficulty;
+ std::wstring m_Behavior;
bool m_UseSharedComponent;
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptInterface;
JS::PersistentRootedValue m_Obj;
std::vector > m_Commands;
};
public:
struct SCommandSets
{
player_id_t player;
std::vector > commands;
};
CAIWorker() :
m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptRuntime)),
m_TurnNum(0),
m_CommandsComputed(true),
m_HasLoadedEntityTemplates(false),
m_HasSharedComponent(false),
m_SerializablePrototypes(new ObjectIdCache(g_ScriptRuntime)),
m_EntityTemplates(g_ScriptRuntime->m_rt),
m_TechTemplates(g_ScriptRuntime->m_rt),
m_SharedAIObj(g_ScriptRuntime->m_rt),
m_PassabilityMapVal(g_ScriptRuntime->m_rt),
m_TerritoryMapVal(g_ScriptRuntime->m_rt)
{
m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface->LoadGlobalScripts();
m_ScriptInterface->SetCallbackData(static_cast (this));
m_SerializablePrototypes->init();
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
m_ScriptInterface->RegisterFunction("PostCommand");
m_ScriptInterface->RegisterFunction("IncludeModule");
m_ScriptInterface->RegisterFunction("Exit");
m_ScriptInterface->RegisterFunction("ComputePath");
m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage");
m_ScriptInterface->RegisterFunction("GetTemplate");
JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get()));
}
~CAIWorker()
{
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
}
bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; }
bool LoadScripts(const std::wstring& moduleName)
{
// Ignore modules that are already loaded
if (m_LoadedModules.find(moduleName) != m_LoadedModules.end())
return true;
// Mark this as loaded, to prevent it recursively loading itself
m_LoadedModules.insert(moduleName);
// Load and execute *.js
VfsPaths pathnames;
if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0)
{
LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName));
return false;
}
for (const VfsPath& path : pathnames)
{
if (!m_ScriptInterface->LoadGlobalScriptFile(path))
{
LOGERROR("Failed to load script %s", path.string8());
return false;
}
}
return true;
}
static void IncludeModule(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->LoadScripts(name);
}
static void PostCommand(ScriptInterface::CxPrivate* pCxPrivate, int playerid, JS::HandleValue cmd)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->PostCommand(playerid, cmd);
}
void PostCommand(int playerid, JS::HandleValue cmd)
{
for (size_t i=0; im_Player == playerid)
{
m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd));
return;
}
}
LOGERROR("Invalid playerid in PostCommand!");
}
static JS::Value ComputePath(ScriptInterface::CxPrivate* pCxPrivate,
JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
JSAutoRequest rq(cx);
CFixedVector2D pos, goalPos;
std::vector waypoints;
JS::RootedValue retVal(cx);
self->m_ScriptInterface->FromJSVal(cx, position, pos);
self->m_ScriptInterface->FromJSVal(cx, goal, goalPos);
self->ComputePath(pos, goalPos, passClass, waypoints);
self->m_ScriptInterface->ToJSVal >(cx, &retVal, waypoints);
return retVal;
}
void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints)
{
WaypointPath ret;
PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y };
m_LongPathfinder.ComputePath(pos.X, pos.Y, pathGoal, passClass, ret);
for (Waypoint& wp : ret.m_Waypoints)
waypoints.emplace_back(wp.x, wp.z);
}
static CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
return self->GetTemplate(name);
}
CParamNode GetTemplate(const std::string& name)
{
if (!m_TemplateLoader.TemplateExists(name))
return CParamNode(false);
return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity");
}
static void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
kill_mainloop();
}
/**
* Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights).
*/
static void DumpImage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max)
{
// TODO: this is totally not threadsafe.
VfsPath filename = L"screenshots/aidump/" + name;
if (data.size() != w*h)
{
debug_warn(L"DumpImage: data size doesn't match w*h");
return;
}
if (max == 0)
{
debug_warn(L"DumpImage: max must not be 0");
return;
}
const size_t bpp = 8;
int flags = TEX_BOTTOM_UP|TEX_GREY;
const size_t img_size = w * h * bpp/8;
const size_t hdr_size = tex_hdr_size(filename);
shared_ptr buf;
AllocateAligned(buf, hdr_size+img_size, maxSectorSize);
Tex t;
if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0)
return;
u8* img = buf.get() + hdr_size;
for (size_t i = 0; i < data.size(); ++i)
img[i] = (u8)((data[i] * 255) / max);
tex_write(&t, filename);
}
void SetRNGSeed(u32 seed)
{
m_RNG.seed(seed);
}
bool TryLoadSharedComponent(bool hasTechs)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
// we don't need to load it.
if (!m_HasSharedComponent)
return false;
// reset the value so it can be used to determine if we actually initialized it.
m_HasSharedComponent = false;
if (LoadScripts(L"common-api"))
m_HasSharedComponent = true;
else
return false;
// mainly here for the error messages
OsPath path = L"simulation/ai/common-api/";
// Constructor name is SharedScript, it's in the module API3
// TODO: Hardcoding this is bad, we need a smarter way.
JS::RootedValue AIModule(cx);
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3");
return false;
}
if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor)
|| ctor.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript");
return false;
}
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
m_ScriptInterface->Eval(L"({})", &settings);
JS::RootedValue playersID(cx);
m_ScriptInterface->Eval(L"({})", &playersID);
for (size_t i = 0; i < m_Players.size(); ++i)
{
JS::RootedValue val(cx);
m_ScriptInterface->ToJSVal(cx, &val, m_Players[i]->m_Player);
m_ScriptInterface->SetPropertyInt(playersID, i, val, true);
}
m_ScriptInterface->SetProperty(settings, "players", playersID);
ENSURE(m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_EntityTemplates, false);
if (hasTechs)
{
m_ScriptInterface->SetProperty(settings, "techTemplates", m_TechTemplates, false);
}
else
{
// won't get the tech templates directly.
JS::RootedValue fakeTech(cx);
m_ScriptInterface->Eval("({})", &fakeTech);
m_ScriptInterface->SetProperty(settings, "techTemplates", fakeTech, false);
}
JS::AutoValueVector argv(cx);
argv.append(settings);
m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj);
if (m_SharedAIObj.get().isNull())
{
LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript");
return false;
}
return true;
}
- bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty)
+ bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior)
{
- shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptInterface));
+ shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, behavior, m_ScriptInterface));
if (!ai->Initialise())
return false;
// this will be set to true if we need to load the shared Component.
if (!m_HasSharedComponent)
m_HasSharedComponent = ai->m_UseSharedComponent;
m_Players.push_back(ai);
return true;
}
bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
- // this will be run last by InitGame.Js, passing the full game representation.
+ // this will be run last by InitGame.js, passing the full game representation.
// For now it will run for the shared Component.
// This is NOT run during deserialization.
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
m_ScriptInterface->ReadStructuredClone(gameState, &state);
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, passabilityMap);
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, territoryMap);
m_PassabilityMap = passabilityMap;
m_NonPathfindingPassClasses = nonPathfindingPassClassMasks;
m_PathfindingPassClasses = pathfindingPassClassMasks;
m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
if (m_HasSharedComponent)
{
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state);
for (size_t i = 0; i < m_Players.size(); ++i)
{
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->InitAI(state, m_SharedAIObj);
}
}
return true;
}
void UpdateGameState(const shared_ptr& gameState)
{
ENSURE(m_CommandsComputed);
m_GameState = gameState;
}
void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
ENSURE(m_CommandsComputed);
bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H;
m_PassabilityMap = passabilityMap;
if (globallyDirty)
m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
else
m_LongPathfinder.Update(&m_PassabilityMap, dirtinessGrid);
JSContext* cx = m_ScriptInterface->GetContext();
if (dimensionChange || justDeserialized)
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, m_PassabilityMap);
else
{
// Avoid a useless memory reallocation followed by a garbage collection.
JSAutoRequest rq(cx);
JS::RootedObject mapObj(cx, &m_PassabilityMapVal.toObject());
JS::RootedValue mapData(cx);
ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData));
JS::RootedObject dataObj(cx, &mapData.toObject());
u32 length = 0;
ENSURE(JS_GetArrayLength(cx, dataObj, &length));
u32 nbytes = (u32)(length * sizeof(NavcellData));
JS::AutoCheckCannotGC nogc;
memcpy((void*)JS_GetUint16ArrayData(dataObj, nogc), m_PassabilityMap.m_Data, nbytes);
}
}
void UpdateTerritoryMap(const Grid& territoryMap)
{
ENSURE(m_CommandsComputed);
bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H;
m_TerritoryMap = territoryMap;
JSContext* cx = m_ScriptInterface->GetContext();
if (dimensionChange)
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, m_TerritoryMap);
else
{
// Avoid a useless memory reallocation followed by a garbage collection.
JSAutoRequest rq(cx);
JS::RootedObject mapObj(cx, &m_TerritoryMapVal.toObject());
JS::RootedValue mapData(cx);
ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData));
JS::RootedObject dataObj(cx, &mapData.toObject());
u32 length = 0;
ENSURE(JS_GetArrayLength(cx, dataObj, &length));
u32 nbytes = (u32)(length * sizeof(u8));
JS::AutoCheckCannotGC nogc;
memcpy((void*)JS_GetUint8ArrayData(dataObj, nogc), m_TerritoryMap.m_Data, nbytes);
}
}
void StartComputation()
{
m_CommandsComputed = false;
}
void WaitToFinishComputation()
{
if (!m_CommandsComputed)
{
PerformComputation();
m_CommandsComputed = true;
}
}
void GetCommands(std::vector& commands)
{
WaitToFinishComputation();
commands.clear();
commands.resize(m_Players.size());
for (size_t i = 0; i < m_Players.size(); ++i)
{
commands[i].player = m_Players[i]->m_Player;
commands[i].commands = m_Players[i]->m_Commands;
}
}
void RegisterTechTemplates(const shared_ptr& techTemplates)
{
m_ScriptInterface->ReadStructuredClone(techTemplates, &m_TechTemplates);
}
void LoadEntityTemplates(const std::vector >& templates)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
m_HasLoadedEntityTemplates = true;
m_ScriptInterface->Eval("({})", &m_EntityTemplates);
JS::RootedValue val(cx);
for (size_t i = 0; i < templates.size(); ++i)
{
templates[i].second->ToJSVal(cx, false, &val);
m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true);
}
}
void Serialize(std::ostream& stream, bool isDebug)
{
WaitToFinishComputation();
if (isDebug)
{
CDebugSerializer serializer(*m_ScriptInterface, stream);
serializer.Indent(4);
SerializeState(serializer);
}
else
{
CStdSerializer serializer(*m_ScriptInterface, stream);
// TODO: see comment in Deserialize()
serializer.SetSerializablePrototypes(m_SerializablePrototypes);
SerializeState(serializer);
}
}
void SerializeState(ISerializer& serializer)
{
if (m_Players.empty())
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
std::stringstream rngStream;
rngStream << m_RNG;
serializer.StringASCII("rng", rngStream.str(), 0, 32);
serializer.NumberU32_Unbounded("turn", m_TurnNum);
serializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
JS::RootedValue sharedData(cx);
if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData))
LOGERROR("AI shared script Serialize call failed");
serializer.ScriptVal("sharedData", &sharedData);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
serializer.String("name", m_Players[i]->m_AIName, 1, 256);
serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player);
serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty);
+ serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256);
serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size());
for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j)
{
JS::RootedValue val(cx);
m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val);
serializer.ScriptVal("command", &val);
}
bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize");
if (hasCustomSerialize)
{
JS::RootedValue scriptData(cx);
if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData))
LOGERROR("AI script Serialize call failed");
serializer.ScriptVal("data", &scriptData);
}
else
{
serializer.ScriptVal("data", &m_Players[i]->m_Obj);
}
}
// AI pathfinder
SerializeMap()(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(serializer, "pathfinding pass classes", m_PathfindingPassClasses);
serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W);
serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H);
serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data,
m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData));
}
void Deserialize(std::istream& stream, u32 numAis)
{
m_PlayerMetadata.clear();
m_Players.clear();
if (numAis == 0)
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad
CStdDeserializer deserializer(*m_ScriptInterface, stream);
std::string rngString;
std::stringstream rngStream;
deserializer.StringASCII("rng", rngString, 0, 32);
rngStream << rngString;
rngStream >> m_RNG;
deserializer.NumberU32_Unbounded("turn", m_TurnNum);
deserializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
TryLoadSharedComponent(false);
JS::RootedValue sharedData(cx);
deserializer.ScriptVal("sharedData", &sharedData);
if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData))
LOGERROR("AI shared script Deserialize call failed");
}
for (size_t i = 0; i < numAis; ++i)
{
std::wstring name;
player_id_t player;
u8 difficulty;
+ std::wstring behavior;
deserializer.String("name", name, 1, 256);
deserializer.NumberI32_Unbounded("player", player);
deserializer.NumberU8_Unbounded("difficulty",difficulty);
- if (!AddPlayer(name, player, difficulty))
+ deserializer.String("behavior", behavior, 1, 256);
+ if (!AddPlayer(name, player, difficulty, behavior))
throw PSERROR_Deserialize_ScriptError();
u32 numCommands;
deserializer.NumberU32_Unbounded("num commands", numCommands);
m_Players.back()->m_Commands.reserve(numCommands);
for (size_t j = 0; j < numCommands; ++j)
{
JS::RootedValue val(cx);
deserializer.ScriptVal("command", &val);
m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val));
}
// TODO: this is yucky but necessary while the AIs are sharing data between contexts;
// ideally a new (de)serializer instance would be created for each player
// so they would have a single, consistent script context to use and serializable
// prototypes could be stored in their ScriptInterface
deserializer.SetSerializablePrototypes(m_DeserializablePrototypes);
bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize");
if (hasCustomDeserialize)
{
JS::RootedValue scriptData(cx);
deserializer.ScriptVal("data", &scriptData);
if (m_Players[i]->m_UseSharedComponent)
{
if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj))
LOGERROR("AI script Deserialize call failed");
}
else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData))
{
LOGERROR("AI script deserialize() call failed");
}
}
else
{
deserializer.ScriptVal("data", &m_Players.back()->m_Obj);
}
}
// AI pathfinder
SerializeMap()(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(deserializer, "pathfinding pass classes", m_PathfindingPassClasses);
u16 mapW, mapH;
deserializer.NumberU16_Unbounded("pathfinder grid w", mapW);
deserializer.NumberU16_Unbounded("pathfinder grid h", mapH);
m_PassabilityMap = Grid(mapW, mapH);
deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData));
m_LongPathfinder.Reload(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses);
}
int getPlayerSize()
{
return m_Players.size();
}
void RegisterSerializablePrototype(std::wstring name, JS::HandleValue proto)
{
// Require unique prototype and name (for reverse lookup)
// TODO: this is yucky - see comment in Deserialize()
ENSURE(proto.isObject() && "A serializable prototype has to be an object!");
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedObject obj(cx, &proto.toObject());
if (m_SerializablePrototypes->has(obj) || m_DeserializablePrototypes.find(name) != m_DeserializablePrototypes.end())
{
LOGERROR("RegisterSerializablePrototype called with same prototype multiple times: p=%p n='%s'", (void *)obj.get(), utf8_from_wstring(name));
return;
}
m_SerializablePrototypes->add(cx, obj, name);
m_DeserializablePrototypes[name] = JS::Heap(obj);
}
private:
static void Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc)
{
for (std::pair>& prototype : m_DeserializablePrototypes)
JS_CallObjectTracer(trc, &prototype.second, "CAIWorker::m_DeserializablePrototypes");
for (std::pair>& metadata : m_PlayerMetadata)
JS_CallValueTracer(trc, &metadata.second, "CAIWorker::m_PlayerMetadata");
}
void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out)
{
if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end())
{
// Load and cache the AI player metadata
m_ScriptInterface->ReadJSONFile(path, out);
m_PlayerMetadata[path] = JS::Heap(out);
return;
}
out.set(m_PlayerMetadata[path].get());
}
void PerformComputation()
{
// Deserialize the game state, to pass to the AI's HandleMessage
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
{
PROFILE3("AI compute read state");
m_ScriptInterface->ReadStructuredClone(m_GameState, &state);
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
}
// It would be nice to do
// m_ScriptInterface->FreezeObject(state.get(), true);
// to prevent AI scripts accidentally modifying the state and
// affecting other AI scripts they share it with. But the performance
// cost is far too high, so we won't do that.
// If there is a shared component, run it
if (m_HasSharedComponent)
{
PROFILE3("AI run shared component");
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
PROFILE3("AI script");
PROFILE2_ATTR("player: %d", m_Players[i]->m_Player);
PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str());
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj);
else
m_Players[i]->Run(state, m_Players[i]->m_Player);
}
}
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptRuntime;
shared_ptr m_ScriptInterface;
boost::rand48 m_RNG;
u32 m_TurnNum;
JS::PersistentRootedValue m_EntityTemplates;
bool m_HasLoadedEntityTemplates;
JS::PersistentRootedValue m_TechTemplates;
std::map > m_PlayerMetadata;
std::vector > m_Players; // use shared_ptr just to avoid copying
bool m_HasSharedComponent;
JS::PersistentRootedValue m_SharedAIObj;
std::vector m_Commands;
std::set m_LoadedModules;
shared_ptr m_GameState;
Grid m_PassabilityMap;
JS::PersistentRootedValue m_PassabilityMapVal;
Grid m_TerritoryMap;
JS::PersistentRootedValue m_TerritoryMapVal;
std::map m_NonPathfindingPassClasses;
std::map m_PathfindingPassClasses;
LongPathfinder m_LongPathfinder;
bool m_CommandsComputed;
shared_ptr > m_SerializablePrototypes;
std::map > m_DeserializablePrototypes;
CTemplateLoader m_TemplateLoader;
};
/**
* Implementation of ICmpAIManager.
*/
class CCmpAIManager : public ICmpAIManager
{
public:
static void ClassInit(CComponentManager& UNUSED(componentManager))
{
}
DEFAULT_COMPONENT_ALLOCATOR(AIManager)
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_TerritoriesDirtyID = 0;
m_TerritoriesDirtyBlinkingID = 0;
m_JustDeserialized = false;
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize());
// Because the AI worker uses its own ScriptInterface, we can't use the
// ISerializer (which was initialised with the simulation ScriptInterface)
// directly. So we'll just grab the ISerializer's stream and write to it
// with an independent serializer.
m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
u32 numAis;
deserialize.NumberU32_Unbounded("num ais", numAis);
if (numAis > 0)
LoadUsedEntityTemplates();
m_Worker.Deserialize(deserialize.GetStream(), numAis);
m_JustDeserialized = true;
}
- virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty)
+ virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior)
{
LoadUsedEntityTemplates();
- m_Worker.AddPlayer(id, player, difficulty);
+ m_Worker.AddPlayer(id, player, difficulty, behavior);
// AI players can cheat and see through FoW/SoD, since that greatly simplifies
// their implementation.
// (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD)
CmpPtr cmpRangeManager(GetSystemEntity());
if (cmpRangeManager)
cmpRangeManager->SetLosRevealAll(player, true);
}
virtual void SetRNGSeed(u32 seed)
{
m_Worker.SetRNGSeed(seed);
}
virtual void TryLoadSharedComponent()
{
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
// load the technology templates
CmpPtr cmpDataTemplateManager(GetSystemEntity());
ENSURE(cmpDataTemplateManager);
JS::RootedValue techTemplates(cx);
cmpDataTemplateManager->GetAllTechs(&techTemplates);
m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates));
m_Worker.TryLoadSharedComponent(true);
}
virtual void RunGamestateInit()
{
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
// We flush events from the initialization so we get a clean state now.
JS::RootedValue state(cx);
cmpAIInterface->GetFullRepresentation(&state, true);
// Get the passability data
Grid dummyGrid;
const Grid* passabilityMap = &dummyGrid;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
Grid dummyGrid2;
const Grid* territoryMap = &dummyGrid2;
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID))
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
LoadPathfinderClasses(state);
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
if (cmpPathfinder)
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
virtual void StartComputation()
{
PROFILE("AI setup");
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
if (m_Worker.getPlayerSize() == 0)
return;
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
JS::RootedValue state(cx);
if (m_JustDeserialized)
cmpAIInterface->GetFullRepresentation(&state, false);
else
cmpAIInterface->GetRepresentation(&state);
LoadPathfinderClasses(state); // add the pathfinding classes to it
// Update the game state
m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state));
// Update the pathfinding data
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
{
const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation();
if (dirtinessInformations.dirty || m_JustDeserialized)
{
const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid();
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.UpdatePathfinder(passabilityMap,
dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized,
nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
cmpPathfinder->FlushAIPathfinderDirtinessInformation();
}
// Update the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized))
{
const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid();
m_Worker.UpdateTerritoryMap(territoryMap);
}
m_Worker.StartComputation();
m_JustDeserialized = false;
}
virtual void PushCommands()
{
std::vector commands;
m_Worker.GetCommands(commands);
CmpPtr cmpCommandQueue(GetSystemEntity());
if (!cmpCommandQueue)
return;
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue clonedCommandVal(cx);
for (size_t i = 0; i < commands.size(); ++i)
{
for (size_t j = 0; j < commands[i].commands.size(); ++j)
{
scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal);
cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal);
}
}
}
private:
size_t m_TerritoriesDirtyID;
size_t m_TerritoriesDirtyBlinkingID;
bool m_JustDeserialized;
/**
* Load the templates of all entities on the map (called when adding a new AI player for a new game
* or when deserializing)
*/
void LoadUsedEntityTemplates()
{
if (m_Worker.HasLoadedEntityTemplates())
return;
CmpPtr cmpTemplateManager(GetSystemEntity());
ENSURE(cmpTemplateManager);
std::vector templateNames = cmpTemplateManager->FindUsedTemplates();
std::vector > usedTemplates;
usedTemplates.reserve(templateNames.size());
for (const std::string& name : templateNames)
{
const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name);
if (node)
usedTemplates.emplace_back(name, node);
}
// Send the data to the worker
m_Worker.LoadEntityTemplates(usedTemplates);
}
void LoadPathfinderClasses(JS::HandleValue state)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue classesVal(cx);
scriptInterface.Eval("({})", &classesVal);
std::map classes;
cmpPathfinder->GetPassabilityClasses(classes);
for (std::map::iterator it = classes.begin(); it != classes.end(); ++it)
scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true);
scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true);
}
CAIWorker m_Worker;
};
REGISTER_COMPONENT_TYPE(AIManager)
Index: ps/trunk/source/simulation2/components/ICmpAIManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpAIManager.cpp (revision 20645)
+++ ps/trunk/source/simulation2/components/ICmpAIManager.cpp (revision 20646)
@@ -1,89 +1,89 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "ICmpAIManager.h"
#include "simulation2/system/InterfaceScripted.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/Filesystem.h"
BEGIN_INTERFACE_WRAPPER(AIManager)
-DEFINE_INTERFACE_METHOD_3("AddPlayer", void, ICmpAIManager, AddPlayer, std::wstring, player_id_t, uint8_t)
+DEFINE_INTERFACE_METHOD_4("AddPlayer", void, ICmpAIManager, AddPlayer, std::wstring, player_id_t, uint8_t, std::wstring)
DEFINE_INTERFACE_METHOD_1("SetRNGSeed", void, ICmpAIManager, SetRNGSeed, uint32_t)
DEFINE_INTERFACE_METHOD_0("TryLoadSharedComponent", void, ICmpAIManager, TryLoadSharedComponent)
DEFINE_INTERFACE_METHOD_0("RunGamestateInit", void, ICmpAIManager, RunGamestateInit)
END_INTERFACE_WRAPPER(AIManager)
// Implement the static method that finds all AI scripts
// that can be loaded via AddPlayer:
struct GetAIsHelper
{
NONCOPYABLE(GetAIsHelper);
public:
GetAIsHelper(const ScriptInterface& scriptInterface) :
m_ScriptInterface(scriptInterface),
m_AIs(scriptInterface.GetJSRuntime())
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
m_AIs = JS_NewArrayObject(cx, 0);
}
void Run()
{
vfs::ForEachFile(g_VFS, L"simulation/ai/", Callback, (uintptr_t)this, L"*.json", vfs::DIR_RECURSIVE);
}
static Status Callback(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
GetAIsHelper* self = (GetAIsHelper*)cbData;
JSContext* cx = self->m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
// Extract the 3rd component of the path (i.e. the directory after simulation/ai/)
fs::wpath components = pathname.string();
fs::wpath::iterator it = components.begin();
std::advance(it, 2);
std::wstring dirname = GetWstringFromWpath(*it);
JS::RootedValue ai(cx);
JS::RootedValue data(cx);
self->m_ScriptInterface.ReadJSONFile(pathname, &data);
self->m_ScriptInterface.Eval("({})", &ai);
self->m_ScriptInterface.SetProperty(ai, "id", dirname, true);
self->m_ScriptInterface.SetProperty(ai, "data", data, true);
u32 length;
JS_GetArrayLength(cx, self->m_AIs, &length);
JS_SetElement(cx, self->m_AIs, length, ai);
return INFO::OK;
}
JS::PersistentRootedObject m_AIs;
const ScriptInterface& m_ScriptInterface;
};
JS::Value ICmpAIManager::GetAIs(const ScriptInterface& scriptInterface)
{
GetAIsHelper helper(scriptInterface);
helper.Run();
return JS::ObjectValue(*helper.m_AIs);
}
Index: ps/trunk/source/simulation2/components/ICmpAIManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpAIManager.h (revision 20645)
+++ ps/trunk/source/simulation2/components/ICmpAIManager.h (revision 20646)
@@ -1,59 +1,59 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_ICMPAIMANAGER
#define INCLUDED_ICMPAIMANAGER
#include "simulation2/system/Interface.h"
#include "simulation2/helpers/Player.h"
class ICmpAIManager : public IComponent
{
public:
/**
* Add a new AI player into the world, based on the AI script identified
* by @p id (corresponding to a subdirectory in simulation/ai/),
* to control player @p player.
*/
- virtual void AddPlayer(const std::wstring& id, player_id_t player, uint8_t difficulty) = 0;
+ virtual void AddPlayer(const std::wstring& id, player_id_t player, uint8_t difficulty, const std::wstring&) = 0;
virtual void SetRNGSeed(uint32_t seed) = 0;
virtual void TryLoadSharedComponent() = 0;
virtual void RunGamestateInit() = 0;
/**
* Call this at the end of a turn, to trigger AI computation which will be
* ready for the next turn.
*/
virtual void StartComputation() = 0;
/**
* Call this at the start of a turn, to push the computed AI commands into
* the command queue.
*/
virtual void PushCommands() = 0;
/**
* Returns a vector of {"id":"value-for-AddPlayer", "name":"Human readable name"}
* objects, based on all the available AI scripts.
*/
static JS::Value GetAIs(const ScriptInterface& scriptInterface);
DECLARE_INTERFACE_TYPE(AIManager)
};
#endif // INCLUDED_ICMPAIMANAGER