| name | sdv-mp-gamepad-audit |
| description | This skill should be used when the user asks to "check multiplayer support", "audit gamepad navigation", "verify split-screen", or before committing changes to menu code. Also use when working on per-player state, mutex locking, PerScreen usage, controller navigation, or any file in the Managers/ or Menus/ directories. |
| version | 1.0.0 |
SDV Multiplayer & Gamepad Audit
Static analysis for multiplayer and gamepad support. Since runtime testing of these features is difficult, this skill provides comprehensive code auditing to catch issues before manual testing.
When to Run This Audit
- Before committing changes to any file in
Managers/ or Menus/
- After adding new state variables (fields, properties)
- When implementing features that affect multiple players
- Before PR creation
Multiplayer Audit Checklist
1. PerScreen Wrapper Audit
All per-player mutable state MUST use PerScreen<T>:
private static MenuManager menuManager;
private static int currentTab = 0;
private static readonly PerScreen<MenuManager> menuManager = new();
private static readonly PerScreen<int> currentTab = new(() => 0);
menuManager.Value.DoSomething();
int tab = currentTab.Value;
Audit Points:
2. Context Checks
Verify proper context gating:
if (!Context.IsWorldReady) return;
if (!Context.IsPlayerFree) return;
if (!Context.IsMainPlayer) return;
if (Context.IsMainPlayer)
{
Helper.Data.WriteSaveData("key", data);
}
Audit Points:
3. Mutex Locking (Shared Furniture)
Dressers and other shared furniture require mutex:
if (!dresser.mutex.IsLocked())
{
dresser.mutex.RequestLock(delegate {
Game1.activeClickableMenu = new WardrobeMenu();
});
}
public override void emergencyShutDown()
{
base.emergencyShutDown();
dresserObject?.mutex?.ReleaseLock();
}
protected override void cleanupBeforeExit()
{
base.cleanupBeforeExit();
dresserObject?.mutex?.ReleaseLock();
}
Audit Points:
4. Player Data Keying
Per-player save data must be uniquely keyed:
string dataPath = $"data/favoritesData/{Game1.player.Name}_{Constants.SaveFolderName}_{Game1.player.UniqueMultiplayerID}.json";
Game1.player.modData[$"{ModId}/PlayerFlag"] = "true";
Audit Points:
5. Location Iteration
Use utilities that work for farmhands:
foreach (var loc in Game1.locations) { }
Utility.ForAllLocations(loc => {
});
Gamepad Audit Checklist
1. ClickableComponent Navigation
Every clickable element needs navigation IDs:
var button = new ClickableComponent(bounds, "name");
button.myID = 1001;
button.leftNeighborID = 1000;
button.rightNeighborID = 1002;
button.upNeighborID = 900;
button.downNeighborID = 1100;
Audit Points:
2. Component List Population
public override void populateClickableComponentList()
{
base.populateClickableComponentList();
allClickableComponents.AddRange(labels);
allClickableComponents.AddRange(buttons);
allClickableComponents.AddRange(equipmentIcons);
}
Audit Points:
3. Focus Management
public override void snapToDefaultClickableComponent()
{
base.snapToDefaultClickableComponent();
currentlySnappedComponent = getComponentWithID(defaultFocusId);
snapCursorToCurrentSnappedComponent();
}
Audit Points:
4. Button Hints
For important actions, show controller button hints:
if (Game1.options.gamepadControls)
{
b.Draw(Game1.controllerMaps, position, sourceRect, Color.White);
}
Audit Output Format
When running this audit, report findings as:
## Multiplayer Audit Results
### Issues Found
1. **PerScreen Missing** - `Managers/MenuManager.cs:45`
- Field `currentCategory` is static but not wrapped in PerScreen<T>
- Fix: Change to `private static readonly PerScreen<string> currentCategory = new();`
2. **Mutex Not Released** - `Menus/WardrobeMenu.cs:312`
- `emergencyShutDown()` doesn't release mutex
- Fix: Add `dresserObject?.mutex?.ReleaseLock();`
### Gamepad Navigation Issues
1. **Missing neighborID** - `Menus/FavoritesMenu.cs:189`
- `deleteButton` has no `upNeighborID` set
- Fix: Add `deleteButton.upNeighborID = renameButton.myID;`
### Passed Checks
- [x] All PerScreen fields are readonly
- [x] Context.IsWorldReady checks present in event handlers
- [x] Save operations check Context.IsMainPlayer
Quick Reference
Split-Screen Player Access
var current = perScreenField.Value;
var screen0 = perScreenField.GetValueForScreen(0);
int screenId = Context.ScreenId;
Common Navigation ID Ranges
const int LABELS = 10000;
const int BUTTONS = 20000;
const int CATEGORIES = 30000;
const int OUTFIT_CARDS = 40000;