Add Modes - #1041
Conversation
d6ea4ee to
b24bc6d
Compare
cef7eea to
e59088f
Compare
|
Marked as draft because I tested and this isn't working as-is. With BAR on branch pr/5074 and BYAR-Chobby on branch pr/1041 clicking on Skirmish does nothing and produces the following error: |
6a15372 to
f2c74d8
Compare
|
Converting this to draft again. It is technically ready for review (and entirely functional) on this side but the BAR side is not quite feature complete yet so holding off until the whole package is ready to go. |
f2c74d8 to
7c97c3f
Compare
3e7771b to
b4c8803
Compare
d17cddd to
f2c842d
Compare
|
|
||
| isProgrammaticUpdate = true | ||
| if not allowRanked then | ||
| localModoptions["ranked_game"] = "0" |
There was a problem hiding this comment.
When a mode has allowRanked false this forces ranked_game to '0', but I don't see anywhere that sets it back when you switch to a mode that does allow ranked. So if I pick a non-ranked mode and then switch back to a ranked one, won't ranked_game stay at '0'? And since it goes out through SetModOptions as a normal value change, I think it actually gets sent to the host, not just shown locally. It's also easy to miss because SetRankedModeAllowed just below doesn't seem to be defined anywhere, so nothing updates the battle-room UI. Could this restore ranked_game on the allowRanked path?
| end | ||
|
|
||
| if mode.modOptions then | ||
| for optKey, rule in pairs(mode.modOptions) do |
There was a problem hiding this comment.
This only sets or clears lockedOptions for keys that are in the new mode's modOptions, and lockedOptions itself looks like it's only reset when the window is first created, not here. So if mode A locks an option and you then switch to a Customize/retain mode that doesn't list it, won't that key keep its old lockedOptions entry and then get overwritten with the server value on Accept? I think someone who had set that option would see it silently revert. Would resetting lockedOptions at the top of applyMode, the same way modeLocked is rebuilt, fix it?
| mode_key = params[1] | ||
| kv_params = params[2:] | ||
|
|
||
| category_data = _as_dict(_modes().get('categories')).get(category) |
There was a problem hiding this comment.
This calls .get('categories') straight on whatever _modes() returns. If the fetched or cached json is ever not an object (a list, say), won't that throw an AttributeError right into the perl bridge? The accesses below it go through _as_dict but this first one doesn't. Could the top level be wrapped the same way?
| if isinstance(spec, dict) and spec.get('locked') | ||
| ) | ||
| for (key, _val) in settings: | ||
| if key in locked_keys: |
There was a problem hiding this comment.
The user's key gets lowercased up at line 209, but locked_keys is built from the mod_options keys as-is. So if a mode locks 'ResourceMult' and someone passes resourcemult=something, won't this check miss it and apply the override anyway? I think BAR option names are all lowercase in practice so it may not bite today, but it makes the lock weaker than it looks. Lowercasing both sides would close it.
|
|
||
| for url in urls: | ||
| try: | ||
| with urllib.request.urlopen(url, timeout=_FETCH_TIMEOUT) as resp: |
There was a problem hiding this comment.
This is a synchronous urlopen on what I understand is SPADS's single thread, with a 5s timeout and up to two URLs, so a slow or unreachable release could stall the host for around 10s on the first !mode after a rehost. From what I can see the vote-eligibility check reaches it too, which a normal player can trigger. It's once per version so not constant, but a multi-second freeze of the autohost still seems bad. Could this be backgrounded or moved to load time?
|
I'm not deep in the SPADS side or a lot of this Lua, so some of this might be me misreading it.
|
|
@burnhamrobertp Man those were great catches. I apologize for the lapse in reply. Ironically, I have been working in there and reviewing this myself ignorant of your review. I'm going to go over your comments with a more fine toothed comb now but I do think I addressed a lot of these already. Re your concerns for my quick hot takes:
|
Add the server-side ModeCommand SPADS plugin backing the Chobby sharing tab. !mode <category> <key> [opt=val ...] expands a mode preset to its full modoption set (a bare !mode applies the whole preset) and levels each option identically to the built-in bSet, rejecting unknown option keys instead of silently setting them. Game-mode/preset data is fetched directly via the plugin and pinned to a short-sha game version, reading the host-regenerated game_modes.json (which replaces the committed sharing_presets.json; the generated file is gitignored). The plugin records the selector key so the client can reflect an externally-set mode.
Add the sharing tab to the modoptions panel and battle room: pick a sharing mode, route the category change through the !mode plugin, and reflect a mode set externally (live-refreshing the panel when it changes under us). Mode descriptions show as hover tooltips. Introduce a shared ModeResolver (libs/liblobby/lobby/moderesolver.lua) that resolves a mode + user deviations to a full modoption set, used by both the panel and skirmish so single-player expands a mode locally with the same logic the SPADS plugin mirrors server-side. !mode deviations are scoped to the mode's own options, and only deviations are sent (guarded against the 256-char say cap). Rounding/retain fixes along the way: round number values to their step in the editbox seed, locked overlay, and summary; honor retainValues for non-sticky modes; reset category to defaults when switching mode; hide subsection headers with no visible options; and stop leaking subheader/separator pseudo-rows as modoptions.
GetDesyncGameStates logged "Failed to load" then fell through to string.len(infolog) on a nil result. Wrap processing in the else branch so a failed VFS.LoadFile just logs and skips.
Configuration.password is false when rememberPassword is off; the old unconditional auto-login passed that boolean to lobby:Login, which then gsub'd a boolean in TextEraseNewline. Gate auto-login on a string password (or steamMode) so manual login still works.
The hand-rolled modules/*/modes scan mirrored the game's layout convention; use ModuleHandler.ModeDirs() when the archive ships it so new module-contributed mode dirs need no lobby change. Older archives without module_handler keep the fallback scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opening the modoptions window auto-applied the first preset file each category shipped: the default fell back to a hardcoded "enabled", which no category is obliged to have. For missions that turned every game into a Mission game on Apply — deathmode pinned, every unit def loaded — for having opened the options window. Selection now seeds from the battle's selector value, else the selector's own default, re-read on every open so a pick that was never applied does not survive a cancel. The !mode send treats an unset selector as the default mode: a coop room that fielded a ScavengersAI by hand and tuned scav_* options was in "none" all along, and reflecting a nil selector back as "!mode scavengers none" would have reset options the room set by other means. A preset's bots now take the field in skirmish: scavengers activate on a ScavengersAI being present, which no modoption can say, so picking the Scavengers mode adds the AI and switching away removes it. Only AIs the reconciliation itself added are ever removed; multiplayer bot fielding stays with the server's !mode handling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A modoption section may now declare `mode_category`: the axis that owns the choice its options belong to. BAR's game axis is the first user — scav_defense_options declares mode_category = "game", so the Scavengers choice lives on game_mode beside standard and mission, where it is orthogonal to them, instead of every flavor minting a selector of its own that could contradict its neighbours'. A governed section gets no tab: its dials fold into the governing category's mode panel, where the active mode already decides what shows and what locks. The resolver scopes a category to its governed sections the same way — section entries ride in the defs list it already receives — so a mode's full effective set covers the dials it pins across sections, in the panel, the !mode send, and the skirmish expansion alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ppear on selection The mode panel rendered only what the active preset claimed, which made Standard and Mission blank pages: Standard claims nothing on purpose, and Mission's pins live in other sections. Three rendering rules replace the one: * The category's own section keeps the strict whitelist — the mode owns it outright, as the sharing tab always worked. * A section governed by declaration (mode_category) renders editable unless claimed: these are shared dials the axis carries — BAR hands Main to the game axis — so a mode locks what it pins and leaves the rest open. * A governed section bound to one preset (mode_key) renders only while that preset is the mode: scav dials appear when Scavengers is picked and not before. The mode's description now opens the panel, and options a mode pins outside the panel's sections (a mission pins forceallunits, which lives with the cheats) render beneath — locked rows included — so picking a mode shows everything it does. Options hidden from the lobby stay hidden; their pins still apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
processChildrenLocks shifts every sibling below a locked row by comparing rowOrginal, which only option rows carry. The mode panel's description TextBox has none, so the first lock that fired in a mode panel — deathmode's territorial domination locks, now that Main folds under Game — compared a number with nil and took the whole modoptions window down with it, mid-build, leaving a blank page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A long description (Mission's) wrapped past the fixed row-and-a-half reserved for it and rendered into the first option row. Measure the text against a conservative width — layout has not run when the panel is built — and start the rows below what it will actually occupy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
game_modes.json named the file before "game" was itself a category; now it reads as that category's slice of a file that carries them all. The exporter is export_modes.lua, the command is /exportmodes, the file is modes.json. Comments here follow (one also pointed at the exporter's pre-module path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ent categories The export renamed game_modes.json to modes.json — the file carries every category, and "game" is itself a category now — so the plugin's asset scheme follows: modes-<shortsha>.json, modes-<channel>.json, the `modes` release tag, the local and cache paths. Nothing had shipped under the old names. The help examples spoke a selector that no longer exists (!mode sharing — the category is transfer now) and showed nothing from the game axis; both fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An unranked mode now declares itself in its preset — Ranked(false) pins ranked_game off, and the locked row in the panel is the indicator, next to every other thing the mode decides. A floating red label above options that visibly contradict... nothing, was explaining a mechanism the options themselves now show. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The middle rule (governed sections rendering editable-unless-claimed) made every mode wear all of Main: draft order under a mission, TeamFFA box shuffling under scavengers. It dies, and mode_key with it. One rule remains, the one the transfer tab always had: claimed and not hidden is shown, anything else is not. The open game modes now claim the ordinary dials explicitly — Standard's preset is the inventory of what an ordinary game lets a lobby decide — and territorial domination's round dials ride on its end rule, shown in no other mode. Sections keep their remaining jobs: grouping, reset scope, and the resolver's category defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whitelist left every filtered-out group's ruled line behind, so a curated mode drew a stack of separators between nothing. A separator now renders only when visible rows sit on both sides of it — held until the next visible row flushes it, dropped at the end if none does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bby's A mission is written for factions. Its manifest publishes side indexes through the mission_name items (the archive resolves names to indexes at discovery, so the lobby does a plain lookup), and picking the Mission mode applies them: the player moves to the story's side, and the seat-filler bot is fielded as the enemy's — factioned, per mission, without the mode preset having to know any mission's particulars. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An AI the user placed is the user's business — unless it sits in the seat the mode just set: a leftover skirmish AI beside a mission's seat-filler plays a skirmish against the scenery. Bots now clear their own ally slot when fielded (with a console line saying so), uniformly for every bot-fielding mode; co-op teammates on other allies stay put, and re-adding is one click if someone truly wants both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mode-bot ledger outlived the battle: the lobby object is a singleton, so a fresh skirmish inherited last battle's 'already fielded' entries and skipped re-adding the seat-filler. The ledger now resets at the new-battle boundary, the reconcile prunes entries whose AI left the field, and the skirmish path applies SetMode even when nothing 'changed' — modoptions persist across battles, the table does not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #1040
Summary
Creates a dedicated Sharing tab that loads the game's
modes/sharing/*.luapresets, applies per-mode values/locks/visibility, and shows ranked eligibility. Enables clean separation between sharing policy configuration and gadget implementation.Changes
ModeConfigpresets frommodes/sharing/ranked=truefor every sharing mode)Screenshot of BAR changes without this PR
Related PRs
BAR: beyond-all-reason/Beyond-All-Reason#5704
Architecture: beyond-all-reason/Beyond-All-Reason#8018
Issue: #1040
Why
Moves sharing configuration from scattered individual modoptions to coherent, mode-driven policies. Players choose high-level intent (Enabled, Disabled, Easy Tax, Tech Core, Customize) rather than manually configuring multiple interdependent options.
Screenshots
(See the bar PR)
LLM Disclosure
The Chobby part of this PR was generated with grok-code-fast-1. I’d already written and reviewed and had a JSON version, then manually implemented the lua sharing mode type, each of the modes and modoption changes on the BAR side. I asked the model to update Chobby to match the new declarative format—needed only minor fixes, and it performed well. LGTM after full review.