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 +#include #include #include class CActorDef; class CConfigDBHook; class CMeshManager; class CObjectBase; class CObjectEntry; class CSkeletonAnimManager; class CSimulation2; class CTerrain; /////////////////////////////////////////////////////////////////////////////////////////// // CObjectManager: manager class for all possible actor types class CObjectManager { NONCOPYABLE(CObjectManager); public: // Unique identifier of an actor variation struct ObjectKey { ObjectKey(const CStr& identifier, const std::vector& var) : ObjectBaseIdentifier(identifier), ActorVariation(var) {} bool operator< (const CObjectManager::ObjectKey& a) const; private: CStr ObjectBaseIdentifier; std::vector ActorVariation; }; + /** + * Governs how random variants are selected by ObjectBase + */ + enum class VariantDiversity + { + NONE, + LIMITED, + FULL + }; + public: // constructor, destructor CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation); ~CObjectManager(); // Provide access to the manager classes for meshes and animations - they're // needed when objects are being created and so this seems like a convenient // place to centralise access. CMeshManager& GetMeshManager() const { return m_MeshManager; } CSkeletonAnimManager& GetSkeletonAnimManager() const { return m_SkeletonAnimManager; } void UnloadObjects(); /** * Get the actor definition for the given path name. * If the actor cannot be loaded, this will return a placeholder actor. * @return Success/failure boolean and a valid actor definition. */ std::pair FindActorDef(const CStrW& actorName); /** * Get the object entry for a given actor & the given selections list. * @param selections - a possibly incomplete list of selections. * @param seed - the randomness seed to use to complete the random selections. */ CObjectEntry* FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed); /** * @see FindObjectVariation. * These take a complete selection. These are pointers to sets that are * guaranteed to exist (pointers are used to avoid copying the sets). */ CObjectEntry* FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections); CObjectEntry* FindObjectVariation(const CStrW& objname, const std::vector*>& completeSelections); /** * Get the terrain object that actors managed by this manager should be linked * with (primarily for the purpose of decals) */ CTerrain* GetTerrain(); + VariantDiversity GetVariantDiversity() const; + /** * Reload any scripts that were loaded from the given filename. * (This is used to implement hotloading.) */ Status ReloadChangedFile(const VfsPath& path); /** * Reload actors that have a quality setting. Used when changing the actor quality. */ void ActorQualityChanged(); + /** + * Reload actors. Used when changing the variant diversity. + */ + void VariantDiversityChanged(); + CMeshManager& m_MeshManager; CSkeletonAnimManager& m_SkeletonAnimManager; CSimulation2& m_Simulation; u8 m_QualityLevel = 100; std::unique_ptr m_QualityHook; + VariantDiversity m_VariantDiversity = VariantDiversity::FULL; + std::unique_ptr m_VariantDiversityHook; + template struct Hotloadable { Hotloadable() = default; Hotloadable(std::unique_ptr&& ptr) : obj(std::move(ptr)) {} bool outdated = false; std::unique_ptr obj; }; // TODO: define a hash and switch to unordered_map std::map> m_Objects; std::unordered_map> m_ActorDefs; }; #endif