Skip to content

feat(devtools): add an internal presentation workbench - #150

Merged
ifBars merged 8 commits into
betafrom
agent/issue-148-presentation-workbench
Jul 27, 2026
Merged

feat(devtools): add an internal presentation workbench#150
ifBars merged 8 commits into
betafrom
agent/issue-148-presentation-workbench

Conversation

@ifBars

@ifBars ifBars commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • add an S1API-owned in-game workbench for first-person, avatar-held, and native icon presentation tuning
  • keep the workbench entirely internal: consuming mods use their normal item/product registrations and no workbench API
  • resolve only registered product and item IDs; linked avatar-equippable previews remain an item/product tab rather than a standalone command target
  • render avatar-held previews on a detached clone of the native mugshot rig, following the issue Add an in-game workbench for creating custom NPCs #149 preview lifecycle and camera controls
  • capture durable icon sprites through IconFactory after render-ready frames instead of holding the native rig's transient texture
  • add independent avatar-held visual/transform overrides to product profiles while preserving the legacy held fallback

Developer workflow

presentation_workbench product <product-id>
presentation_workbench item <item-id>
presentation_workbench <auto-resolved-product-or-item-id>
presentation_workbench close

No workbench registration code is added to consuming mods.

Context semantics

  • First person: clones the discovered visual into the real local PlayerInventory.EquipContainer on the Viewmodel layer without equipping or changing an inventory slot.
  • Avatar: uses the detached native mugshot rig and issue Add an in-game workbench for creating custom NPCs #149 camera behavior. Linked AvatarEquippable prefabs retain their scaffold/alignment point, use the game's alignment math, honor trigger-vs-bool animation metadata, and support drag orbit plus wheel zoom.
  • Icon: uses IconFactory.GenerateIconSprite, waits for render-ready frames, retries bounded captures, and presents a durable copied texture. Product profiles retain their exact generated-icon source/settings; ordinary items use their discoverable equippable visual.

Copy actions emit existing ProductPresentationTransform, local transform assignments, or IconFactory.GenerateIconSprite(...) code. No workbench-specific type appears in copied code or the exported assembly.

image

Validation

  • MonoMelon build: pass, 0 warnings
  • Il2CppMelon build: pass, 0 warnings
  • focused Mono tests: 7/7 pass
  • focused IL2CPP tests: 6/6 pass
  • direct Mono smoke in D:\SteamLibrary\steamapps\common\Schedule I_alternate: pass
    • native banana icon: visible and visually inspected
    • S1-Forklift item avatar tab: full-body rig stable, equippable attached, visually inspected
    • workbench cleanup: pass
    • DocFX page includes an in-game screenshot of the corrected front-facing avatar view
    • no smoke probe remains in Mods
  • deployed Mono DLL SHA-256: 1913EE47C3944649283AE6427521912C30903BF3ACEAD5AC57F4BEE54D2708B7

Closes #148

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e209fa2e-f59e-4a76-83a7-d2e705587189

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes align with issue #148 by adding an internal workbench, target resolution, avatar-held overrides, tests, and documentation.
Out of Scope Changes check ✅ Passed No clearly unrelated changes stand out; the code, tests, and docs all support the presentation workbench scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an internal presentation workbench.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ifBars ifBars changed the title feat(rendering): add an in-game presentation workbench feat(devtools): add an internal presentation workbench Jul 27, 2026
@ifBars
ifBars marked this pull request as ready for review July 27, 2026 07:51
@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an internal developer-only presentation workbench: a console-driven, in-game tool for tuning first-person viewmodel, third-person avatar, and native icon captures without rebuilding a mod. It is kept entirely off the public API surface (verified by the exported-types test) and cleans up all session state on close or scene unload.

  • Adds WithAvatarHeldVisual / WithAvatarHeldTransform builder methods to ProductPresentationProfile so first- and third-person poses can be independently overridden while preserving legacy held-fallback behaviour for mods that set neither.
  • Introduces PresentationWorkbenchRuntime, PresentationWorkbenchView, adapters, resolver, and exporter — all internal — driven by a new presentation_workbench console command that resolves product, item, and avatar targets automatically.
  • Hooks the workbench tick into S1API.cs and adds documentation and 32+ tests covering the resolver, public-surface contract, culture-invariant export, and the new avatar-held builder semantics.

Confidence Score: 4/5

Safe to merge for the workbench feature itself; one ordering footgun in the new builder methods could silently misconfigure avatar equippables for any mod that calls WithAvatarHeldTransform and WithAvatarHeldVisual as separate steps in the wrong order.

The workbench infrastructure is well-structured, internally contained, and the state-restore logic on close is thorough. WithAvatarHeldVisual(provider) unconditionally resets _avatarHeldTransform to null, so a builder chain of .WithAvatarHeldTransform(t).WithAvatarHeldVisual(p) silently discards the configured avatar transform and falls back to the first-person held transform instead — no error is raised and the avatar equippable is built with the wrong pose. The remaining finding is a dead assignment in ScheduleIconCapture that has no runtime impact.

Files Needing Attention: S1API/Products/ProductPresentationProfileBuilder.cs — the WithAvatarHeldVisual(Func) overload needs review of the _avatarHeldTransform reset.

Important Files Changed

Filename Overview
S1API/Products/ProductPresentationProfileBuilder.cs Adds WithAvatarHeldVisual and WithAvatarHeldTransform builder methods; the no-transform overload of WithAvatarHeldVisual unconditionally resets _avatarHeldTransform, creating a silent order-dependency when the two methods are called independently.
S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs Core 791-line session lifecycle for the workbench: state save/restore, first-person/avatar/icon preview switching, debounced icon capture, and clipboard export. ScheduleIconCapture has a dead assignment in the immediate path but is otherwise correct.
S1API/Internal/Rendering/PresentationWorkbenchView.cs Procedural Unity UGUI construction of the workbench overlay; all event bindings are cleaned up in Dispose, UI layout uses normalized anchor coords, and input parsing uses InvariantCulture. No issues found.
S1API/Products/ProductPresentationProfile.cs Adds AvatarHeldVisualProvider / AvatarHeldTransform properties and TryGetAvatarHeldVisualProvider / TryGetAvatarHeldTransform helpers with correct held-context fallback semantics.
S1API/Internal/Products/CustomProductPresentationRuntime.cs Threads avatar-source resolution through the new ResolveAvatarHeldSource helper and replaces ApplyVisualTransform(Held) with TryGetAvatarHeldTransform; backwards-compatible with existing products that set no avatar-specific fields.

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
S1API/Products/ProductPresentationProfileBuilder.cs:127-134
**`WithAvatarHeldVisual(provider)` silently erases a previously-set avatar transform**

Calling `.WithAvatarHeldVisual(provider)` unconditionally sets `_avatarHeldTransform = null`, so a builder chain of `.WithAvatarHeldTransform(t).WithAvatarHeldVisual(p)` silently drops `t` with no error or warning. `TryGetAvatarHeldTransform` then falls through to the Held first-person transform, producing incorrect third-person positioning without any indication that the configured transform was lost. The single-argument overload of `WithAvatarHeldVisual` should leave `_avatarHeldTransform` unchanged so that the two methods are truly independent and order-insensitive. The reset should only happen inside `WithAvatarHeldVisual(provider, transform)` to clear a previously-set standalone avatar transform when both are replaced together.

### Issue 2 of 2
S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs:519-533
The `immediate = true` branch of `ScheduleIconCapture` overwrites `_iconRefreshAt` immediately after the ternary sets it to `Time.unscaledTime`, making that first assignment dead code. The ternary should only compute the deferred timestamp for the `false` path.

```suggestion
            private void ScheduleIconCapture(bool immediate)
            {
                if (immediate)
                {
                    _iconRefreshAt = -1f;
                    CaptureIcon();
                }
                else
                {
                    _iconRefreshAt = Time.unscaledTime + IconDebounceSeconds;
                    _view.SetStatus("Icon recapture scheduled.");
                }
            }
```

Reviews (1): Last reviewed commit: "fix(rendering): preserve avatar preview ..." | Re-trigger Greptile

Comment thread S1API/Products/ProductPresentationProfileBuilder.cs
Comment thread S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
S1API/Internal/Rendering/ItemPresentationWorkbenchAdapter.cs (2)

96-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use whitespace-aware fallback for AnimationTrigger, matching ProductPresentationWorkbenchAdapter.

?? "RightArm_Hold_ClosedHand" only covers null; the product adapter uses string.IsNullOrWhiteSpace. An empty trigger authored on the equippable will silently propagate here.

♻️ Suggested change
-            string animationTrigger =
-                avatarEquippable?.AnimationTrigger ??
-                "RightArm_Hold_ClosedHand";
+            string animationTrigger =
+                string.IsNullOrWhiteSpace(avatarEquippable?.AnimationTrigger)
+                    ? "RightArm_Hold_ClosedHand"
+                    : avatarEquippable!.AnimationTrigger;

Also applies to: 141-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@S1API/Internal/Rendering/ItemPresentationWorkbenchAdapter.cs` around lines 96
- 98, Update the AnimationTrigger fallback in ItemPresentationWorkbenchAdapter
to use string.IsNullOrWhiteSpace, matching ProductPresentationWorkbenchAdapter,
so null, empty, and whitespace-only authored triggers resolve to
"RightArm_Hold_ClosedHand" at both referenced locations.

46-54: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Throwing from a TryCreate… method breaks the try-pattern contract.

PresentationWorkbenchResolver.TryResolve chains the three TryCreate* calls; an exception here aborts auto-resolution instead of falling through to the avatar adapter (it only surfaces as a logged error in PresentationWorkbenchRuntime.Open). Returning false is more consistent, since the icon context is optional.

♻️ Suggested change
-            GameObject iconVisual =
-                firstPersonVisual ??
-                avatar!.Provider() ??
-                throw new InvalidOperationException(
-                    $"Avatar visual for item '{itemId}' disappeared during discovery.");
+            GameObject? iconVisual =
+                firstPersonVisual ?? avatar!.Provider();
+            if (iconVisual == null)
+                return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@S1API/Internal/Rendering/ItemPresentationWorkbenchAdapter.cs` around lines 46
- 54, Update the TryCreate path in ItemPresentationWorkbenchAdapter so missing
avatar visuals return false instead of throwing InvalidOperationException.
Preserve the existing fallback selection and iconProvider behavior when a visual
is available, allowing PresentationWorkbenchResolver.TryResolve to continue to
the next adapter when no icon context exists.
S1API.Tests/Rendering/PresentationWorkbenchTests.cs (1)

45-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider dropping the MONOMELON gate on the exporter test.

PresentationWorkbenchExporter output is backend-agnostic, so this invariant-culture assertion is worth running in every configuration rather than only the Mono one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@S1API.Tests/Rendering/PresentationWorkbenchTests.cs` around lines 45 - 88,
Remove the MONOMELON conditional compilation directives surrounding
ExportUsesExistingApisAndInvariantCulture so the exporter test runs in every
configuration. Keep the test body and its invariant-culture assertions
unchanged.
S1API/Internal/Rendering/PresentationWorkbenchView.cs (1)

186-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent no-op on unparsable camera fill.

Unlike the vector rows, an invalid fill value gives the user no feedback. Add a SetStatus in the else branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@S1API/Internal/Rendering/PresentationWorkbenchView.cs` around lines 186 -
192, Update the Bind callback for _cameraFillField to add an else branch when
TryParse fails, calling SetStatus with an appropriate invalid camera-fill
message. Preserve the existing updateCameraFill(parsed) behavior for
successfully parsed values.
S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs (1)

360-362: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

cullingMask = -1 fallback renders every layer, including UI/viewmodel.

When the Player layer is missing, the preview camera draws everything into the RenderTexture. Prefer a narrow fallback (e.g. default layer only) or skip the avatar camera entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs` around lines 360 -
362, Update the avatar camera culling-mask assignment near _avatarCamera so a
missing Player layer does not fall back to -1, which renders all layers. Use a
narrow fallback such as the Default layer mask, or skip configuring the avatar
camera when LayerMask.NameToLayer("Player") returns an invalid index; preserve
the Player-layer mask when it exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@S1API/Internal/Products/CustomProductPresentationRuntime.cs`:
- Around line 940-973: The ResolveAvatarHeldSource method should treat
AvatarHeldVisualProvider failures as optional override failures: log null or
thrown-provider errors, then return the existing heldSource instead of throwing.
Preserve successful provider results and caching, and update both cached-null
and invocation-exception paths consistently.

In `@S1API/Internal/Rendering/PresentationWorkbenchView.cs`:
- Line 469: Update the font lookup in the relevant label-rendering code of
PresentationWorkbenchView to request Unity 2022.3’s built-in "LegacyRuntime.ttf"
instead of "Arial.ttf", preserving the existing dynamic label behavior.

---

Nitpick comments:
In `@S1API.Tests/Rendering/PresentationWorkbenchTests.cs`:
- Around line 45-88: Remove the MONOMELON conditional compilation directives
surrounding ExportUsesExistingApisAndInvariantCulture so the exporter test runs
in every configuration. Keep the test body and its invariant-culture assertions
unchanged.

In `@S1API/Internal/Rendering/ItemPresentationWorkbenchAdapter.cs`:
- Around line 96-98: Update the AnimationTrigger fallback in
ItemPresentationWorkbenchAdapter to use string.IsNullOrWhiteSpace, matching
ProductPresentationWorkbenchAdapter, so null, empty, and whitespace-only
authored triggers resolve to "RightArm_Hold_ClosedHand" at both referenced
locations.
- Around line 46-54: Update the TryCreate path in
ItemPresentationWorkbenchAdapter so missing avatar visuals return false instead
of throwing InvalidOperationException. Preserve the existing fallback selection
and iconProvider behavior when a visual is available, allowing
PresentationWorkbenchResolver.TryResolve to continue to the next adapter when no
icon context exists.

In `@S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs`:
- Around line 360-362: Update the avatar camera culling-mask assignment near
_avatarCamera so a missing Player layer does not fall back to -1, which renders
all layers. Use a narrow fallback such as the Default layer mask, or skip
configuring the avatar camera when LayerMask.NameToLayer("Player") returns an
invalid index; preserve the Player-layer mask when it exists.

In `@S1API/Internal/Rendering/PresentationWorkbenchView.cs`:
- Around line 186-192: Update the Bind callback for _cameraFillField to add an
else branch when TryParse fails, calling SetStatus with an appropriate invalid
camera-fill message. Preserve the existing updateCameraFill(parsed) behavior for
successfully parsed values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 68d4a1ed-bca2-4561-9bf1-ba6b7508e7e5

📥 Commits

Reviewing files that changed from the base of the PR and between 16e3861 and 312f1f1.

📒 Files selected for processing (21)
  • S1API.Tests/Products/ProductApiCompatibilityTests.cs
  • S1API.Tests/Products/ProductPresentationProfileApiCompileFixture.cs
  • S1API.Tests/Products/ProductPresentationProfileBuilderTests.cs
  • S1API.Tests/Rendering/PresentationWorkbenchTests.cs
  • S1API/Internal/Console/PresentationWorkbenchCommand.cs
  • S1API/Internal/Products/CustomProductPresentationRuntime.cs
  • S1API/Internal/Products/ProductPresentationWorkbenchAdapter.cs
  • S1API/Internal/Rendering/ItemPresentationWorkbenchAdapter.cs
  • S1API/Internal/Rendering/PresentationWorkbenchExporter.cs
  • S1API/Internal/Rendering/PresentationWorkbenchModel.cs
  • S1API/Internal/Rendering/PresentationWorkbenchResolver.cs
  • S1API/Internal/Rendering/PresentationWorkbenchRuntime.cs
  • S1API/Internal/Rendering/PresentationWorkbenchView.cs
  • S1API/Internal/Rendering/PresentationWorkbenchVisualResolver.cs
  • S1API/Products/ProductPresentationProfile.cs
  • S1API/Products/ProductPresentationProfileBuilder.cs
  • S1API/Products/ProductPresentationProfileRegistry.cs
  • S1API/S1API.cs
  • S1API/docs/generic-custom-products.md
  • S1API/docs/presentation-workbench.md
  • S1API/docs/toc.yml

Comment thread S1API/Internal/Products/CustomProductPresentationRuntime.cs
Comment thread S1API/Internal/Rendering/PresentationWorkbenchView.cs Outdated
@ifBars ifBars self-assigned this Jul 27, 2026
@ifBars ifBars added the enhancement New feature or request label Jul 27, 2026
@ifBars

ifBars commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ifBars

ifBars commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Review follow-up for 5ec3550:

  • Applied the blank/whitespace animation-trigger fallback.
  • Made item workbench discovery return false if its fallback avatar source disappears instead of throwing.
  • Added visible validation feedback for non-numeric camera-fill input.
  • The camera-layer fallback concern was already addressed in d670b12: workbench icon captures use the dedicated IconGeneration layer rather than -1/all layers.
  • Kept the exporter test Mono-gated. Enabling it for IL2CPP reproduces TypeInitializationException / DllNotFoundException: GameAssembly when constructing the IL2CPP Vector3 wrapper in the normal unit-test host.

Validation:

  • MonoMelon: 352/352 tests passed; build succeeded with 0 warnings.
  • Il2CppMelon focused presentation/product tests: 22/22 passed; build succeeded. The full IL2CPP run completed 239 passing assertions, then the existing uninitialized-native-wrapper finalizer contamination aborted the host while trying to load GameAssembly.

@ifBars
ifBars merged commit db1482b into beta Jul 27, 2026
4 checks passed
@ifBars
ifBars deleted the agent/issue-148-presentation-workbench branch July 27, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant