Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 25612)
+++ ps/trunk/binaries/data/config/default.cfg (revision 25613)
@@ -1,560 +1,563 @@
; 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
; Allows to force GL version for SDL
forceglversion = false
forceglprofile = "compatibility" ; Possible values: compatibility, core, es
forceglmajorversion = 3
forceglminorversion = 3
; Big screenshot tiles
screenshot.tiles = 4
screenshot.tilewidth = 480
screenshot.tileheight = 270
; 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
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
shadowsfixed = false ; When enabled shadows are rendered only on the
shadowsfixeddistance = 300.0 ; fixed distance and without swimming effect.
vsync = false
particles = true
fog = true
silhouettes = true
showsky = true
; Uses a synchonized call to a GL driver to get an error state. Useful
; for a debugging of a system without GL_KHR_debug.
gl.checkerrorafterswap = false
novbo = false
; Disable hardware cursors
nohwcursor = false
; 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
; Use anti-aliasing techniques.
antialiasing = "disabled"
; Use sharpening techniques.
sharpening = "disabled"
sharpness = 0.3
; Quality used for actors.
max_actor_quality=200
+; Whether or not actor variants are selected randomly, possible values are "full", "limited", "none".
+variant_diversity = "full"
+
; 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 = 60 ; Throttle FPS in menus only.
[profiler2]
server = "127.0.0.1"
server.port = "8000" ; Use a free port on your machine.
server.threads = "6" ; Enough for the browser's parallel connection limit
[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", "Alt+F4" ; Exit to desktop
cancel = Escape ; Close or cancel the current dialog box/popup
confirm = Return ; Confirm the current command
pause = Pause, "Shift+Space" ; 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
; > DIALOG HOTKEYS
summary = "Ctrl+Tab" ; Toggle in-game summary
lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window.
structree = "Alt+Shift+T" ; Show structure tree
civinfo = "Alt+Shift+H" ; Show civilization info
; > 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
timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter
ceasefirecounter.toggle = "" ; Toggle ceasefire counter
; > 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 = "" ; Focus the camera on the rally point of the selected building
zoom.in = Plus, 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]
cancel = Esc ; Un-select all units and cancel building placement
add = Shift ; Add units to selection
militaryonly = Alt ; Add only military units to the selection
nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection
idleonly = "I" ; Select only idle units
woundedonly = "O" ; Select only wounded units
remove = Ctrl ; Remove units from selection
idlebuilder = Semicolon ; Select next idle builder
idleworker = Period, NumDecimal ; Select next idle worker
idlewarrior = Slash, NumDivide ; Select next idle warrior
idleunit = BackSlash ; Select next idle unit
offscreen = Alt ; Include offscreen units in selection
[hotkey.selection.group.add]
0 = "Shift+0", "Shift+Num0"
1 = "Shift+1", "Shift+Num1"
2 = "Shift+2", "Shift+Num2"
3 = "Shift+3", "Shift+Num3"
4 = "Shift+4", "Shift+Num4"
5 = "Shift+5", "Shift+Num5"
6 = "Shift+6", "Shift+Num6"
7 = "Shift+7", "Shift+Num7"
8 = "Shift+8", "Shift+Num8"
9 = "Shift+9", "Shift+Num9"
[hotkey.selection.group.save]
0 = "Ctrl+0", "Ctrl+Num0"
1 = "Ctrl+1", "Ctrl+Num1"
2 = "Ctrl+2", "Ctrl+Num2"
3 = "Ctrl+3", "Ctrl+Num3"
4 = "Ctrl+4", "Ctrl+Num4"
5 = "Ctrl+5", "Ctrl+Num5"
6 = "Ctrl+6", "Ctrl+Num6"
7 = "Ctrl+7", "Ctrl+Num7"
8 = "Ctrl+8", "Ctrl+Num8"
9 = "Ctrl+9", "Ctrl+Num9"
[hotkey.selection.group.select]
0 = 0, Num0
1 = 1, Num1
2 = 2, Num2
3 = 3, Num3
4 = 4, Num4
5 = 5, Num5
6 = 6, Num6
7 = 7, Num7
8 = 8, Num8
9 = 9, Num9
[hotkey.gamesetup]
mapbrowser.open = "M"
[hotkey.session]
kill = Delete, Backspace ; 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
unloadturrets = "U" ; Unload turreted units.
leaveturret = "U" ; Leave turret point.
move = "" ; 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
garrison = Ctrl ; Modifier to garrison when clicking on building
occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder.
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
pushorderfront = "" ; Modifier to push unit orders to the front 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
snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure
toggledefaultformation = "" ; Switch between null default formation and the last default formation used (defaults to "box")
; Overlays
showstatusbars = Tab ; Toggle display of status bars
devcommands.toggle = "Alt+D" ; Toggle developer commands panel
highlightguarding = PageDown ; Toggle highlight of guarding units
highlightguarded = PageUp ; Toggle highlight of guarded units
diplomacycolors = "Alt+X" ; Toggle diplomacy colors
toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures
toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures
togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units
[hotkey.session.gui]
toggle = "Alt+G" ; Toggle visibility of session GUI
menu.toggle = "F10" ; Toggle in-game menu
diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page
barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page
objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page
tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel
[hotkey.session.savedgames]
delete = Delete, Backspace ; 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 = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game
[hotkey.tab]
next = "Tab", "Alt+S" ; Show the next tab
prev = "Shift+Tab", "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, balanced, aggressive or defensive)
settingsslide = true ; Enable/Disable settings panel slide
[gui.loadingscreen]
progressdescription = false ; Whether to display the progress percent or a textual description
[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
ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner
batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey)
scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1
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
rankabovestatusbar = true ; Show rank icons above status bars
experiencestatusbar = true ; Show an experience status bar above each selected unit
respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending)
snaptoedges = "disabled" ; Possible values: disabled, enabled.
snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges
disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time.
defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already.
formationwalkonly = "true" ; Formations are disabled when giving gather/attack/... orders.
howtoshownames = 0 ; Whether the specific names are show as default, as opposed to the generic names. And whether the secondary names are shown. (0 - show both; specific names primary, 1 - show both; generic names primary, 2 - show only specific names, 3 - show only generic names)
[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
[gui.session.diplomacycolors]
self = "21 55 149" ; Color of your units when diplomacy colors are enabled
ally = "86 180 31" ; Color of allies when diplomacy colors are enabled
neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled
enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled
[joystick] ; EXPERIMENTAL: joystick/gamepad settings
enable = false
deadzone = 8192
[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 = "arena25" ; Default MUC room to join
server = "lobby.wildfiregames.com" ; Address of lobby server
tls = true ; Whether to use TLS encryption when connecting to the server.
verify_certificate = false ; Whether to reject connecting to the lobby if the TLS certificate is invalid (TODO: wait for Gloox GnuTLS trust implementation to be fixed)
terms_url = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/prelobby/common/terms/"; Allows the user to save the text and print the terms
terms_of_service = "0" ; Version (hash) of the Terms of Service that the user has accepted
terms_of_use = "0" ; Version (hash) of the Terms of Use that the user has accepted
privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted
xpartamupp = "wfgbot25" ; Name of the server-side XMPP-account that manage games
echelon = "echelon25" ; Name of the server-side XMPP-account that manages ratings
buddies = "," ; Comma separated list of playernames that the current user has marked as buddies
rememberpassword = true ; Whether to store the encrypted password in the user config
[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"
[modio]
public_key = "RWRcbM/EwV7bucTiQVCcRBhCkYkXmJEO7s4ktyufkB+gW/NxHhOZ38xh" ; Public key corresponding to the private key valid mods are signed with
disclaimer = "0" ; Version (hash) of the Disclaimer that the user has accepted
[modio.v1]
baseurl = "https://api.mod.io/v1"
api_key = "23df258a71711ea6e4b50893acc1ba55"
name_id = "0ad"
[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
observermaxlag = 10 ; Make clients wait for observers if they lag more than X turns behind. -1 means "never wait for observers".
autocatchup = true ; Auto-accelerate the sim rate if lagging behind (as an observer).
[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
[rlinterface]
address = "127.0.0.1:6000"
[sound]
mastergain = 0.9
musicgain = 0.2
ambientgain = 0.6
actiongain = 0.7
uigain = 0.7
mindistance = 1
maxdistance = 350
maxstereoangle = 0.62 ; About PI/5 radians
[sound.notify]
nick = true ; Play a sound when someone mentions your name in the lobby or game
gamesetup.join = false ; Play a sound when a new client joins the game setup
[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_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to
url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published
url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms
terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted
[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/options/options.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 25612)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 25613)
@@ -1,682 +1,693 @@
[
{
"label": "General",
"options":
[
{
"type": "string",
"label": "Player name (single-player)",
"tooltip": "How you want to be addressed in single-player matches.",
"config": "playername.singleplayer"
},
{
"type": "string",
"label": "Player name (multiplayer)",
"tooltip": "How you want to be addressed in multiplayer matches (except lobby).",
"config": "playername.multiplayer"
},
{
"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": "FPS overlay",
"tooltip": "Show frames per second in top right corner.",
"config": "overlay.fps"
},
{
"type": "boolean",
"label": "Real time overlay",
"tooltip": "Show current system time in top right corner.",
"config": "overlay.realtime"
},
{
"type": "boolean",
"label": "Game time 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": "boolean",
"label": "Chat timestamp",
"tooltip": "Display the time at which a chat message was posted.",
"config": "chat.timestamp"
},
{
"type": "dropdown",
"label": "Naming of entities.",
"tooltip": "How to show entity names.",
"config": "gui.session.howtoshownames",
"list": [
{
"value": 0,
"label": "Specific primary.",
"tooltip": "Show specific names as primary and generic names as secondary."
},
{
"value": 1,
"label": "Generic primary.",
"tooltip": "Show generic names as primary and specific names as secondary."
},
{
"value": 2,
"label": "Only Specific.",
"tooltip": "Show only specific names for units."
},
{
"value": 3,
"label": "Only Generic.",
"tooltip": "Show only generic names for units."
}
]
}
]
},
{
"label": "Graphics (general)",
"tooltip": "Set the balance between performance and visual appearance.",
"options":
[
{
"type": "boolean",
"label": "Windowed mode",
"tooltip": "Start 0 A.D. in a window.",
"config": "windowed"
},
{
"type": "boolean",
"label": "Fog",
"tooltip": "Enable fog.",
"config": "fog"
},
{
"type": "boolean",
"label": "Post-processing",
"tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).",
"config": "postproc"
},
{
"type": "boolean",
"label": "Shadows",
"tooltip": "Enable shadows.",
"config": "shadows"
},
{
"type": "boolean",
"label": "Unit silhouettes",
"tooltip": "Show outlines of units behind structures.",
"config": "silhouettes"
},
{
"type": "boolean",
"label": "Particles",
"tooltip": "Enable particles.",
"config": "particles"
},
{
"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": "Graphics (advanced)",
"tooltip": "More specific rendering settings.",
"options":
[
{
"type": "boolean",
"label": "Prefer GLSL",
"tooltip": "Use OpenGL 2.0 shaders (recommended).",
"config": "preferglsl"
},
{
"type": "boolean",
"label": "Fog",
"tooltip": "Enable fog.",
"dependencies": ["preferglsl"],
"config": "fog"
},
{
"type": "boolean",
"label": "Post-processing",
"tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).",
"config": "postproc"
},
{
"type": "dropdown",
"label": "Antialiasing",
"tooltip": "Reduce aliasing effect on edges.",
"dependencies": ["postproc", "preferglsl"],
"config": "antialiasing",
"list": [
{ "value": "disabled", "label": "Disabled", "tooltip": "Do not use antialiasing." },
{ "value": "fxaa", "label": "FXAA", "tooltip": "Fast, but simple antialiasing." },
{ "value": "msaa2", "label": "MSAA (2×)", "tooltip": "Slow, but high-quality antialiasing, uses two samples per pixel. Supported for GL3.3+." },
{ "value": "msaa4", "label": "MSAA (4×)", "tooltip": "Slow, but high-quality antialiasing, uses four samples per pixel. Supported for GL3.3+." },
{ "value": "msaa8", "label": "MSAA (8×)", "tooltip": "Slow, but high-quality antialiasing, uses eight samples per pixel. Supported for GL3.3+." },
{ "value": "msaa16", "label": "MSAA (16×)", "tooltip": "Slow, but high-quality antialiasing, uses sixteen samples per pixel. Supported for GL3.3+." }
]
},
{
"type": "dropdown",
"label": "Sharpening",
"tooltip": "Reduce blurry effects.",
"dependencies": ["postproc", "preferglsl"],
"config": "sharpening",
"list": [
{ "value": "disabled", "label": "Disabled", "tooltip": "Do not use sharpening." },
{ "value": "cas", "label": "FidelityFX CAS", "tooltip": "Contrast adaptive sharpening, a fast, contrast based sharpening pass." }
]
},
{
"type": "slider",
"label": "Sharpness factor",
"tooltip": "The sharpness of the choosen pass.",
"dependencies": ["postproc", "preferglsl"],
"config": "sharpness",
"min": 0,
"max": 1
},
{
"type": "dropdown",
"label": "Model quality",
"tooltip": "Model quality setting.",
"config": "max_actor_quality",
"list": [
{ "value": 100, "label": "Low", "tooltip": "Simpler models for better performance." },
{ "value": 150, "label": "Medium", "tooltip": "Average quality and average performance." },
{ "value": 200, "label": "High", "tooltip": "High quality models." }
]
},
{
+ "type": "dropdown",
+ "label": "Model appearance randomization",
+ "tooltip": "Randomize the appearance of entities. Disabling gives a small performance improvement.",
+ "config": "variant_diversity",
+ "list": [
+ { "value": "none", "label": "None", "tooltip": "Entities will all look the same." },
+ { "value": "limited", "label": "Limited", "tooltip": "Entities will be less diverse." },
+ { "value": "full", "label": "Normal", "tooltip": "Entities appearance is randomized normally." }
+ ]
+ },
+ {
"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"
},
{
"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",
"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"
},
{
"type": "boolean",
"label": "Water effects",
"tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.",
"config": "watereffects"
},
{
"type": "boolean",
"label": "High-quality water effects",
"tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.",
"dependencies": ["watereffects"],
"config": "waterfancyeffects"
},
{
"type": "boolean",
"label": "Water reflections",
"tooltip": "Allow water to reflect a mirror image.",
"dependencies": ["watereffects"],
"config": "waterreflection"
},
{
"type": "boolean",
"label": "Water refraction",
"tooltip": "Use a real water refraction map and not transparency.",
"dependencies": ["watereffects"],
"config": "waterrefraction"
},
{
"type": "boolean",
"label": "Real water depth",
"tooltip": "Use actual water depth in rendering calculations.",
"dependencies": ["watereffects", "waterrefraction"],
"config": "waterrealdepth"
}
]
},
{
"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"
},
{
"type": "boolean",
"label": "New player notification in game setup",
"tooltip": "Receive audio notification when a new client joins the game setup.",
"config": "sound.notify.gamesetup.join"
}
]
},
{
"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": "Enable settings panel slide",
"tooltip": "Slide the settings panel when opening, closing or resizing.",
"config": "gui.gamesetup.settingsslide"
},
{
"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",
"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": "balanced", "label": "Balanced" },
{ "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",
"tooltip": "Players joining the match will be assigned if there is a free slot."
},
{
"value": "buddies",
"label": "Buddies",
"tooltip": "Players joining the match will only be assigned if they are a buddy of the host and if there is a free slot."
},
{
"value": "disabled",
"label": "Disabled",
"tooltip": "Players only receive a slot when the host assigns them explicitly."
}
]
}
]
},
{
"label": "Networking / Lobby",
"tooltip": "These settings only affect the multiplayer.",
"options":
[
{
"type": "boolean",
"label": "TLS encryption",
"tooltip": "Protect login and data exchanged with the lobby server using TLS encryption.",
"config": "lobby.tls"
},
{
"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"
},
{
"type": "boolean",
"label": "Network warnings",
"tooltip": "Show which player has a bad connection in multiplayer games.",
"config": "overlay.netwarnings"
},
{
"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": "Max lag for observers",
"tooltip": "When hosting, pause the game if observers are lagging more than this many turns. If set to -1, observers are ignored.",
"config": "network.observermaxlag",
"min": -1,
"max": 10000
},
{
"type": "boolean",
"label": "(Observer) Speed up when lagging.",
"tooltip": "When observing a game, automatically speed up if you start lagging, to catch up with the live match.",
"config": "network.autocatchup"
}
]
},
{
"label": "Game Session",
"tooltip": "Change options regarding the in-game settings.",
"options":
[
{
"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": "number",
"label": "Batch training size",
"tooltip": "Number of units trained per batch by default.",
"config": "gui.session.batchtrainingsize",
"min": 1,
"max": 20
},
{
"type": "slider",
"label": "Scroll batch increment ratio",
"tooltip": "Number of times you have to scroll to increase/decrease the batchsize by 1.",
"config": "gui.session.scrollbatchratio",
"min": 0.1,
"max": 30
},
{
"type": "boolean",
"label": "Chat notification attack",
"tooltip": "Show a chat notification if you are attacked by another player.",
"config": "gui.session.notifications.attack"
},
{
"type": "boolean",
"label": "Chat notification 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": "Chat notification barter",
"tooltip": "Show a chat notification to observers when a player bartered resources.",
"config": "gui.session.notifications.barter"
},
{
"type": "dropdown",
"label": "Chat notification 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" }
]
},
{
"type": "boolean",
"label": "Attack range visualization",
"tooltip": "Display the attack range of selected defensive structures. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.attackrange"
},
{
"type": "boolean",
"label": "Aura range visualization",
"tooltip": "Display the range of auras of selected units and structures. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.aurasrange"
},
{
"type": "boolean",
"label": "Heal range visualization",
"tooltip": "Display the healing range of selected units. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.healrange"
},
{
"type": "boolean",
"label": "Rank icon above status bar",
"tooltip": "Show rank icons above status bars.",
"config": "gui.session.rankabovestatusbar"
},
{
"type": "boolean",
"label": "Experience status bar",
"tooltip": "Show an experience status bar above each selected unit.",
"config": "gui.session.experiencestatusbar"
},
{
"type": "boolean",
"label": "Detailed tooltips",
"tooltip": "Show detailed tooltips for trainable units in unit-producing structures.",
"config": "showdetailedtooltips"
},
{
"type": "dropdown",
"label": "Sort resources and population tooltip",
"tooltip": "Dynamically sort players in the resources and population tooltip by value.",
"config": "gui.session.respoptooltipsort",
"list": [
{ "value": 0, "label": "Unordered" },
{ "value": -1, "label": "Ascending" },
{ "value": 1, "label": "Descending" }
]
},
{
"type": "color",
"label": "Diplomacy colors: self",
"tooltip": "Color of your units when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.self"
},
{
"type": "color",
"label": "Diplomacy colors: ally",
"tooltip": "Color of allies when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.ally"
},
{
"type": "color",
"label": "Diplomacy colors: neutral",
"tooltip": "Color of neutral players when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.neutral"
},
{
"type": "color",
"label": "Diplomacy colors: enemy",
"tooltip": "Color of enemies when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.enemy"
},
{
"type": "dropdown",
"label": "Snap to edges",
"tooltip": "This option allows to align new structures with nearby structures.",
"config": "gui.session.snaptoedges",
"list": [
{
"value": "disabled",
"label": "Hotkey to enable snapping",
"tooltip": "New structures are aligned with nearby structures while pressing the hotkey."
},
{
"value": "enabled",
"label": "Hotkey to disable snapping",
"tooltip": "New structures are aligned with nearby structures unless the hotkey is pressed."
}
]
},
{
"type": "dropdown",
"label": "Control group membership",
"tooltip": "Decide whether units can be part of multiple control groups.",
"config": "gui.session.disjointcontrolgroups",
"list": [
{
"value": "true",
"label": "Single",
"tooltip": "When adding a Unit or Structure to a control group, they are removed from other control groups. Use this choice if you want control groups to refer to distinct armies."
},
{
"value": "false",
"label": "Multiple",
"tooltip": "Units and Structures can be part of multiple control groups. This is useful to keep control groups for distinct armies and a control group for the entire army simultaneously."
}
]
},
{
"type": "dropdown",
"label": "Formation control",
"tooltip": "Decide whether formations are enabled for all orders or only 'Walk' and 'Patrol'.",
"config": "gui.session.formationwalkonly",
"list": [
{
"value": "true",
"label": "Walk/Patrol Only",
"tooltip": "Other orders will disband existing formations."
},
{
"value": "false",
"label": "No Override",
"tooltip": "Units in formations stay in formations."
}
]
}
]
}
]
Index: ps/trunk/source/graphics/ObjectBase.cpp
===================================================================
--- ps/trunk/source/graphics/ObjectBase.cpp (revision 25612)
+++ ps/trunk/source/graphics/ObjectBase.cpp (revision 25613)
@@ -1,997 +1,1009 @@
/* Copyright (C) 2021 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
#include
#include "ObjectBase.h"
#include "ObjectManager.h"
#include "ps/XML/Xeromyces.h"
#include "ps/Filesystem.h"
#include "ps/CLogger.h"
#include "lib/timer.h"
#include "maths/MathUtil.h"
#include
namespace
{
/**
* The maximal quality for an actor.
*/
static constexpr int MAX_QUALITY = 255;
/**
* How many quality levels a given actor can have.
*/
static constexpr int MAX_LEVELS_PER_ACTOR_DEF = 5;
int GetQuality(const CStr& value)
{
if (value == "low")
return 100;
else if (value == "medium")
return 150;
else if (value == "high")
return 200;
else
return value.ToInt();
}
} // anonymous namespace
CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8 qualityLevel)
: m_ObjectManager(objectManager), m_ActorDef(actorDef)
{
m_QualityLevel = qualityLevel;
m_Properties.m_CastShadows = false;
m_Properties.m_FloatOnWater = false;
// Remove leading art/actors/ & include quality level.
m_Identifier = m_ActorDef.m_Pathname.string8().substr(11) + CStr::FromInt(m_QualityLevel);
}
std::unique_ptr CObjectBase::CopyWithQuality(u8 newQualityLevel) const
{
std::unique_ptr ret = std::make_unique(m_ObjectManager, m_ActorDef, newQualityLevel);
// No need to actually change any quality-related stuff here, we assume that this is a copy for props.
ret->m_VariantGroups = m_VariantGroups;
ret->m_Material = m_Material;
ret->m_Properties = m_Properties;
return ret;
}
bool CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root)
{
// Define all the elements used in the XML file
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
EL(castshadow);
EL(float);
EL(group);
EL(material);
AT(maxquality);
AT(minquality);
#undef AT
#undef EL
// Set up the group vector to avoid reallocation and copying later.
{
int groups = 0;
XERO_ITER_EL(root, child)
{
if (child.GetNodeName() == el_group)
++groups;
}
m_VariantGroups.reserve(groups);
}
// (This XML-reading code is rather worryingly verbose...)
auto shouldSkip = [&](XMBElement& node) {
XERO_ITER_ATTR(node, attr)
{
if (attr.Name == at_minquality && GetQuality(attr.Value) > m_QualityLevel)
return true;
else if (attr.Name == at_maxquality && GetQuality(attr.Value) <= m_QualityLevel)
return true;
}
return false;
};
XERO_ITER_EL(root, child)
{
int child_name = child.GetNodeName();
if (shouldSkip(child))
continue;
if (child_name == el_group)
{
std::vector& currentGroup = m_VariantGroups.emplace_back();
currentGroup.reserve(child.GetChildNodes().size());
XERO_ITER_EL(child, variant)
{
if (shouldSkip(variant))
continue;
if (!LoadVariant(XeroFile, variant, currentGroup.emplace_back()))
return false;
}
if (currentGroup.size() == 0)
{
LOGERROR("Actor group has zero variants ('%s')", m_Identifier);
return false;
}
}
else if (child_name == el_castshadow)
m_Properties.m_CastShadows = true;
else if (child_name == el_float)
m_Properties.m_FloatOnWater = true;
else if (child_name == el_material)
m_Material = VfsPath("art/materials") / child.GetText().FromUTF8();
}
if (m_Material.empty())
m_Material = VfsPath("art/materials/default.xml");
return true;
}
bool CObjectBase::LoadVariant(const CXeromyces& XeroFile, const XMBElement& variant, Variant& currentVariant)
{
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
EL(animation);
EL(animations);
EL(color);
EL(decal);
EL(mesh);
EL(particles);
EL(prop);
EL(props);
EL(texture);
EL(textures);
EL(variant);
AT(actor);
AT(angle);
AT(attachpoint);
AT(depth);
AT(event);
AT(file);
AT(frequency);
AT(id);
AT(load);
AT(maxheight);
AT(minheight);
AT(name);
AT(offsetx);
AT(offsetz);
AT(selectable);
AT(sound);
AT(speed);
AT(width);
#undef AT
#undef EL
if (variant.GetNodeName() != el_variant)
{
LOGERROR("Invalid variant format (unrecognised root element '%s')", XeroFile.GetElementString(variant.GetNodeName()));
return false;
}
// Load variants first, so that they can be overriden if necessary.
XERO_ITER_ATTR(variant, attr)
{
if (attr.Name == at_file)
{
// Open up an external file to load.
// Don't crash hard when failures happen, but log them and continue
m_ActorDef.m_UsedFiles.insert(attr.Value);
CXeromyces XeroVariant;
if (XeroVariant.Load(g_VFS, "art/variants/" + attr.Value) == PSRETURN_OK)
{
XMBElement variantRoot = XeroVariant.GetRoot();
if (!LoadVariant(XeroVariant, variantRoot, currentVariant))
return false;
}
else
{
LOGERROR("Could not open path %s", attr.Value);
return false;
}
// Continue loading extra definitions in this variant to allow nested files
}
}
XERO_ITER_ATTR(variant, attr)
{
if (attr.Name == at_name)
currentVariant.m_VariantName = attr.Value.LowerCase();
else if (attr.Name == at_frequency)
currentVariant.m_Frequency = attr.Value.ToInt();
}
XERO_ITER_EL(variant, option)
{
int option_name = option.GetNodeName();
if (option_name == el_mesh)
{
currentVariant.m_ModelFilename = VfsPath("art/meshes") / option.GetText().FromUTF8();
}
else if (option_name == el_textures)
{
XERO_ITER_EL(option, textures_element)
{
if (textures_element.GetNodeName() != el_texture)
{
LOGERROR(" can only contain elements.");
return false;
}
Samp samp;
XERO_ITER_ATTR(textures_element, se)
{
if (se.Name == at_file)
samp.m_SamplerFile = VfsPath("art/textures/skins") / se.Value.FromUTF8();
else if (se.Name == at_name)
samp.m_SamplerName = CStrIntern(se.Value);
}
currentVariant.m_Samplers.push_back(samp);
}
}
else if (option_name == el_decal)
{
XMBAttributeList attrs = option.GetAttributes();
Decal decal;
decal.m_SizeX = attrs.GetNamedItem(at_width).ToFloat();
decal.m_SizeZ = attrs.GetNamedItem(at_depth).ToFloat();
decal.m_Angle = DEGTORAD(attrs.GetNamedItem(at_angle).ToFloat());
decal.m_OffsetX = attrs.GetNamedItem(at_offsetx).ToFloat();
decal.m_OffsetZ = attrs.GetNamedItem(at_offsetz).ToFloat();
currentVariant.m_Decal = decal;
}
else if (option_name == el_particles)
{
XMBAttributeList attrs = option.GetAttributes();
VfsPath file = VfsPath("art/particles") / attrs.GetNamedItem(at_file).FromUTF8();
currentVariant.m_Particles = file;
// For particle hotloading, it's easiest to reload the entire actor,
// so remember the relevant particle file as a dependency for this actor
m_ActorDef.m_UsedFiles.insert(file);
}
else if (option_name == el_color)
{
currentVariant.m_Color = option.GetText();
}
else if (option_name == el_animations)
{
XERO_ITER_EL(option, anim_element)
{
if (anim_element.GetNodeName() != el_animation)
{
LOGERROR(" can only contain elements.");
return false;
}
Anim anim;
XERO_ITER_ATTR(anim_element, ae)
{
if (ae.Name == at_name)
anim.m_AnimName = ae.Value;
else if (ae.Name == at_id)
anim.m_ID = ae.Value;
else if (ae.Name == at_frequency)
anim.m_Frequency = ae.Value.ToInt();
else if (ae.Name == at_file)
anim.m_FileName = VfsPath("art/animation") / ae.Value.FromUTF8();
else if (ae.Name == at_speed)
anim.m_Speed = ae.Value.ToInt() > 0 ? ae.Value.ToInt() / 100.f : 1.f;
else if (ae.Name == at_event)
anim.m_ActionPos = Clamp(ae.Value.ToFloat(), 0.f, 1.f);
else if (ae.Name == at_load)
anim.m_ActionPos2 = Clamp(ae.Value.ToFloat(), 0.f, 1.f);
else if (ae.Name == at_sound)
anim.m_SoundPos = Clamp(ae.Value.ToFloat(), 0.f, 1.f);
}
currentVariant.m_Anims.push_back(anim);
}
}
else if (option_name == el_props)
{
XERO_ITER_EL(option, prop_element)
{
ENSURE(prop_element.GetNodeName() == el_prop);
Prop prop;
XERO_ITER_ATTR(prop_element, pe)
{
if (pe.Name == at_attachpoint)
prop.m_PropPointName = pe.Value;
else if (pe.Name == at_actor)
prop.m_ModelName = pe.Value.FromUTF8();
else if (pe.Name == at_minheight)
prop.m_minHeight = pe.Value.ToFloat();
else if (pe.Name == at_maxheight)
prop.m_maxHeight = pe.Value.ToFloat();
else if (pe.Name == at_selectable)
prop.m_selectable = pe.Value != "false";
}
currentVariant.m_Props.push_back(prop);
}
}
}
return true;
}
std::vector CObjectBase::CalculateVariationKey(const std::vector*>& selections) const
{
// (TODO: see CObjectManager::FindObjectVariation for an opportunity to
// call this function a bit less frequently)
// Calculate a complete list of choices, one per group, based on the
// supposedly-complete selections (i.e. not making random choices at this
// stage).
// In each group, if one of the variants has a name matching a string in the
// first 'selections', set use that one.
// Otherwise, try with the next (lower priority) selections set, and repeat.
// Otherwise, choose the first variant (arbitrarily).
std::vector choices;
std::multimap chosenProps;
for (std::vector >::const_iterator grp = m_VariantGroups.begin();
grp != m_VariantGroups.end();
++grp)
{
// Ignore groups with nothing inside. (A warning will have been
// emitted by the loading code.)
if (grp->size() == 0)
continue;
int match = -1; // -1 => none found yet
// If there's only a single variant, choose that one
if (grp->size() == 1)
{
match = 0;
}
else
{
// Determine the first variant that matches the provided strings,
// starting with the highest priority selections set:
for (const std::set* selset : selections)
{
ENSURE(grp->size() < 256); // else they won't fit in 'choices'
for (size_t i = 0; i < grp->size(); ++i)
{
if (selset->count((*grp)[i].m_VariantName))
{
match = (u8)i;
break;
}
}
// Stop after finding the first match
if (match != -1)
break;
}
// If no match, just choose the first
if (match == -1)
match = 0;
}
choices.push_back(match);
// Remember which props were chosen, so we can call CalculateVariationKey on them
// at the end.
// Erase all existing props which are overridden by this variant:
const Variant& var((*grp)[match]);
for (const Prop& prop : var.m_Props)
chosenProps.erase(prop.m_PropPointName);
// and then insert the new ones:
for (const Prop& prop : var.m_Props)
if (!prop.m_ModelName.empty())
chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName));
}
// Load each prop, and add their CalculateVariationKey to our key:
for (std::multimap::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
{
if (auto [success, prop] = m_ObjectManager.FindActorDef(it->second); success)
{
std::vector propChoices = prop.GetBase(m_QualityLevel)->CalculateVariationKey(selections);
choices.insert(choices.end(), propChoices.begin(), propChoices.end());
}
}
return choices;
}
const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector& variationKey) const
{
Variation variation;
// variationKey should correspond with m_Variants, giving the id of the
// chosen variant from each group. (Except variationKey has some bits stuck
// on the end for props, but we don't care about those in here.)
std::vector >::const_iterator grp = m_VariantGroups.begin();
std::vector::const_iterator match = variationKey.begin();
for ( ;
grp != m_VariantGroups.end() && match != variationKey.end();
++grp, ++match)
{
// Ignore groups with nothing inside. (A warning will have been
// emitted by the loading code.)
if (grp->size() == 0)
continue;
size_t id = *match;
if (id >= grp->size())
{
// This should be impossible
debug_warn(L"BuildVariation: invalid variant id");
continue;
}
// Get the matched variant
const CObjectBase::Variant& var ((*grp)[id]);
// Apply its data:
if (! var.m_ModelFilename.empty())
variation.model = var.m_ModelFilename;
if (var.m_Decal.m_SizeX && var.m_Decal.m_SizeZ)
variation.decal = var.m_Decal;
if (! var.m_Particles.empty())
variation.particles = var.m_Particles;
if (! var.m_Color.empty())
variation.color = var.m_Color;
// If one variant defines one prop attached to e.g. "root", and this
// variant defines two different props with the same attachpoint, the one
// original should be erased, and replaced by the two new ones.
//
// So, erase all existing props which are overridden by this variant:
for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
variation.props.erase(it->m_PropPointName);
// and then insert the new ones:
for (std::vector::const_iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
if (! it->m_ModelName.empty()) // if the name is empty then the overridden prop is just deleted
variation.props.insert(make_pair(it->m_PropPointName, *it));
// Same idea applies for animations.
// So, erase all existing animations which are overridden by this variant:
for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
variation.anims.erase(it->m_AnimName);
// and then insert the new ones:
for (std::vector::const_iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
variation.anims.insert(make_pair(it->m_AnimName, *it));
// Same for samplers, though perhaps not strictly necessary:
for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
variation.samplers.erase(it->m_SamplerName.string());
for (std::vector::const_iterator it = var.m_Samplers.begin(); it != var.m_Samplers.end(); ++it)
variation.samplers.insert(make_pair(it->m_SamplerName.string(), *it));
}
return variation;
}
std::set CObjectBase::CalculateRandomRemainingSelections(uint32_t seed, const std::vector>& initialSelections) const
{
rng_t rng;
rng.seed(seed);
std::set remainingSelections = CalculateRandomRemainingSelections(rng, initialSelections);
for (const std::set& sel : initialSelections)
remainingSelections.insert(sel.begin(), sel.end());
return remainingSelections; // now actually a complete set of selections
}
std::set CObjectBase::CalculateRandomRemainingSelections(rng_t& rng, const std::vector>& initialSelections) const
{
std::set remainingSelections;
std::multimap chosenProps;
// Calculate a complete list of selections, so there is at least one
// (and in most cases only one) per group.
// In each group, if one of the variants has a name matching a string in
// 'selections', use that one.
// If more than one matches, choose randomly from those matching ones.
// If none match, choose randomly from all variants.
//
// When choosing randomly, make use of each variant's frequency. If all
// variants have frequency 0, treat them as if they were 1.
+ CObjectManager::VariantDiversity diversity = m_ObjectManager.GetVariantDiversity();
+
for (std::vector >::const_iterator grp = m_VariantGroups.begin();
grp != m_VariantGroups.end();
++grp)
{
// Ignore groups with nothing inside. (A warning will have been
// emitted by the loading code.)
if (grp->size() == 0)
continue;
int match = -1; // -1 => none found yet
// If there's only a single variant, choose that one
if (grp->size() == 1)
{
match = 0;
}
else
{
// See if a variant (or several, but we only care about the first)
// is already matched by the selections we've made, keeping their
// priority order into account
for (size_t s = 0; s < initialSelections.size(); ++s)
{
for (size_t i = 0; i < grp->size(); ++i)
{
if (initialSelections[s].count((*grp)[i].m_VariantName))
{
match = (int)i;
break;
}
}
if (match >= 0)
break;
}
// If there was one, we don't need to do anything now because there's
// already something to choose. Otherwise, choose randomly from the others.
if (match == -1)
{
// Sum the frequencies
int totalFreq = 0;
for (size_t i = 0; i < grp->size(); ++i)
totalFreq += (*grp)[i].m_Frequency;
// Someone might be silly and set all variants to have freq==0, in
// which case we just pretend they're all 1
bool allZero = (totalFreq == 0);
- if (allZero) totalFreq = (int)grp->size();
-
- // Choose a random number in the interval [0..totalFreq)
- int randNum = boost::random::uniform_int_distribution(0, totalFreq-1)(rng);
+ if (allZero)
+ totalFreq = (int)grp->size();
- // and use that to choose one of the variants
+ // Choose a random number in the interval [0..totalFreq) to choose one of the variants.
+ // If the diversity is "none", force 0 to return the first valid variant.
+ int randNum = diversity == CObjectManager::VariantDiversity::NONE ? 0 : boost::random::uniform_int_distribution(0, totalFreq-1)(rng);
for (size_t i = 0; i < grp->size(); ++i)
{
randNum -= (allZero ? 1 : (*grp)[i].m_Frequency);
if (randNum < 0)
{
- remainingSelections.insert((*grp)[i].m_VariantName);
// (If this change to 'remainingSelections' interferes with earlier choices, then
// we'll get some non-fatal inconsistencies that just break the randomness. But that
// shouldn't happen, much.)
// (As an example, suppose you have a group with variants "a" and "b", and another
// with variants "a" and "c"; now if random selection choses "b" for the first
// and "a" for the second, then the selection of "a" from the second group will
// cause "a" to be used in the first instead of the "b").
match = (int)i;
+
+ // In limited diversity, somewhat-randomly continue. This cuts variants to about a third,
+ // though not quite because we must pick a variant so the actual probability is more complex.
+ // (It's also dependent on actor files not containing too many 0-frequency variants)
+ if (diversity == CObjectManager::VariantDiversity::LIMITED && (i % 3 != 0))
+ {
+ // Reset to 0 or we'll just pick every subsequent variant.
+ randNum = 0;
+ continue;
+ }
break;
}
}
- ENSURE(randNum < 0);
+ ENSURE(match != -1);
// This should always succeed; otherwise it
// wouldn't have chosen any of the variants.
+ remainingSelections.insert((*grp)[match].m_VariantName);
}
}
// Remember which props were chosen, so we can call CalculateRandomVariation on them
// at the end.
const Variant& var ((*grp)[match]);
// Erase all existing props which are overridden by this variant:
for (const Prop& prop : var.m_Props)
chosenProps.erase(prop.m_PropPointName);
// and then insert the new ones:
for (const Prop& prop : var.m_Props)
if (!prop.m_ModelName.empty())
chosenProps.insert(make_pair(prop.m_PropPointName, prop.m_ModelName));
}
// Load each prop, and add their required selections to ours:
for (std::multimap::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
{
if (auto [success, prop] = m_ObjectManager.FindActorDef(it->second); success)
{
std::vector > propInitialSelections = initialSelections;
if (!remainingSelections.empty())
propInitialSelections.push_back(remainingSelections);
std::set propRemainingSelections = prop.GetBase(m_QualityLevel)->CalculateRandomRemainingSelections(rng, propInitialSelections);
remainingSelections.insert(propRemainingSelections.begin(), propRemainingSelections.end());
// Add the prop's used files to our own (recursively) so we can hotload
// when any prop is changed
m_ActorDef.m_UsedFiles.insert(prop.m_UsedFiles.begin(), prop.m_UsedFiles.end());
}
}
return remainingSelections;
}
std::vector > CObjectBase::GetVariantGroups() const
{
std::vector > groups;
// Queue of objects (main actor plus props (recursively)) to be processed
std::queue objectsQueue;
objectsQueue.push(this);
// Set of objects already processed, so we don't do them more than once
std::set objectsProcessed;
while (!objectsQueue.empty())
{
const CObjectBase* obj = objectsQueue.front();
objectsQueue.pop();
// Ignore repeated objects (likely to be props)
if (objectsProcessed.find(obj) != objectsProcessed.end())
continue;
objectsProcessed.insert(obj);
// Iterate through the list of groups
for (size_t i = 0; i < obj->m_VariantGroups.size(); ++i)
{
// Copy the group's variant names into a new vector
std::vector group;
group.reserve(obj->m_VariantGroups[i].size());
for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j)
group.push_back(obj->m_VariantGroups[i][j].m_VariantName);
// If this group is identical to one elsewhere, don't bother listing
// it twice.
// Linear search is theoretically not very efficient, but hopefully
// we don't have enough props for that to matter...
bool dupe = false;
for (size_t j = 0; j < groups.size(); ++j)
{
if (groups[j] == group)
{
dupe = true;
break;
}
}
if (dupe)
continue;
// Add non-trivial groups (i.e. not just one entry) to the returned list
if (obj->m_VariantGroups[i].size() > 1)
groups.push_back(group);
// Add all props onto the queue to be considered
for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j)
{
const std::vector& props = obj->m_VariantGroups[i][j].m_Props;
for (size_t k = 0; k < props.size(); ++k)
if (!props[k].m_ModelName.empty())
if (auto [success, prop] = m_ObjectManager.FindActorDef(props[k].m_ModelName.c_str()); success)
objectsQueue.push(prop.GetBase(m_QualityLevel).get());
}
}
}
return groups;
}
void CObjectBase::GetQualitySplits(std::vector& splits) const
{
std::vector::iterator it = std::find_if(splits.begin(), splits.end(), [this](u8 qualityLevel) { return qualityLevel >= m_QualityLevel; });
if (it == splits.end() || *it != m_QualityLevel)
splits.emplace(it, m_QualityLevel);
for (const std::vector& group : m_VariantGroups)
for (const Variant& variant : group)
for (const Prop& prop : variant.m_Props)
{
// TODO: we probably should clean those up after XML load.
if (prop.m_ModelName.empty())
continue;
auto [success, propActor] = m_ObjectManager.FindActorDef(prop.m_ModelName.c_str());
if (!success)
continue;
std::vector newSplits = propActor.QualityLevels();
if (newSplits.size() <= 1)
continue;
// This is not entirely optimal since we might loop though redundant quality levels, but that shouldn't matter.
// Custom implementation because this is inplace, std::set_union needs a 3rd vector.
std::vector::iterator v1 = splits.begin();
std::vector::iterator v2 = newSplits.begin();
while (v2 != newSplits.end())
{
if (v1 == splits.end() || *v1 > *v2)
{
v1 = ++splits.insert(v1, *v2);
++v2;
}
else if (*v1 == *v2)
{
++v1;
++v2;
}
else
++v1;
}
}
}
const CStr& CObjectBase::GetIdentifier() const
{
return m_Identifier;
}
bool CObjectBase::UsesFile(const VfsPath& pathname) const
{
return m_ActorDef.UsesFile(pathname);
}
CActorDef::CActorDef(CObjectManager& objectManager) : m_ObjectManager(objectManager)
{
}
std::vector CActorDef::QualityLevels() const
{
std::vector splits;
splits.reserve(m_ObjectBases.size());
for (const std::shared_ptr& base : m_ObjectBases)
splits.emplace_back(base->m_QualityLevel);
return splits;
}
const std::shared_ptr& CActorDef::GetBase(u8 QualityLevel) const
{
for (const std::shared_ptr& base : m_ObjectBases)
if (base->m_QualityLevel >= QualityLevel)
return base;
// This code path ought to be impossible to take,
// because by construction we must have at least one valid CObjectBase of quality MAX_QUALITY
// (which necessarily fits the u8 comparison above).
// However compilers will warn that we return a reference to a local temporary if I return nullptr,
// so just return something sane instead.
ENSURE(false);
return m_ObjectBases.back();
}
bool CActorDef::Load(const VfsPath& pathname)
{
m_UsedFiles.clear();
m_UsedFiles.insert(pathname);
m_ObjectBases.clear();
CXeromyces XeroFile;
if (XeroFile.Load(g_VFS, pathname, "actor") != PSRETURN_OK)
return false;
// Define all the elements used in the XML file
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
EL(actor);
EL(inline);
EL(qualitylevels);
AT(file);
AT(inline);
AT(quality);
AT(version);
#undef AT
#undef EL
XMBElement root = XeroFile.GetRoot();
if (root.GetNodeName() != el_actor && root.GetNodeName() != el_qualitylevels)
{
LOGERROR("Invalid actor format (actor '%s', unrecognised root element '%s')",
pathname.string8().c_str(), XeroFile.GetElementString(root.GetNodeName()));
return false;
}
m_Pathname = pathname;
if (root.GetNodeName() == el_actor)
{
std::unique_ptr base = std::make_unique(m_ObjectManager, *this, MAX_QUALITY);
if (!base->Load(XeroFile, root))
{
LOGERROR("Invalid actor (actor '%s')", pathname.string8());
return false;
}
m_ObjectBases.emplace_back(std::move(base));
}
else
{
XERO_ITER_ATTR(root, attr)
{
if (attr.Name == at_version && attr.Value.ToInt() != 1)
{
LOGERROR("Invalid actor format (actor '%s', version %i is not supported)",
pathname.string8().c_str(), attr.Value.ToInt());
return false;
}
}
u8 quality = 0;
XMBElement inlineActor;
XERO_ITER_EL(root, child)
{
if (child.GetNodeName() == el_inline)
inlineActor = child;
}
XERO_ITER_EL(root, actor)
{
if (actor.GetNodeName() != el_actor)
continue;
bool found_quality = false;
bool use_inline = false;
CStr file;
XERO_ITER_ATTR(actor, attr)
{
if (attr.Name == at_quality)
{
int v = GetQuality(attr.Value);
if (v > MAX_QUALITY)
{
LOGERROR("Quality levels can only go up to %i (file %s)", MAX_QUALITY, pathname.string8());
return false;
}
if (v <= quality)
{
LOGERROR("Elements must be in increasing quality order (file %s)", pathname.string8());
return false;
}
quality = v;
found_quality = true;
}
else if (attr.Name == at_file)
{
if (attr.Value.empty())
LOGWARNING("Empty actor file specified (file %s)", pathname.string8());
file = attr.Value;
}
else if (attr.Name == at_inline)
use_inline = true;
}
if (!found_quality)
quality = MAX_QUALITY;
std::unique_ptr base = std::make_unique(m_ObjectManager, *this, quality);
if (use_inline)
{
if (inlineActor.GetNodeName() == -1)
{
LOGERROR("Actor quality level refers to inline definition, but no inline definition found (file %s)", pathname.string8());
return false;
}
if (!base->Load(XeroFile, inlineActor))
{
LOGERROR("Invalid inline actor (actor '%s')", pathname.string8());
return false;
}
}
else if (file.empty())
{
if (!base->Load(XeroFile, actor))
{
LOGERROR("Invalid actor (actor '%s')", pathname.string8());
return false;
}
}
else
{
if (actor.GetChildNodes().size() > 0)
LOGWARNING("Actor definition refers to file but has children elements, they will be ignored (file %s)", pathname.string8());
// Open up an external file to load.
// Don't crash hard when failures happen, but log them and continue
CXeromyces XeroActor;
if (XeroActor.Load(g_VFS, "art/actors/" + file, "actor") == PSRETURN_OK)
{
const XMBElement& root = XeroActor.GetRoot();
if (root.GetNodeName() != el_actor)
{
LOGERROR("Included actors cannot define quality levels (opening %s from file %s)", file, pathname.string8());
return false;
}
if (!base->Load(XeroActor, root))
{
LOGERROR("Invalid actor (actor '%s' loaded from '%s')", file, pathname.string8());
return false;
}
}
else
{
LOGERROR("Could not open actor file at path %s (file %s)", file, pathname.string8());
return false;
}
m_UsedFiles.insert(file);
}
m_ObjectBases.emplace_back(std::move(base));
}
if (quality != MAX_QUALITY)
{
LOGERROR("The highest quality level must be %i, but the highest level found was %i (file %s)", MAX_QUALITY, quality, pathname.string8().c_str());
return false;
}
}
// For each quality level, check if we need to further split (because of props).
std::vector splits = QualityLevels();
for (const std::shared_ptr& base : m_ObjectBases)
base->GetQualitySplits(splits);
ENSURE(splits.size() >= 1);
if (splits.size() > MAX_LEVELS_PER_ACTOR_DEF)
{
LOGERROR("Too many quality levels (%i) for actor %s (max %i)", splits.size(), pathname.string8().c_str(), MAX_LEVELS_PER_ACTOR_DEF);
return false;
}
std::vector>::iterator it = m_ObjectBases.begin();
std::vector::const_iterator qualityLevels = splits.begin();
while (it != m_ObjectBases.end())
if ((*it)->m_QualityLevel > *qualityLevels)
{
it = ++m_ObjectBases.emplace(it, (*it)->CopyWithQuality(*qualityLevels));
++qualityLevels;
}
else if ((*it)->m_QualityLevel == *qualityLevels)
{
++it;
++qualityLevels;
}
else
++it;
return true;
}
bool CActorDef::UsesFile(const VfsPath& pathname) const
{
return m_UsedFiles.find(pathname) != m_UsedFiles.end();
}
void CActorDef::LoadErrorPlaceholder(const VfsPath& pathname)
{
m_UsedFiles.clear();
m_ObjectBases.clear();
m_UsedFiles.emplace(pathname);
m_Pathname = pathname;
m_ObjectBases.emplace_back(std::make_shared(m_ObjectManager, *this, MAX_QUALITY));
}
Index: ps/trunk/source/graphics/ObjectManager.cpp
===================================================================
--- ps/trunk/source/graphics/ObjectManager.cpp (revision 25612)
+++ ps/trunk/source/graphics/ObjectManager.cpp (revision 25613)
@@ -1,193 +1,232 @@
/* Copyright (C) 2021 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 "ObjectManager.h"
#include "graphics/ObjectBase.h"
#include "graphics/ObjectEntry.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Game.h"
#include "ps/Profile.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpVisual.h"
bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const
{
if (ObjectBaseIdentifier < a.ObjectBaseIdentifier)
return true;
else if (ObjectBaseIdentifier > a.ObjectBaseIdentifier)
return false;
else
return ActorVariation < a.ActorVariation;
}
static Status ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast(param)->ReloadChangedFile(path);
}
CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation)
: m_MeshManager(meshManager), m_SkeletonAnimManager(skeletonAnimManager), m_Simulation(simulation)
{
RegisterFileReloadFunc(ReloadChangedFileCB, this);
m_QualityHook = std::make_unique(g_ConfigDB.RegisterHookAndCall("max_actor_quality", [this]() { ActorQualityChanged(); }));
+ m_VariantDiversityHook = std::make_unique(g_ConfigDB.RegisterHookAndCall("variant_diversity", [this]() { VariantDiversityChanged(); }));
if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng"))
LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'");
}
CObjectManager::~CObjectManager()
{
UnloadObjects();
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}
std::pair CObjectManager::FindActorDef(const CStrW& actorName)
{
ENSURE(!actorName.empty());
decltype(m_ActorDefs)::iterator it = m_ActorDefs.find(actorName);
if (it != m_ActorDefs.end() && !it->second.outdated)
return { true, *it->second.obj };
std::unique_ptr actor = std::make_unique(*this);
VfsPath pathname = VfsPath("art/actors/") / actorName;
bool success = true;
if (!actor->Load(pathname))
{
// In case of failure, load a placeholder - we want to have an actor around for hotloading.
// (this will leave garbage actors in the object manager if loading files with typos in the name,
// but that's unlikely to be a large memory problem).
LOGERROR("CObjectManager::FindActorDef(): Cannot find actor '%s'", utf8_from_wstring(actorName));
actor->LoadErrorPlaceholder(pathname);
success = false;
}
return { success, *m_ActorDefs.insert_or_assign(actorName, std::move(actor)).first->second.obj };
}
CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed)
{
if (!actor)
return nullptr;
const std::shared_ptr& base = actor->GetBase(m_QualityLevel);
std::vector*> completeSelections;
for (const std::set& selectionSet : selections)
completeSelections.emplace_back(&selectionSet);
// To maintain a consistent look between quality levels, first complete with the highest-quality variants.
// then complete again at the required quality level (since not all variants may be available).
std::set highQualitySelections = actor->GetBase(255)->CalculateRandomRemainingSelections(seed, selections);
completeSelections.emplace_back(&highQualitySelections);
// We don't have to pass the high-quality selections here because they have higher priority anyways.
std::set remainingSelections = base->CalculateRandomRemainingSelections(seed, selections);
completeSelections.emplace_back(&remainingSelections);
return FindObjectVariation(base, completeSelections);
}
CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections)
{
PROFILE2("FindObjectVariation");
// Look to see whether this particular variation has already been loaded
std::vector choices = base->CalculateVariationKey(completeSelections);
ObjectKey key (base->GetIdentifier(), choices);
decltype(m_Objects)::iterator it = m_Objects.find(key);
if (it != m_Objects.end() && !it->second.outdated)
return it->second.obj.get();
// If it hasn't been loaded, load it now.
std::unique_ptr obj = std::make_unique(base, m_Simulation);
// TODO (for some efficiency): use the pre-calculated choices for this object,
// which has already worked out what to do for props, instead of passing the
// selections into BuildVariation and having it recalculate the props' choices.
if (!obj->BuildVariation(completeSelections, choices, *this))
return nullptr;
return m_Objects.insert_or_assign(key, std::move(obj)).first->second.obj.get();
}
CTerrain* CObjectManager::GetTerrain()
{
CmpPtr cmpTerrain(m_Simulation, SYSTEM_ENTITY);
if (!cmpTerrain)
return NULL;
return cmpTerrain->GetCTerrain();
}
+CObjectManager::VariantDiversity CObjectManager::GetVariantDiversity() const
+{
+ return m_VariantDiversity;
+}
+
void CObjectManager::UnloadObjects()
{
m_Objects.clear();
m_ActorDefs.clear();
}
Status CObjectManager::ReloadChangedFile(const VfsPath& path)
{
// Mark old entries as outdated so we don't reload them from the cache
for (std::pair>& object : m_Objects)
if (!object.second.outdated && object.second.obj->m_Base->UsesFile(path))
object.second.outdated = true;
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
// Reload actors that use a changed object
for (std::pair>& actor : m_ActorDefs)
{
if (!actor.second.outdated && actor.second.obj->UsesFile(path))
actor.second.outdated = true;
// Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the
// object with all correct variations, and we don't want to waste space storing it just for the
// rare occurrence of hotloading, so we'll tell the component (which does preserve the information)
// to do the reloading itself
for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
static_cast(eit->second)->Hotload(actor.first);
}
return INFO::OK;
}
void CObjectManager::ActorQualityChanged()
{
int quality;
CFG_GET_VAL("max_actor_quality", quality);
if (quality == m_QualityLevel)
return;
m_QualityLevel = quality > 255 ? 255 : quality < 0 ? 0 : quality;
// No need to reload entries or actors, but we do need to reload all units.
const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
static_cast(eit->second)->Hotload();
// Trigger an interpolate call - needed because the game is generally paused & models disappear otherwise.
m_Simulation.Interpolate(0.f, 0.f, 0.f);
}
+
+void CObjectManager::VariantDiversityChanged()
+{
+ CStr value;
+ CFG_GET_VAL("variant_diversity", value);
+ VariantDiversity variantDiversity = VariantDiversity::FULL;
+ if (value == "none")
+ variantDiversity = VariantDiversity::NONE;
+ else if (value == "limited")
+ variantDiversity = VariantDiversity::LIMITED;
+ // Otherwise assume full.
+
+ if (variantDiversity == m_VariantDiversity)
+ return;
+
+ m_VariantDiversity = variantDiversity;
+
+ // Mark old entries as outdated so we don't reload them from the cache.
+ for (std::pair>& object : m_Objects)
+ object.second.outdated = true;
+
+ // Reload actors.
+ for (std::pair>& actor : m_ActorDefs)
+ actor.second.outdated = true;
+
+ // Reload visual actor components.
+ const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
+ for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
+ static_cast(eit->second)->Hotload();
+
+ // Trigger an interpolate call - needed because the game is generally paused & models disappear otherwise.
+ m_Simulation.Interpolate(0.f, 0.f, 0.f);
+}
Index: ps/trunk/source/graphics/ObjectManager.h
===================================================================
--- ps/trunk/source/graphics/ObjectManager.h (revision 25612)
+++ ps/trunk/source/graphics/ObjectManager.h (revision 25613)
@@ -1,130 +1,151 @@
/* Copyright (C) 2021 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_OBJECTMANAGER
#define INCLUDED_OBJECTMANAGER
#include "ps/CStr.h"
#include "lib/file/vfs/vfs_path.h"
#include
#include