give the runtime code assemblies, a build entry point, and fix two names

No project code is in Assembly-CSharp any more. Five assemblies: Core (no
dependencies), Locomotion.Math, Skills, Game (everything else under Scripts),
and StarterAssets.

Game is one assembly rather than one per area, which is what the plan assumed.
Measuring the dependency graph first found three cycles, all through UI —
CharacterPanelView reaches into Dialogue, PlayerControlLock into Player, and
UILayerBootstrap into Cinematics — plus two edges a using-scan cannot see,
because NPCStandIn is in the global namespace. Assemblies cannot be circular, so
splitting further means relocating those three files. That is a code-movement
task, not an asmdef task, and nothing needs it yet.

StarterAssets had to get an assembly of its own. It had none, so it lived in
Assembly-CSharp, and an asmdef assembly cannot reference the predefined
assemblies — four runtime files use it, and all four would have stopped
compiling.

The payoff is immediate: BootstrapTests no longer needs reflection to reach
NPCStandIn and CinemachineFollowsPlayer, which is exactly why Phase 3 wanted
this.

Assets/Editor/PlayerBuild.cs is the headless build entry point. It reads the
enabled scenes, asserts Bootstrap is scene 0 because the player opens scene 0 on
launch, honours the -buildOutput the CLI forwards from -o, and calls
EditorApplication.Exit(1) on anything short of Succeeded — without which Unity
exits 0 on a build that produced nothing and CI goes green on it. Verified end to
end: a 172MB .app in about two minutes.

Two renames, both different from what the plan described. DialougueSkillComparison.cs
contains a class called SkillFunctions — the filename never matched the type, so
it is now SkillFunctions.cs. The two .yarn files with spaces in their names lost
the spaces rather than becoming kebab-case, which would have made them
inconsistent with Bartender.yarn and SC101.yarn; spaces were the actual problem.
Both renames preserved their .meta GUIDs, and the yarnproject graph keys and the
two character sheets were updated.

EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes, make build green.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 21:01:27 +02:00
co-authored by Claude Opus 5
parent 40b4547c71
commit bb0f2b760c
27 changed files with 351 additions and 74 deletions
@@ -23,11 +23,11 @@
"x": 282,
"y": 12
},
"Bradford Kane.yarn": {
"BradfordKane.yarn": {
"x": 1320,
"y": 2287
},
"Chevalier Cassian Thal.yarn": {
"ChevalierCassianThal.yarn": {
"x": 1621,
"y": 2260
},
@@ -0,0 +1,123 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
namespace NightclubArcadia.EditorTools
{
/// <summary>
/// Headless player builds.
///
/// Not called "BuildPipeline" on purpose — that name collides with
/// UnityEditor.BuildPipeline, which this has to call.
///
/// The reason to have this at all rather than let the CLI drive Unity's built-in
/// build: a flag-only build reports success even when it produced nothing useful,
/// because Unity exits 0 unless the process itself failed. CI then goes green on a
/// build that does not exist. Every entry point here inspects the BuildReport and
/// calls EditorApplication.Exit(1) on anything short of success.
///
/// unity build NightclubArcadia --target StandaloneOSX \
/// --execute-method NightclubArcadia.EditorTools.PlayerBuild.BuildMacOS \
/// -o build/NightclubArcadia.app
///
/// The CLI forwards -o as -buildOutput, and honouring it is this method's job.
/// </summary>
public static class PlayerBuild
{
public static void BuildMacOS() => Build(BuildTarget.StandaloneOSX, "build/NightclubArcadia.app");
public static void BuildWindows() => Build(BuildTarget.StandaloneWindows64, "build/NightclubArcadia.exe");
public static void BuildLinux() => Build(BuildTarget.StandaloneLinux64, "build/NightclubArcadia");
static void Build(BuildTarget target, string defaultOutput)
{
var output = ArgValue("-buildOutput") ?? defaultOutput;
var scenes = EditorBuildSettings.scenes
.Where(s => s.enabled)
.Select(s => s.path)
.ToArray();
if (scenes.Length == 0)
{
Fail("No enabled scenes in the build settings — the player would have nothing to load.");
return;
}
// Bootstrap loads the others by name at runtime, so it has to be index 0.
if (!scenes[0].EndsWith("/Bootstrap.unity", StringComparison.Ordinal))
{
Fail($"Scene 0 is '{scenes[0]}', expected Assets/Scenes/Bootstrap.unity. " +
"The player opens scene 0 on launch.");
return;
}
Debug.Log($"[PlayerBuild] {target} -> {output}");
foreach (var scene in scenes)
{
Debug.Log($"[PlayerBuild] scene: {scene}");
}
var options = new BuildPlayerOptions
{
scenes = scenes,
locationPathName = output,
target = target,
options = BuildOptions.None,
};
BuildReport report;
try
{
report = UnityEditor.BuildPipeline.BuildPlayer(options);
}
catch (Exception e)
{
Fail($"BuildPlayer threw: {e}");
return;
}
var summary = report.summary;
Debug.Log($"[PlayerBuild] {summary.result} — {summary.totalSize} bytes in {summary.totalTime}, " +
$"{summary.totalErrors} error(s), {summary.totalWarnings} warning(s)");
if (summary.result != BuildResult.Succeeded)
{
foreach (var step in report.steps)
{
foreach (var msg in step.messages.Where(m => m.type == LogType.Error || m.type == LogType.Exception))
{
Debug.LogError($"[PlayerBuild] {step.name}: {msg.content}");
}
}
Fail($"Build did not succeed: {summary.result}");
return;
}
Debug.Log("[PlayerBuild] ok");
}
static void Fail(string message)
{
Debug.LogError($"[PlayerBuild] {message}");
EditorApplication.Exit(1);
}
/// <summary>Reads "-name value" out of the command line Unity was launched with.</summary>
static string ArgValue(string name)
{
var args = Environment.GetCommandLineArgs();
for (var i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], name, StringComparison.Ordinal))
{
return args[i + 1];
}
}
return null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ec643a873224cc89e1f94dfb735037d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
{
"name": "NightclubArcadia.Core",
"rootNamespace": "NightclubArcadia.Core",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6a610a410f5944dca5c7961fee29bdff
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
{
"name": "NightclubArcadia.Game",
"rootNamespace": "NightclubArcadia",
"references": [
"NightclubArcadia.Core",
"NightclubArcadia.Skills",
"NightclubArcadia.Locomotion.Math",
"StarterAssets",
"YarnSpinner.Unity",
"Unity.Cinemachine",
"Unity.InputSystem",
"Unity.AI.Navigation",
"Unity.TextMeshPro",
"UnityEngine.UI"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5d49f74ccc8b4e41aeb2d51faf439392
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -342,6 +342,9 @@ MonoBehaviour:
skyOcclusionIntensityMultiplier:
m_OverrideState: 1
m_Value: 1
worldOffset:
m_OverrideState: 1
m_Value: {x: 0, y: 0, z: 0}
--- !u!114 &-1216621516061285780
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -376,6 +379,9 @@ MonoBehaviour:
highQualityFiltering:
m_OverrideState: 1
m_Value: 0
filter:
m_OverrideState: 1
m_Value: 0
downscale:
m_OverrideState: 1
m_Value: 0
@@ -462,8 +468,6 @@ MonoBehaviour:
- {fileID: -6288072647309666549}
- {fileID: 7518938298396184218}
- {fileID: -1410297666881709256}
- {fileID: -7750755424749557576}
- {fileID: -5139089513906902183}
--- !u!114 &853819529557874667
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -100,16 +100,16 @@ MonoBehaviour:
m_Keys: []
m_Values:
m_PrefilteringModeMainLightShadows: 3
m_PrefilteringModeAdditionalLight: 4
m_PrefilteringModeAdditionalLightShadows: 0
m_PrefilteringModeAdditionalLight: 0
m_PrefilteringModeAdditionalLightShadows: 2
m_PrefilterXRKeywords: 1
m_PrefilteringModeForwardPlus: 1
m_PrefilteringModeForwardPlus: 2
m_PrefilteringModeDeferredRendering: 0
m_PrefilteringModeScreenSpaceOcclusion: 1
m_PrefilteringModeScreenSpaceOcclusion: 2
m_PrefilterDebugKeywords: 1
m_PrefilterWriteRenderingLayers: 0
m_PrefilterWriteRenderingLayers: 1
m_PrefilterHDROutput: 1
m_PrefilterAlphaOutput: 0
m_PrefilterAlphaOutput: 1
m_PrefilterSSAODepthNormals: 0
m_PrefilterSSAOSourceDepthLow: 1
m_PrefilterSSAOSourceDepthMedium: 1
@@ -121,21 +121,21 @@ MonoBehaviour:
m_PrefilterSSAOSampleCountHigh: 1
m_PrefilterDBufferMRT1: 1
m_PrefilterDBufferMRT2: 1
m_PrefilterDBufferMRT3: 0
m_PrefilterSoftShadowsQualityLow: 0
m_PrefilterSoftShadowsQualityMedium: 0
m_PrefilterSoftShadowsQualityHigh: 0
m_PrefilterDBufferMRT3: 1
m_PrefilterSoftShadowsQualityLow: 1
m_PrefilterSoftShadowsQualityMedium: 1
m_PrefilterSoftShadowsQualityHigh: 1
m_PrefilterSoftShadows: 0
m_PrefilterScreenCoord: 1
m_PrefilterScreenSpaceIrradiance: 0
m_PrefilterScreenSpaceIrradiance: 1
m_PrefilterNativeRenderPass: 1
m_PrefilterUseLegacyLightmaps: 0
m_PrefilterBicubicLightmapSampling: 0
m_PrefilterReflectionProbeRotation: 0
m_PrefilterBicubicLightmapSampling: 1
m_PrefilterReflectionProbeRotation: 1
m_PrefilterReflectionProbeBlending: 0
m_PrefilterReflectionProbeBoxProjection: 0
m_PrefilterReflectionProbeAtlas: 0
m_PrefilterPointSamplingUpsampling: 0
m_PrefilterPointSamplingUpsampling: 1
m_ShaderVariantLogLevel: 0
m_ShadowCascades: 0
m_Textures:
@@ -68,7 +68,22 @@ MonoBehaviour:
- rid: 6097656519685046288
- rid: 6097656519685046289
m_RuntimeSettings:
m_List: []
m_List:
- rid: 6852985685364965378
- rid: 6852985685364965379
- rid: 6852985685364965380
- rid: 6852985685364965381
- rid: 6852985685364965384
- rid: 6852985685364965392
- rid: 6852985685364965394
- rid: 8712630790384254976
- rid: 6097656519685046277
- rid: 6097656519685046279
- rid: 6097656519685046281
- rid: 6097656519685046282
- rid: 6097656519685046283
- rid: 6097656519685046286
- rid: 6097656519685046288
m_AssetVersion: 10
m_ObsoleteDefaultVolumeProfile: {fileID: 0}
m_RenderingLayerNames:
@@ -0,0 +1,16 @@
{
"name": "StarterAssets",
"rootNamespace": "StarterAssets",
"references": [
"Unity.InputSystem"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cf9c9abf848e4aceb606fcd539acb801
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,7 +1,6 @@
using System.Collections;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using NightclubArcadia.Cinematics;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -20,13 +19,6 @@ namespace NightclubArcadia.PlayMode.Tests
/// none of it has run. This can.
///
/// If this suite fails, the game does not start. Treat it that way.
///
/// Two checks reach their types by name through reflection rather than referencing
/// them. NPCStandIn and NightclubArcadia.Cinematics live in Assembly-CSharp, and an
/// assembly-definition test assembly cannot reference the predefined assemblies —
/// only the other way round. Giving the runtime code its own asmdefs
/// (docs/restructure-plan.md Phase 4.1) removes the need for this; until then the
/// reflection is the accommodation, and it is confined to these two tests.
/// </summary>
public class BootstrapTests
{
@@ -84,18 +76,12 @@ namespace NightclubArcadia.PlayMode.Tests
[Test]
public void EveryNpc_ResolvedItsDialogueRunner()
{
var npcs = Object.FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Include, FindObjectsSortMode.None)
.Where(m => m != null && m.GetType().Name == "NPCStandIn")
.ToArray();
var npcs = Object.FindObjectsByType<NPCStandIn>(FindObjectsInactive.Include, FindObjectsSortMode.None);
Assert.IsNotEmpty(npcs, "Expected NPCs in the level scene");
foreach (var npc in npcs)
{
var field = npc.GetType().GetField("dialogueRunner",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(field, "NPCStandIn no longer has a dialogueRunner field");
Assert.IsNotNull(field.GetValue(npc) as Object,
Assert.IsNotNull(npc.dialogueRunner,
$"'{npc.name}' did not resolve a DialogueRunner. It lives in the Systems scene, " +
"so the serialized reference is gone and SceneServices.Resolve has to find it.");
}
@@ -104,16 +90,14 @@ namespace NightclubArcadia.PlayMode.Tests
[Test]
public void RevealCameras_BoundToThePlayer()
{
var bound = Object.FindObjectsByType<CinemachineCamera>(FindObjectsInactive.Include, FindObjectsSortMode.None)
.Where(cam => cam.GetComponents<MonoBehaviour>()
.Any(m => m != null && m.GetType().Name == "CinemachineFollowsPlayer"))
.ToArray();
Assert.IsNotEmpty(bound, "Expected at least one runtime-bound camera");
var binders = Object.FindObjectsByType<CinemachineFollowsPlayer>(
FindObjectsInactive.Include, FindObjectsSortMode.None);
Assert.IsNotEmpty(binders, "Expected at least one runtime-bound camera");
foreach (var cam in bound)
foreach (var binder in binders)
{
Assert.IsNotNull(cam.Follow,
$"'{cam.name}' never bound its Follow target to the player");
Assert.IsNotNull(binder.GetComponent<CinemachineCamera>().Follow,
$"'{binder.name}' never bound its Follow target to the player");
}
}
@@ -2,6 +2,8 @@
"name": "NightclubArcadia.PlayMode.Tests",
"rootNamespace": "NightclubArcadia.PlayMode.Tests",
"references": [
"NightclubArcadia.Game",
"NightclubArcadia.Core",
"NightclubArcadia.Skills",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",