SceneRewardReader.cs
using Sandbox;
using System.Text.Json.Nodes;

// Digs the EscapeZone's payout out of a mission scene without loading it.
//
// The job board lives in the hideout, so the mission map isn't in memory
// when the board needs to advertise what a job pays. A SceneFile does
// expose its raw GameObject JSON though, so the number can be read
// straight out of that - which keeps the map itself the single source of
// truth instead of asking for the figure to be typed in twice and kept in
// step by hand.
public static class SceneRewardReader
{
	// Matches EscapeZone.EscapeReward's own default. Used when the scene
	// has an EscapeZone whose reward was left at the default, since s&box
	// omits default values when it serialises.
	const int DefaultReward = 1000;

	/// <summary>
	/// The EscapeReward of the first EscapeZone in this scene, or 0 if the
	/// scene is unset or has no escape zone in it.
	/// </summary>
	public static int ReadEscapeReward( SceneFile scene )
	{
		if ( scene?.GameObjects is null )
			return 0;

		foreach ( var go in scene.GameObjects )
		{
			var reward = FindInObject( go );

			if ( reward.HasValue )
				return reward.Value;
		}

		return 0;
	}

	// Walks the GameObject tree looking for an EscapeZone component.
	// Recursive because the escape zone is very unlikely to be a
	// root-level object - it'll be nested under whatever the map is
	// organised into.
	static int? FindInObject( JsonNode node )
	{
		if ( node is not JsonObject obj )
			return null;

		if ( obj["Components"] is JsonArray components )
		{
			foreach ( var component in components )
			{
				if ( component is not JsonObject c )
					continue;

				if ( c["__type"]?.GetValue<string>() != nameof( EscapeZone ) )
					continue;

				return c["EscapeReward"] is JsonValue value && value.TryGetValue<int>( out var reward )
					? reward
					: DefaultReward;
			}
		}

		if ( obj["Children"] is JsonArray children )
		{
			foreach ( var child in children )
			{
				var reward = FindInObject( child );

				if ( reward.HasValue )
					return reward;
			}
		}

		return null;
	}
}