Broken RUntime
This commit is contained in:
@@ -84,13 +84,16 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
foreach (var exporter in exporters)
|
||||
{
|
||||
// Dynamic objects are handled by the runtime collision system, skip them
|
||||
if (!exporter.StaticCollider && exporter.CollisionType != PSXCollisionType.None)
|
||||
continue;
|
||||
|
||||
PSXCollisionType effectiveType = exporter.CollisionType;
|
||||
|
||||
if (effectiveType == PSXCollisionType.None)
|
||||
{
|
||||
if (autoIncludeSolid)
|
||||
{
|
||||
// Auto-include as Solid so all geometry blocks the player
|
||||
effectiveType = PSXCollisionType.Solid;
|
||||
autoIncluded++;
|
||||
}
|
||||
@@ -146,8 +149,7 @@ namespace SplashEdit.RuntimeCode
|
||||
flags = (byte)PSXSurfaceFlag.Solid;
|
||||
|
||||
// Check if stairs (tagged on exporter or steep-ish)
|
||||
if (exporter.ObjectFlags.HasFlag(PSXObjectFlags.Static) &&
|
||||
dotUp < 0.95f && dotUp > cosWalkable)
|
||||
if (dotUp < 0.95f && dotUp > cosWalkable)
|
||||
{
|
||||
flags |= (byte)PSXSurfaceFlag.Stairs;
|
||||
}
|
||||
|
||||
@@ -5,31 +5,12 @@ using UnityEngine.Serialization;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Collision type for PS1 runtime
|
||||
/// </summary>
|
||||
public enum PSXCollisionType
|
||||
{
|
||||
None = 0, // No collision
|
||||
Solid = 1, // Solid collision - blocks movement
|
||||
Trigger = 2, // Trigger - fires events but doesn't block
|
||||
Platform = 3 // Platform - solid from above, passable from below
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Object behavior flags for PS1 runtime
|
||||
/// </summary>
|
||||
[System.Flags]
|
||||
public enum PSXObjectFlags
|
||||
{
|
||||
None = 0,
|
||||
Static = 1 << 0, // Object never moves (can be optimized)
|
||||
Dynamic = 1 << 1, // Object can move
|
||||
Visible = 1 << 2, // Object is rendered
|
||||
CastsShadow = 1 << 3, // Object casts shadows (future)
|
||||
ReceivesShadow = 1 << 4, // Object receives shadows (future)
|
||||
Interactable = 1 << 5, // Player can interact with this
|
||||
AlwaysRender = 1 << 6, // Skip frustum culling for this object
|
||||
Solid = 1,
|
||||
Trigger = 2,
|
||||
Platform = 3
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(Renderer))]
|
||||
@@ -43,184 +24,70 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>();
|
||||
public PSXMesh Mesh { get; protected set; }
|
||||
|
||||
[Header("Export Settings")]
|
||||
|
||||
[FormerlySerializedAs("BitDepth")]
|
||||
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT;
|
||||
[SerializeField] private LuaFile luaFile;
|
||||
|
||||
[Header("Object Flags")]
|
||||
[SerializeField] private PSXObjectFlags objectFlags = PSXObjectFlags.Static | PSXObjectFlags.Visible;
|
||||
|
||||
[Header("Collision Settings")]
|
||||
|
||||
[SerializeField] private PSXCollisionType collisionType = PSXCollisionType.None;
|
||||
[SerializeField] private bool staticCollider = true;
|
||||
[SerializeField] private bool exportCollisionMesh = false;
|
||||
[SerializeField] private Mesh customCollisionMesh; // Optional simplified collision mesh
|
||||
[Tooltip("Layer mask for collision detection (1-8)")]
|
||||
[SerializeField] private Mesh customCollisionMesh;
|
||||
[Range(1, 8)]
|
||||
[SerializeField] private int collisionLayer = 1;
|
||||
|
||||
[Header("Navigation")]
|
||||
[Tooltip("Include this object's walkable surfaces in nav region generation")]
|
||||
[SerializeField] private bool generateNavigation = false;
|
||||
|
||||
[Header("Gizmo Settings")]
|
||||
[FormerlySerializedAs("PreviewNormals")]
|
||||
[SerializeField] private bool previewNormals = false;
|
||||
[SerializeField] private float normalPreviewLength = 0.5f;
|
||||
[SerializeField] private bool showCollisionBounds = true;
|
||||
|
||||
// Public accessors for editor and export
|
||||
public PSXBPP BitDepth => bitDepth;
|
||||
public PSXCollisionType CollisionType => collisionType;
|
||||
public bool StaticCollider => staticCollider;
|
||||
public bool ExportCollisionMesh => exportCollisionMesh;
|
||||
public Mesh CustomCollisionMesh => customCollisionMesh;
|
||||
public int CollisionLayer => collisionLayer;
|
||||
public PSXObjectFlags ObjectFlags => objectFlags;
|
||||
public bool GenerateNavigation => generateNavigation;
|
||||
|
||||
// For assigning texture from editor
|
||||
public Texture2D texture;
|
||||
|
||||
private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new();
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (previewNormals)
|
||||
{
|
||||
MeshFilter filter = GetComponent<MeshFilter>();
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
Mesh mesh = filter.sharedMesh;
|
||||
Vector3[] vertices = mesh.vertices;
|
||||
Vector3[] normals = mesh.normals;
|
||||
|
||||
Gizmos.color = Color.green;
|
||||
|
||||
for (int i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
Vector3 worldVertex = transform.TransformPoint(vertices[i]);
|
||||
Vector3 worldNormal = transform.TransformDirection(normals[i]);
|
||||
|
||||
Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Draw collision bounds when object is selected
|
||||
if (showCollisionBounds && collisionType != PSXCollisionType.None)
|
||||
{
|
||||
MeshFilter filter = GetComponent<MeshFilter>();
|
||||
Mesh collisionMesh = customCollisionMesh != null ? customCollisionMesh : (filter?.sharedMesh);
|
||||
|
||||
if (collisionMesh != null)
|
||||
{
|
||||
Bounds bounds = collisionMesh.bounds;
|
||||
|
||||
// Choose color based on collision type
|
||||
switch (collisionType)
|
||||
{
|
||||
case PSXCollisionType.Solid:
|
||||
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.5f); // Red
|
||||
break;
|
||||
case PSXCollisionType.Trigger:
|
||||
Gizmos.color = new Color(0.3f, 1f, 0.3f, 0.5f); // Green
|
||||
break;
|
||||
case PSXCollisionType.Platform:
|
||||
Gizmos.color = new Color(0.3f, 0.3f, 1f, 0.5f); // Blue
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw AABB
|
||||
Matrix4x4 oldMatrix = Gizmos.matrix;
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
|
||||
// Draw filled with lower alpha
|
||||
Color fillColor = Gizmos.color;
|
||||
fillColor.a = 0.1f;
|
||||
Gizmos.color = fillColor;
|
||||
Gizmos.DrawCube(bounds.center, bounds.size);
|
||||
|
||||
Gizmos.matrix = oldMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CreatePSXTextures2D()
|
||||
{
|
||||
Renderer renderer = GetComponent<Renderer>();
|
||||
Textures.Clear();
|
||||
if (renderer != null)
|
||||
if (renderer == null) return;
|
||||
|
||||
Material[] materials = renderer.sharedMaterials;
|
||||
foreach (Material mat in materials)
|
||||
{
|
||||
// If an override texture is set, use it for all submeshes
|
||||
if (texture != null)
|
||||
if (mat == null || mat.mainTexture == null) continue;
|
||||
|
||||
Texture mainTexture = mat.mainTexture;
|
||||
Texture2D tex2D = mainTexture is Texture2D existing
|
||||
? existing
|
||||
: ConvertToTexture2D(mainTexture);
|
||||
|
||||
if (tex2D == null) continue;
|
||||
|
||||
if (cache.TryGetValue((tex2D.GetInstanceID(), bitDepth), out var cached))
|
||||
{
|
||||
PSXTexture2D tex;
|
||||
if (cache.ContainsKey((texture.GetInstanceID(), bitDepth)))
|
||||
{
|
||||
tex = cache[(texture.GetInstanceID(), bitDepth)];
|
||||
}
|
||||
else
|
||||
{
|
||||
tex = PSXTexture2D.CreateFromTexture2D(texture, bitDepth);
|
||||
tex.OriginalTexture = texture;
|
||||
cache.Add((texture.GetInstanceID(), bitDepth), tex);
|
||||
}
|
||||
Textures.Add(tex);
|
||||
return;
|
||||
Textures.Add(cached);
|
||||
}
|
||||
|
||||
Material[] materials = renderer.sharedMaterials;
|
||||
|
||||
foreach (Material mat in materials)
|
||||
else
|
||||
{
|
||||
if (mat != null && mat.mainTexture != null)
|
||||
{
|
||||
Texture mainTexture = mat.mainTexture;
|
||||
Texture2D tex2D = null;
|
||||
|
||||
if (mainTexture is Texture2D existingTex2D)
|
||||
{
|
||||
tex2D = existingTex2D;
|
||||
}
|
||||
else
|
||||
{
|
||||
tex2D = ConvertToTexture2D(mainTexture);
|
||||
}
|
||||
|
||||
if (tex2D != null)
|
||||
{
|
||||
PSXTexture2D tex;
|
||||
if (cache.ContainsKey((tex2D.GetInstanceID(), bitDepth)))
|
||||
{
|
||||
tex = cache[(tex2D.GetInstanceID(), bitDepth)];
|
||||
}
|
||||
else
|
||||
{
|
||||
tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
|
||||
tex.OriginalTexture = tex2D;
|
||||
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
|
||||
}
|
||||
Textures.Add(tex);
|
||||
}
|
||||
}
|
||||
var tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
|
||||
tex.OriginalTexture = tex2D;
|
||||
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
|
||||
Textures.Add(tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Texture2D ConvertToTexture2D(Texture texture)
|
||||
private static Texture2D ConvertToTexture2D(Texture src)
|
||||
{
|
||||
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
|
||||
Texture2D texture2D = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false);
|
||||
|
||||
RenderTexture currentActiveRT = RenderTexture.active;
|
||||
RenderTexture.active = texture as RenderTexture;
|
||||
RenderTexture.active = src as RenderTexture;
|
||||
|
||||
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
|
||||
texture2D.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0);
|
||||
texture2D.Apply();
|
||||
|
||||
RenderTexture.active = currentActiveRT;
|
||||
|
||||
@@ -9,6 +9,11 @@ using UnityEngine;
|
||||
|
||||
namespace SplashEdit.RuntimeCode
|
||||
{
|
||||
public enum PSXSceneType
|
||||
{
|
||||
Exterior = 0,
|
||||
Interior = 1
|
||||
}
|
||||
|
||||
[ExecuteInEditMode]
|
||||
public class PSXSceneExporter : MonoBehaviour
|
||||
@@ -35,7 +40,7 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
[Header("Scene Type")]
|
||||
[Tooltip("Exterior uses BVH frustum culling. Interior uses room/portal occlusion.")]
|
||||
public int SceneType = 0; // 0=exterior, 1=interior
|
||||
public PSXSceneType SceneType = PSXSceneType.Exterior;
|
||||
|
||||
[Header("Cutscenes")]
|
||||
[Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")]
|
||||
@@ -166,7 +171,7 @@ namespace SplashEdit.RuntimeCode
|
||||
// Collect them early so both systems use the same room indices.
|
||||
PSXRoom[] rooms = null;
|
||||
PSXPortalLink[] portalLinks = null;
|
||||
if (SceneType == 1)
|
||||
if (SceneType == PSXSceneType.Interior)
|
||||
{
|
||||
rooms = FindObjectsByType<PSXRoom>(FindObjectsSortMode.None);
|
||||
portalLinks = FindObjectsByType<PSXPortalLink>(FindObjectsSortMode.None);
|
||||
@@ -194,7 +199,7 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
// Phase 5: Build room/portal system (for interior scenes)
|
||||
_roomBuilder = new PSXRoomBuilder();
|
||||
if (SceneType == 1)
|
||||
if (SceneType == PSXSceneType.Interior)
|
||||
{
|
||||
if (rooms != null && rooms.Length > 0)
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace SplashEdit.RuntimeCode
|
||||
public float gravity;
|
||||
|
||||
// Scene configuration (v11)
|
||||
public int sceneType; // 0=exterior, 1=interior
|
||||
public PSXSceneType sceneType;
|
||||
public bool fogEnabled;
|
||||
public Color fogColor;
|
||||
public int fogDensity; // 1-10
|
||||
@@ -111,7 +111,12 @@ namespace SplashEdit.RuntimeCode
|
||||
int colliderCount = 0;
|
||||
foreach (var e in scene.exporters)
|
||||
{
|
||||
if (e.CollisionType != PSXCollisionType.None)
|
||||
if (e.CollisionType == PSXCollisionType.None || e.StaticCollider)
|
||||
continue;
|
||||
Mesh cm = e.CustomCollisionMesh != null
|
||||
? e.CustomCollisionMesh
|
||||
: e.GetComponent<MeshFilter>()?.sharedMesh;
|
||||
if (cm != null)
|
||||
colliderCount++;
|
||||
}
|
||||
|
||||
@@ -121,17 +126,17 @@ namespace SplashEdit.RuntimeCode
|
||||
exporterIndex[scene.exporters[i]] = i;
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Header (72 bytes total — splashpack v8)
|
||||
// Header (104 bytes — splashpack v15)
|
||||
// ──────────────────────────────────────────────────────
|
||||
writer.Write('S');
|
||||
writer.Write('P');
|
||||
writer.Write((ushort)13); // version
|
||||
writer.Write((ushort)15);
|
||||
writer.Write((ushort)luaFiles.Count);
|
||||
writer.Write((ushort)scene.exporters.Length);
|
||||
writer.Write((ushort)0); // navmeshCount (legacy)
|
||||
writer.Write((ushort)scene.atlases.Length);
|
||||
writer.Write((ushort)clutCount);
|
||||
writer.Write((ushort)colliderCount);
|
||||
writer.Write((ushort)scene.interactables.Length);
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.x, gte));
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(-scene.playerPos.y, gte));
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.z, gte));
|
||||
@@ -142,37 +147,23 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerHeight, gte));
|
||||
|
||||
// Scene Lua index
|
||||
if (scene.sceneLuaFile != null)
|
||||
writer.Write((short)luaFiles.IndexOf(scene.sceneLuaFile));
|
||||
else
|
||||
writer.Write((short)-1);
|
||||
|
||||
// BVH info
|
||||
writer.Write((ushort)scene.bvh.NodeCount);
|
||||
writer.Write((ushort)scene.bvh.TriangleRefCount);
|
||||
|
||||
// Component counts (version 4)
|
||||
writer.Write((ushort)scene.interactables.Length);
|
||||
writer.Write((ushort)0); // healthCount (removed)
|
||||
writer.Write((ushort)0); // timerCount (removed)
|
||||
writer.Write((ushort)0); // spawnerCount (removed)
|
||||
writer.Write((ushort)scene.sceneType);
|
||||
writer.Write((ushort)0); // pad0
|
||||
|
||||
// NavGrid (version 5, legacy)
|
||||
writer.Write((ushort)0);
|
||||
writer.Write((ushort)0);
|
||||
|
||||
// Scene type (version 6)
|
||||
writer.Write((ushort)scene.sceneType); // 0=exterior, 1=interior
|
||||
writer.Write((ushort)0);
|
||||
|
||||
// World collision + nav regions (version 7)
|
||||
writer.Write((ushort)scene.collisionExporter.MeshCount);
|
||||
writer.Write((ushort)scene.collisionExporter.TriangleCount);
|
||||
writer.Write((ushort)scene.navRegionBuilder.RegionCount);
|
||||
writer.Write((ushort)scene.navRegionBuilder.PortalCount);
|
||||
|
||||
// Movement parameters (version 8, 12 bytes)
|
||||
// Movement parameters (12 bytes)
|
||||
{
|
||||
const float fps = 30f;
|
||||
float movePerFrame = scene.moveSpeed / fps / gte;
|
||||
@@ -187,52 +178,49 @@ namespace SplashEdit.RuntimeCode
|
||||
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(gravPsx * 4096f), 0, 65535));
|
||||
|
||||
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerRadius, gte));
|
||||
writer.Write((ushort)0); // padding
|
||||
writer.Write((ushort)0); // pad1
|
||||
}
|
||||
|
||||
// Name table offset placeholder (version 9, 4 bytes)
|
||||
long nameTableOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // placeholder for name table offset
|
||||
writer.Write((uint)0);
|
||||
|
||||
// Audio clip info (version 10, 8 bytes)
|
||||
int audioClipCount = scene.audioClips?.Length ?? 0;
|
||||
writer.Write((ushort)audioClipCount);
|
||||
writer.Write((ushort)0); // padding
|
||||
writer.Write((ushort)0); // pad2
|
||||
long audioTableOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // placeholder for audio table offset
|
||||
writer.Write((uint)0);
|
||||
|
||||
// Fog + room/portal header (version 11, 12 bytes)
|
||||
{
|
||||
writer.Write((byte)(scene.fogEnabled ? 1 : 0));
|
||||
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.r * 255f), 0, 255));
|
||||
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.g * 255f), 0, 255));
|
||||
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.b * 255f), 0, 255));
|
||||
writer.Write((byte)Mathf.Clamp(scene.fogDensity, 1, 10));
|
||||
writer.Write((byte)0); // reserved
|
||||
writer.Write((byte)0); // pad3
|
||||
int roomCount = scene.roomBuilder?.RoomCount ?? 0;
|
||||
int portalCount = scene.roomBuilder?.PortalCount ?? 0;
|
||||
int roomTriRefCount = scene.roomBuilder?.TotalTriRefCount ?? 0;
|
||||
// roomCount is the room count NOT including catch-all; the binary adds +1 for it
|
||||
writer.Write((ushort)(roomCount > 0 ? roomCount + 1 : 0));
|
||||
writer.Write((ushort)portalCount);
|
||||
writer.Write((ushort)roomTriRefCount);
|
||||
}
|
||||
|
||||
// Cutscene header (version 12, 8 bytes)
|
||||
int cutsceneCount = scene.cutscenes?.Length ?? 0;
|
||||
writer.Write((ushort)cutsceneCount);
|
||||
writer.Write((ushort)0); // reserved_cs
|
||||
writer.Write((ushort)0); // pad4
|
||||
long cutsceneTableOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // cutsceneTableOffset placeholder
|
||||
|
||||
// UI canvas header (version 13, 8 bytes)
|
||||
int uiCanvasCount = scene.canvases?.Length ?? 0;
|
||||
int uiFontCount = scene.fonts?.Length ?? 0;
|
||||
writer.Write((ushort)uiCanvasCount);
|
||||
writer.Write((byte)uiFontCount); // was uiReserved low byte
|
||||
writer.Write((byte)0); // was uiReserved high byte
|
||||
writer.Write((byte)uiFontCount);
|
||||
writer.Write((byte)0); // uiPad5
|
||||
long uiTableOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // uiTableOffset placeholder
|
||||
writer.Write((uint)0);
|
||||
|
||||
long pixelDataOffsetPos = writer.BaseStream.Position;
|
||||
writer.Write((uint)0); // pixelDataOffset placeholder
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Lua file metadata
|
||||
@@ -295,7 +283,7 @@ namespace SplashEdit.RuntimeCode
|
||||
for (int exporterIdx = 0; exporterIdx < scene.exporters.Length; exporterIdx++)
|
||||
{
|
||||
PSXObjectExporter exporter = scene.exporters[exporterIdx];
|
||||
if (exporter.CollisionType == PSXCollisionType.None)
|
||||
if (exporter.CollisionType == PSXCollisionType.None || exporter.StaticCollider)
|
||||
continue;
|
||||
|
||||
MeshFilter meshFilter = exporter.GetComponent<MeshFilter>();
|
||||
@@ -483,33 +471,6 @@ namespace SplashEdit.RuntimeCode
|
||||
}
|
||||
}
|
||||
|
||||
// Atlas pixel data
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
|
||||
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
|
||||
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
|
||||
writer.Write(atlas.vramPixels[x, y].Pack());
|
||||
}
|
||||
|
||||
// CLUT data
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
|
||||
foreach (VRAMPixel color in texture.ColorPalette)
|
||||
writer.Write((ushort)color.Pack());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Object name table (version 9)
|
||||
// ──────────────────────────────────────────────────────
|
||||
@@ -535,46 +496,50 @@ namespace SplashEdit.RuntimeCode
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Audio clip data (version 10)
|
||||
// Metadata entries are 16 bytes each, written contiguously.
|
||||
// Name strings follow the metadata block with backfilled offsets.
|
||||
// ADPCM blobs deferred to dead zone.
|
||||
// ──────────────────────────────────────────────────────
|
||||
List<long> audioDataOffsetPositions = new List<long>();
|
||||
if (audioClipCount > 0 && scene.audioClips != null)
|
||||
{
|
||||
// Write audio table: per clip metadata (12 bytes each)
|
||||
AlignToFourBytes(writer);
|
||||
long audioTableStart = writer.BaseStream.Position;
|
||||
|
||||
// First pass: write metadata placeholders (16 bytes each)
|
||||
List<long> audioDataOffsetPositions = new List<long>();
|
||||
List<long> audioNameOffsetPositions = new List<long>();
|
||||
List<string> audioClipNames = new List<string>();
|
||||
|
||||
// Phase 1: Write all 16-byte metadata entries contiguously
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
var clip = scene.audioClips[i];
|
||||
audioDataOffsetPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((uint)0); // dataOffset placeholder
|
||||
writer.Write((uint)(clip.adpcmData?.Length ?? 0)); // sizeBytes
|
||||
writer.Write((ushort)clip.sampleRate);
|
||||
string name = clip.clipName ?? "";
|
||||
if (name.Length > 255) name = name.Substring(0, 255);
|
||||
|
||||
audioDataOffsetPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((uint)0); // dataOffset placeholder (backfilled in dead zone)
|
||||
writer.Write((uint)(clip.adpcmData?.Length ?? 0));
|
||||
writer.Write((ushort)clip.sampleRate);
|
||||
writer.Write((byte)(clip.loop ? 1 : 0));
|
||||
writer.Write((byte)System.Math.Min(name.Length, 255));
|
||||
writer.Write((byte)name.Length);
|
||||
audioNameOffsetPositions.Add(writer.BaseStream.Position);
|
||||
writer.Write((uint)0); // nameOffset placeholder
|
||||
audioClipNames.Add(name);
|
||||
}
|
||||
|
||||
// Second pass: write ADPCM data and backfill offsets
|
||||
// Phase 2: Write name strings (after all metadata entries)
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
byte[] data = scene.audioClips[i].adpcmData;
|
||||
if (data != null && data.Length > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
long dataPos = writer.BaseStream.Position;
|
||||
writer.Write(data);
|
||||
string name = audioClipNames[i];
|
||||
long namePos = writer.BaseStream.Position;
|
||||
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
|
||||
writer.Write(nameBytes);
|
||||
writer.Write((byte)0);
|
||||
|
||||
// Backfill data offset
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((uint)dataPos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((uint)namePos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Backfill audio table offset in header
|
||||
@@ -584,28 +549,6 @@ namespace SplashEdit.RuntimeCode
|
||||
writer.Write((uint)audioTableStart);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
int totalAudioBytes = 0;
|
||||
foreach (var clip in scene.audioClips)
|
||||
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
|
||||
|
||||
// Third pass: write audio clip names and backfill name offsets
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
string name = scene.audioClips[i].clipName ?? "";
|
||||
if (name.Length > 255) name = name.Substring(0, 255);
|
||||
long namePos = writer.BaseStream.Position;
|
||||
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
|
||||
writer.Write(nameBytes);
|
||||
writer.Write((byte)0); // null terminator
|
||||
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((uint)namePos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
@@ -635,11 +578,12 @@ namespace SplashEdit.RuntimeCode
|
||||
// ──────────────────────────────────────────────────────
|
||||
// UI canvas + font data (version 13)
|
||||
// Font descriptors: 112 bytes each (before canvas data)
|
||||
// Font pixel data: raw 4bpp (after font descriptors)
|
||||
// Canvas descriptor table: 12 bytes per canvas
|
||||
// Element records: 48 bytes each
|
||||
// Name and text strings follow with offset backfill
|
||||
// Font pixel data is deferred to the dead zone.
|
||||
// ──────────────────────────────────────────────────────
|
||||
List<long> fontDataOffsetPositions = new List<long>();
|
||||
if ((uiCanvasCount > 0 && scene.canvases != null) || uiFontCount > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
@@ -648,7 +592,6 @@ namespace SplashEdit.RuntimeCode
|
||||
// ── Font descriptors (112 bytes each) ──
|
||||
// Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2)
|
||||
// dataOffset(4) dataSize(4)
|
||||
List<long> fontDataOffsetPositions = new List<long>();
|
||||
if (scene.fonts != null)
|
||||
{
|
||||
foreach (var font in scene.fonts)
|
||||
@@ -669,32 +612,9 @@ namespace SplashEdit.RuntimeCode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Font pixel data (raw 4bpp) ──
|
||||
if (scene.fonts != null)
|
||||
{
|
||||
for (int fi = 0; fi < scene.fonts.Length; fi++)
|
||||
{
|
||||
var font = scene.fonts[fi];
|
||||
if (font.PixelData == null || font.PixelData.Length == 0) continue;
|
||||
|
||||
AlignToFourBytes(writer);
|
||||
long dataPos = writer.BaseStream.Position;
|
||||
writer.Write(font.PixelData);
|
||||
|
||||
// Backfill data offset
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
|
||||
writer.Write((uint)dataPos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (scene.fonts.Length > 0)
|
||||
{
|
||||
int totalFontBytes = 0;
|
||||
foreach (var f in scene.fonts) totalFontBytes += f.PixelData?.Length ?? 0;
|
||||
log?.Invoke($"{scene.fonts.Length} custom font(s) written ({totalFontBytes} bytes 4bpp data).", LogType.Log);
|
||||
}
|
||||
}
|
||||
// Font pixel data is deferred to the dead zone (after pixelDataOffset).
|
||||
// The C++ loader reads font pixel data via the dataOffset, uploads to VRAM,
|
||||
// then never accesses it again.
|
||||
|
||||
// ── Canvas descriptor table (12 bytes each) ──
|
||||
// Layout per descriptor:
|
||||
@@ -873,6 +793,96 @@ namespace SplashEdit.RuntimeCode
|
||||
log?.Invoke($"{uiCanvasCount} UI canvases ({totalElements} elements) written.", LogType.Log);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// DEAD ZONE — pixel/audio bulk data (freed after VRAM/SPU upload)
|
||||
// Everything written after this point is not needed at runtime.
|
||||
// ══════════════════════════════════════════════════════
|
||||
AlignToFourBytes(writer);
|
||||
long pixelDataStart = writer.BaseStream.Position;
|
||||
|
||||
// Atlas pixel data
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
|
||||
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
|
||||
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
|
||||
writer.Write(atlas.vramPixels[x, y].Pack());
|
||||
}
|
||||
|
||||
// CLUT data
|
||||
foreach (TextureAtlas atlas in scene.atlases)
|
||||
{
|
||||
foreach (var texture in atlas.ContainedTextures)
|
||||
{
|
||||
if (texture.ColorPalette != null)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
|
||||
|
||||
foreach (VRAMPixel color in texture.ColorPalette)
|
||||
writer.Write((ushort)color.Pack());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio ADPCM data
|
||||
if (audioClipCount > 0 && scene.audioClips != null)
|
||||
{
|
||||
for (int i = 0; i < audioClipCount; i++)
|
||||
{
|
||||
byte[] data = scene.audioClips[i].adpcmData;
|
||||
if (data != null && data.Length > 0)
|
||||
{
|
||||
AlignToFourBytes(writer);
|
||||
long dataPos = writer.BaseStream.Position;
|
||||
writer.Write(data);
|
||||
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin);
|
||||
writer.Write((uint)dataPos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
|
||||
int totalAudioBytes = 0;
|
||||
foreach (var clip in scene.audioClips)
|
||||
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
|
||||
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
|
||||
}
|
||||
|
||||
// Font pixel data
|
||||
if (scene.fonts != null)
|
||||
{
|
||||
for (int fi = 0; fi < scene.fonts.Length; fi++)
|
||||
{
|
||||
var font = scene.fonts[fi];
|
||||
if (font.PixelData == null || font.PixelData.Length == 0) continue;
|
||||
|
||||
AlignToFourBytes(writer);
|
||||
long dataPos = writer.BaseStream.Position;
|
||||
writer.Write(font.PixelData);
|
||||
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
|
||||
writer.Write((uint)dataPos);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill pixelDataOffset in header
|
||||
{
|
||||
long curPos = writer.BaseStream.Position;
|
||||
writer.Seek((int)pixelDataOffsetPos, SeekOrigin.Begin);
|
||||
writer.Write((uint)pixelDataStart);
|
||||
writer.Seek((int)curPos, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
long totalSize = writer.BaseStream.Position;
|
||||
long deadBytes = totalSize - pixelDataStart;
|
||||
log?.Invoke($"Pixel/audio dead zone: {deadBytes / 1024}KB (freed after VRAM/SPU upload).", LogType.Log);
|
||||
|
||||
// Backfill offsets
|
||||
BackfillOffsets(writer, luaOffset, "lua", log);
|
||||
BackfillOffsets(writer, meshOffset, "mesh", log);
|
||||
|
||||
Reference in New Issue
Block a user