Broken UI and Loading screens
This commit is contained in:
@@ -109,6 +109,15 @@ namespace SplashEdit.EditorCode
|
|||||||
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.splashpack");
|
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.splashpack");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the loader pack (loading screen) output path for a scene by index.
|
||||||
|
/// Uses a deterministic naming scheme: scene_0.loading, scene_1.loading, etc.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetSceneLoaderPackPath(int sceneIndex, string sceneName)
|
||||||
|
{
|
||||||
|
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.loading");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ISO output path for release builds.
|
/// ISO output path for release builds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -877,6 +877,7 @@ namespace SplashEdit.EditorCode
|
|||||||
public bool ExportAllScenes()
|
public bool ExportAllScenes()
|
||||||
{
|
{
|
||||||
SplashBuildPaths.EnsureDirectories();
|
SplashBuildPaths.EnsureDirectories();
|
||||||
|
_loaderPackCache = new Dictionary<string, string>();
|
||||||
|
|
||||||
// Save current scene
|
// Save current scene
|
||||||
string currentScenePath = SceneManager.GetActiveScene().path;
|
string currentScenePath = SceneManager.GetActiveScene().path;
|
||||||
@@ -912,6 +913,13 @@ namespace SplashEdit.EditorCode
|
|||||||
string outputPath = SplashBuildPaths.GetSceneSplashpackPath(i, scene.name);
|
string outputPath = SplashBuildPaths.GetSceneSplashpackPath(i, scene.name);
|
||||||
exporter.ExportToPath(outputPath);
|
exporter.ExportToPath(outputPath);
|
||||||
Log($"Exported '{scene.name}' → {Path.GetFileName(outputPath)}", LogType.Log);
|
Log($"Exported '{scene.name}' → {Path.GetFileName(outputPath)}", LogType.Log);
|
||||||
|
|
||||||
|
// Export loading screen if assigned
|
||||||
|
if (exporter.LoadingScreenPrefab != null)
|
||||||
|
{
|
||||||
|
string loaderPath = SplashBuildPaths.GetSceneLoaderPackPath(i, scene.name);
|
||||||
|
ExportLoaderPack(exporter.LoadingScreenPrefab, loaderPath, i, scene.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -934,6 +942,87 @@ namespace SplashEdit.EditorCode
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache of already-exported loader packs for deduplication.
|
||||||
|
/// Key = prefab asset GUID, Value = path of the written file.
|
||||||
|
/// If two scenes reference the same loading screen prefab, we copy the file
|
||||||
|
/// instead of regenerating it.
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, string> _loaderPackCache = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
private void ExportLoaderPack(GameObject prefab, string outputPath, int sceneIndex, string sceneName)
|
||||||
|
{
|
||||||
|
string prefabPath = AssetDatabase.GetAssetPath(prefab);
|
||||||
|
string guid = AssetDatabase.AssetPathToGUID(prefabPath);
|
||||||
|
|
||||||
|
// Dedup: if we already exported this exact prefab, just copy the file
|
||||||
|
if (!string.IsNullOrEmpty(guid) && _loaderPackCache.TryGetValue(guid, out string cachedPath))
|
||||||
|
{
|
||||||
|
if (File.Exists(cachedPath))
|
||||||
|
{
|
||||||
|
File.Copy(cachedPath, outputPath, true);
|
||||||
|
Log($"Loading screen for '{sceneName}' → {Path.GetFileName(outputPath)} (deduped from {Path.GetFileName(cachedPath)})", LogType.Log);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need the PSXData resolution to pass to the writer
|
||||||
|
Vector2 resolution;
|
||||||
|
bool db, vl;
|
||||||
|
List<ProhibitedArea> pa;
|
||||||
|
DataStorage.LoadData(out resolution, out db, out vl, out pa);
|
||||||
|
|
||||||
|
// Instantiate the prefab temporarily so the components are live
|
||||||
|
// (GetComponentsInChildren needs active hierarchy)
|
||||||
|
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Pack UI image textures into VRAM (same flow as PSXSceneExporter)
|
||||||
|
TextureAtlas[] atlases = null;
|
||||||
|
PSXUIImage[] uiImages = instance.GetComponentsInChildren<PSXUIImage>(true);
|
||||||
|
if (uiImages != null && uiImages.Length > 0)
|
||||||
|
{
|
||||||
|
List<PSXTexture2D> uiTextures = new List<PSXTexture2D>();
|
||||||
|
foreach (PSXUIImage img in uiImages)
|
||||||
|
{
|
||||||
|
if (img.SourceTexture != null)
|
||||||
|
{
|
||||||
|
Utils.SetTextureImporterFormat(img.SourceTexture, true);
|
||||||
|
PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(img.SourceTexture, img.BitDepth);
|
||||||
|
tex.OriginalTexture = img.SourceTexture;
|
||||||
|
img.PackedTexture = tex;
|
||||||
|
uiTextures.Add(tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiTextures.Count > 0)
|
||||||
|
{
|
||||||
|
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(resolution, vl);
|
||||||
|
List<Rect> framebuffers = new List<Rect> { buffer1 };
|
||||||
|
if (db) framebuffers.Add(buffer2);
|
||||||
|
|
||||||
|
VRAMPacker packer = new VRAMPacker(framebuffers, pa);
|
||||||
|
var packed = packer.PackTexturesIntoVRAM(new PSXObjectExporter[0], uiTextures);
|
||||||
|
atlases = packed.atlases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectCanvasFromPrefab reads PackedTexture VRAM coords (set by packer above)
|
||||||
|
bool ok = PSXLoaderPackWriter.Write(outputPath, instance, resolution, atlases,
|
||||||
|
(msg, type) => Log(msg, type));
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
Log($"Loading screen for '{sceneName}' → {Path.GetFileName(outputPath)}", LogType.Log);
|
||||||
|
if (!string.IsNullOrEmpty(guid))
|
||||||
|
_loaderPackCache[guid] = outputPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void WriteManifest()
|
private void WriteManifest()
|
||||||
{
|
{
|
||||||
string manifestPath = SplashBuildPaths.ManifestPath;
|
string manifestPath = SplashBuildPaths.ManifestPath;
|
||||||
|
|||||||
399
Runtime/PSXLoaderPackWriter.cs
Normal file
399
Runtime/PSXLoaderPackWriter.cs
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a standalone "loader pack" binary (.loading) for a loading screen canvas.
|
||||||
|
///
|
||||||
|
/// Format v2:
|
||||||
|
/// Header (16 bytes):
|
||||||
|
/// char[2] magic = "LP"
|
||||||
|
/// uint16 version = 2
|
||||||
|
/// uint8 fontCount
|
||||||
|
/// uint8 canvasCount (always 1)
|
||||||
|
/// uint16 resW — target PS1 resolution width
|
||||||
|
/// uint16 resH — target PS1 resolution height
|
||||||
|
/// uint8 atlasCount — number of texture atlases
|
||||||
|
/// uint8 clutCount — number of CLUTs
|
||||||
|
/// uint32 tableOffset — offset to UI table (font descs + canvas data)
|
||||||
|
///
|
||||||
|
/// After header (at offset 16):
|
||||||
|
/// Atlas headers (12 bytes each × atlasCount)
|
||||||
|
/// CLUT headers (12 bytes each × clutCount)
|
||||||
|
/// Atlas pixel data (referenced by offsets in atlas headers)
|
||||||
|
/// CLUT pixel data (referenced by offsets in CLUT headers)
|
||||||
|
///
|
||||||
|
/// At tableOffset:
|
||||||
|
/// Same layout as the splashpack UI section:
|
||||||
|
/// - Font descriptors (112 bytes each)
|
||||||
|
/// - Font pixel data
|
||||||
|
/// - Canvas descriptors (12 bytes each)
|
||||||
|
/// - Element data (48 bytes per element)
|
||||||
|
/// - String data (names + text content)
|
||||||
|
///
|
||||||
|
/// This reuses the same binary layout that UISystem::loadFromSplashpack() parses,
|
||||||
|
/// so the C++ side can reuse the same parsing code.
|
||||||
|
/// </summary>
|
||||||
|
public static class PSXLoaderPackWriter
|
||||||
|
{
|
||||||
|
public const ushort LOADER_PACK_VERSION = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write a loader pack file for a loading screen canvas prefab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Output file path.</param>
|
||||||
|
/// <param name="prefab">The loading screen prefab (must contain PSXCanvas).</param>
|
||||||
|
/// <param name="resolution">Target PS1 resolution.</param>
|
||||||
|
/// <param name="atlases">Texture atlases from VRAM packing (may be null if no images).</param>
|
||||||
|
/// <param name="log">Optional log callback.</param>
|
||||||
|
/// <returns>True on success.</returns>
|
||||||
|
public static bool Write(string path, GameObject prefab, Vector2 resolution,
|
||||||
|
TextureAtlas[] atlases = null,
|
||||||
|
System.Action<string, LogType> log = null)
|
||||||
|
{
|
||||||
|
if (prefab == null)
|
||||||
|
{
|
||||||
|
log?.Invoke("LoaderPackWriter: No prefab specified.", LogType.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect canvas data from the prefab
|
||||||
|
PSXFontData[] fonts;
|
||||||
|
PSXCanvasData[] canvases = PSXUIExporter.CollectCanvasFromPrefab(prefab, resolution, out fonts);
|
||||||
|
|
||||||
|
if (canvases == null || canvases.Length == 0)
|
||||||
|
{
|
||||||
|
log?.Invoke($"LoaderPackWriter: No PSXCanvas found in prefab '{prefab.name}'.", LogType.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only export the first canvas (loading screen = single canvas)
|
||||||
|
PSXCanvasData canvas = canvases[0];
|
||||||
|
|
||||||
|
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
|
||||||
|
using (BinaryWriter writer = new BinaryWriter(fs))
|
||||||
|
{
|
||||||
|
// Count CLUTs across all atlases
|
||||||
|
int clutCount = 0;
|
||||||
|
if (atlases != null)
|
||||||
|
{
|
||||||
|
foreach (var atlas in atlases)
|
||||||
|
foreach (var tex in atlas.ContainedTextures)
|
||||||
|
if (tex.ColorPalette != null)
|
||||||
|
clutCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header (16 bytes) ──
|
||||||
|
writer.Write((byte)'L');
|
||||||
|
writer.Write((byte)'P');
|
||||||
|
writer.Write(LOADER_PACK_VERSION);
|
||||||
|
writer.Write((byte)(fonts?.Length ?? 0));
|
||||||
|
writer.Write((byte)1); // canvasCount = 1
|
||||||
|
writer.Write((ushort)resolution.x);
|
||||||
|
writer.Write((ushort)resolution.y);
|
||||||
|
writer.Write((byte)(atlases?.Length ?? 0)); // atlasCount
|
||||||
|
writer.Write((byte)clutCount); // clutCount
|
||||||
|
long tableOffsetPos = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // tableOffset placeholder
|
||||||
|
|
||||||
|
// ── Atlas headers (12 bytes each) ──
|
||||||
|
List<long> atlasOffsetPlaceholders = new List<long>();
|
||||||
|
if (atlases != null)
|
||||||
|
{
|
||||||
|
foreach (var atlas in atlases)
|
||||||
|
{
|
||||||
|
atlasOffsetPlaceholders.Add(writer.BaseStream.Position);
|
||||||
|
writer.Write((uint)0); // pixelDataOffset placeholder
|
||||||
|
writer.Write((ushort)atlas.Width);
|
||||||
|
writer.Write((ushort)TextureAtlas.Height);
|
||||||
|
writer.Write((ushort)atlas.PositionX);
|
||||||
|
writer.Write((ushort)atlas.PositionY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLUT headers (12 bytes each) ──
|
||||||
|
List<long> clutOffsetPlaceholders = new List<long>();
|
||||||
|
if (atlases != null)
|
||||||
|
{
|
||||||
|
foreach (var atlas in atlases)
|
||||||
|
{
|
||||||
|
foreach (var tex in atlas.ContainedTextures)
|
||||||
|
{
|
||||||
|
if (tex.ColorPalette != null)
|
||||||
|
{
|
||||||
|
clutOffsetPlaceholders.Add(writer.BaseStream.Position);
|
||||||
|
writer.Write((uint)0); // clutDataOffset placeholder
|
||||||
|
writer.Write((ushort)tex.ClutPackingX);
|
||||||
|
writer.Write((ushort)tex.ClutPackingY);
|
||||||
|
writer.Write((ushort)tex.ColorPalette.Count);
|
||||||
|
writer.Write((ushort)0); // pad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Atlas pixel data ──
|
||||||
|
int atlasIdx = 0;
|
||||||
|
if (atlases != null)
|
||||||
|
{
|
||||||
|
foreach (var atlas in atlases)
|
||||||
|
{
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long dataPos = writer.BaseStream.Position;
|
||||||
|
|
||||||
|
// Backfill this atlas header's pixelDataOffset
|
||||||
|
long cur = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)atlasOffsetPlaceholders[atlasIdx], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)dataPos);
|
||||||
|
writer.Seek((int)cur, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Write pixel data in row-major order (same as PSXSceneWriter)
|
||||||
|
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());
|
||||||
|
|
||||||
|
atlasIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLUT pixel data ──
|
||||||
|
int clutIdx = 0;
|
||||||
|
if (atlases != null)
|
||||||
|
{
|
||||||
|
foreach (var atlas in atlases)
|
||||||
|
{
|
||||||
|
foreach (var tex in atlas.ContainedTextures)
|
||||||
|
{
|
||||||
|
if (tex.ColorPalette != null)
|
||||||
|
{
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long dataPos = writer.BaseStream.Position;
|
||||||
|
|
||||||
|
// Backfill this CLUT header's clutDataOffset
|
||||||
|
long cur = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)clutOffsetPlaceholders[clutIdx], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)dataPos);
|
||||||
|
writer.Seek((int)cur, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
foreach (VRAMPixel color in tex.ColorPalette)
|
||||||
|
writer.Write((ushort)color.Pack());
|
||||||
|
|
||||||
|
clutIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI table (same format as splashpack UI section) ──
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long uiTableStart = writer.BaseStream.Position;
|
||||||
|
|
||||||
|
// ── Font descriptors (112 bytes each) ──
|
||||||
|
List<long> fontDataOffsetPositions = new List<long>();
|
||||||
|
if (fonts != null)
|
||||||
|
{
|
||||||
|
foreach (var font in fonts)
|
||||||
|
{
|
||||||
|
writer.Write(font.GlyphWidth); // [0]
|
||||||
|
writer.Write(font.GlyphHeight); // [1]
|
||||||
|
writer.Write(font.VramX); // [2-3]
|
||||||
|
writer.Write(font.VramY); // [4-5]
|
||||||
|
writer.Write(font.TextureHeight); // [6-7]
|
||||||
|
fontDataOffsetPositions.Add(writer.BaseStream.Position);
|
||||||
|
writer.Write((uint)0); // [8-11] dataOffset placeholder
|
||||||
|
writer.Write((uint)(font.PixelData?.Length ?? 0)); // [12-15] dataSize
|
||||||
|
if (font.AdvanceWidths != null && font.AdvanceWidths.Length >= 96)
|
||||||
|
writer.Write(font.AdvanceWidths, 0, 96);
|
||||||
|
else
|
||||||
|
writer.Write(new byte[96]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Font pixel data ──
|
||||||
|
if (fonts != null)
|
||||||
|
{
|
||||||
|
for (int fi = 0; fi < fonts.Length; fi++)
|
||||||
|
{
|
||||||
|
var font = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Canvas descriptor (12 bytes) ──
|
||||||
|
var elements = canvas.Elements ?? new PSXUIElementData[0];
|
||||||
|
string cvName = canvas.Name ?? "loading";
|
||||||
|
if (cvName.Length > 24) cvName = cvName.Substring(0, 24);
|
||||||
|
|
||||||
|
long canvasDataOffsetPos = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // dataOffset placeholder
|
||||||
|
writer.Write((byte)cvName.Length);
|
||||||
|
writer.Write(canvas.SortOrder);
|
||||||
|
writer.Write((byte)elements.Length);
|
||||||
|
byte flags = 0;
|
||||||
|
if (canvas.StartVisible) flags |= 0x01;
|
||||||
|
writer.Write(flags);
|
||||||
|
long canvasNameOffsetPos = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // nameOffset placeholder
|
||||||
|
|
||||||
|
// ── Element data (48 bytes per element) ──
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long elemDataStart = writer.BaseStream.Position;
|
||||||
|
|
||||||
|
// Backfill canvas data offset
|
||||||
|
{
|
||||||
|
long cur = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)canvasDataOffsetPos, SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)elemDataStart);
|
||||||
|
writer.Seek((int)cur, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<long> textOffsetPositions = new List<long>();
|
||||||
|
List<string> textContents = new List<string>();
|
||||||
|
|
||||||
|
for (int ei = 0; ei < elements.Length; ei++)
|
||||||
|
{
|
||||||
|
var el = elements[ei];
|
||||||
|
|
||||||
|
// Identity (8 bytes)
|
||||||
|
writer.Write((byte)el.Type);
|
||||||
|
byte eFlags = 0;
|
||||||
|
if (el.StartVisible) eFlags |= 0x01;
|
||||||
|
writer.Write(eFlags);
|
||||||
|
string eName = el.Name ?? "";
|
||||||
|
if (eName.Length > 24) eName = eName.Substring(0, 24);
|
||||||
|
writer.Write((byte)eName.Length);
|
||||||
|
writer.Write((byte)0); // pad0
|
||||||
|
long elemNameOffPos = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // nameOffset placeholder
|
||||||
|
|
||||||
|
// Layout (8 bytes)
|
||||||
|
writer.Write(el.X);
|
||||||
|
writer.Write(el.Y);
|
||||||
|
writer.Write(el.W);
|
||||||
|
writer.Write(el.H);
|
||||||
|
|
||||||
|
// Anchors (4 bytes)
|
||||||
|
writer.Write(el.AnchorMinX);
|
||||||
|
writer.Write(el.AnchorMinY);
|
||||||
|
writer.Write(el.AnchorMaxX);
|
||||||
|
writer.Write(el.AnchorMaxY);
|
||||||
|
|
||||||
|
// Primary color (4 bytes)
|
||||||
|
writer.Write(el.ColorR);
|
||||||
|
writer.Write(el.ColorG);
|
||||||
|
writer.Write(el.ColorB);
|
||||||
|
writer.Write((byte)0); // pad1
|
||||||
|
|
||||||
|
// Type-specific data (16 bytes)
|
||||||
|
switch (el.Type)
|
||||||
|
{
|
||||||
|
case PSXUIElementType.Image:
|
||||||
|
writer.Write(el.TexpageX);
|
||||||
|
writer.Write(el.TexpageY);
|
||||||
|
writer.Write(el.ClutX);
|
||||||
|
writer.Write(el.ClutY);
|
||||||
|
writer.Write(el.U0);
|
||||||
|
writer.Write(el.V0);
|
||||||
|
writer.Write(el.U1);
|
||||||
|
writer.Write(el.V1);
|
||||||
|
writer.Write(el.BitDepthIndex);
|
||||||
|
writer.Write(new byte[5]);
|
||||||
|
break;
|
||||||
|
case PSXUIElementType.Progress:
|
||||||
|
writer.Write(el.BgR);
|
||||||
|
writer.Write(el.BgG);
|
||||||
|
writer.Write(el.BgB);
|
||||||
|
writer.Write(el.ProgressValue);
|
||||||
|
writer.Write(new byte[12]);
|
||||||
|
break;
|
||||||
|
case PSXUIElementType.Text:
|
||||||
|
writer.Write(el.FontIndex);
|
||||||
|
writer.Write(new byte[15]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
writer.Write(new byte[16]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content offset (8 bytes)
|
||||||
|
long textOff = writer.BaseStream.Position;
|
||||||
|
writer.Write((uint)0); // textOffset placeholder
|
||||||
|
writer.Write((uint)0); // pad2
|
||||||
|
|
||||||
|
textOffsetPositions.Add(textOff);
|
||||||
|
textContents.Add(el.Type == PSXUIElementType.Text ? (el.DefaultText ?? "") : null);
|
||||||
|
|
||||||
|
textOffsetPositions.Add(elemNameOffPos);
|
||||||
|
textContents.Add("__NAME__" + eName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── String data (text content + element names) ──
|
||||||
|
for (int si = 0; si < textOffsetPositions.Count; si++)
|
||||||
|
{
|
||||||
|
string content = textContents[si];
|
||||||
|
if (content == null) continue;
|
||||||
|
|
||||||
|
bool isName = content.StartsWith("__NAME__");
|
||||||
|
string str = isName ? content.Substring(8) : content;
|
||||||
|
if (string.IsNullOrEmpty(str)) continue;
|
||||||
|
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long strPos = writer.BaseStream.Position;
|
||||||
|
byte[] strBytes = Encoding.UTF8.GetBytes(str);
|
||||||
|
writer.Write(strBytes);
|
||||||
|
writer.Write((byte)0); // null terminator
|
||||||
|
|
||||||
|
long cur = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)textOffsetPositions[si], SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)strPos);
|
||||||
|
writer.Seek((int)cur, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Canvas name ──
|
||||||
|
{
|
||||||
|
AlignToFourBytes(writer);
|
||||||
|
long namePos = writer.BaseStream.Position;
|
||||||
|
byte[] nameBytes = Encoding.UTF8.GetBytes(cvName);
|
||||||
|
writer.Write(nameBytes);
|
||||||
|
writer.Write((byte)0);
|
||||||
|
|
||||||
|
long cur = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)canvasNameOffsetPos, SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)namePos);
|
||||||
|
writer.Seek((int)cur, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Backfill header table offset ──
|
||||||
|
{
|
||||||
|
long cur = writer.BaseStream.Position;
|
||||||
|
writer.Seek((int)tableOffsetPos, SeekOrigin.Begin);
|
||||||
|
writer.Write((uint)uiTableStart);
|
||||||
|
writer.Seek((int)cur, SeekOrigin.Begin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke($"LoaderPackWriter: Wrote loading screen '{canvas.Name}' to {Path.GetFileName(path)}", LogType.Log);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AlignToFourBytes(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
long pos = writer.BaseStream.Position;
|
||||||
|
int pad = (int)((4 - (pos % 4)) % 4);
|
||||||
|
for (int i = 0; i < pad; i++)
|
||||||
|
writer.Write((byte)0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXLoaderPackWriter.cs.meta
Normal file
2
Runtime/PSXLoaderPackWriter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d2745d7a7b19ea046a7d10c7ec902333
|
||||||
@@ -41,6 +41,12 @@ namespace SplashEdit.RuntimeCode
|
|||||||
[Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")]
|
[Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")]
|
||||||
public PSXCutsceneClip[] Cutscenes = new PSXCutsceneClip[0];
|
public PSXCutsceneClip[] Cutscenes = new PSXCutsceneClip[0];
|
||||||
|
|
||||||
|
[Header("Loading Screen")]
|
||||||
|
[Tooltip("Optional prefab containing a PSXCanvas to use as a loading screen when loading this scene.\n" +
|
||||||
|
"The canvas may contain a PSXUIProgressBar named 'loading' which will be automatically\n" +
|
||||||
|
"updated during scene load. If null, no loading screen is shown.")]
|
||||||
|
public GameObject LoadingScreenPrefab;
|
||||||
|
|
||||||
private PSXObjectExporter[] _exporters;
|
private PSXObjectExporter[] _exporters;
|
||||||
private TextureAtlas[] _atlases;
|
private TextureAtlas[] _atlases;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class PSXUIExporter
|
public static class PSXUIExporter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Collect all PSXCanvas components and their child UI elements,
|
/// Collect all PSXCanvas components and their child UI elements,
|
||||||
/// converting RectTransform coordinates to PS1 pixel space.
|
/// converting RectTransform coordinates to PS1 pixel space.
|
||||||
/// Also collects and deduplicates custom fonts.
|
/// Also collects and deduplicates custom fonts.
|
||||||
@@ -37,6 +37,42 @@ namespace SplashEdit.RuntimeCode
|
|||||||
return new PSXCanvasData[0];
|
return new PSXCanvasData[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CollectCanvasesInternal(canvases, resolution, out fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collect a single canvas from a prefab instance for loading screen export.
|
||||||
|
/// The prefab must have a PSXCanvas on its root.
|
||||||
|
/// Note: Image elements that reference VRAM textures will NOT work in loading screens
|
||||||
|
/// since the VRAM hasn't been populated yet. Use Box, Text, and ProgressBar only.
|
||||||
|
/// </summary>
|
||||||
|
public static PSXCanvasData[] CollectCanvasFromPrefab(GameObject prefab, Vector2 resolution, out PSXFontData[] fonts)
|
||||||
|
{
|
||||||
|
if (prefab == null)
|
||||||
|
{
|
||||||
|
fonts = new PSXFontData[0];
|
||||||
|
return new PSXCanvasData[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
PSXCanvas canvas = prefab.GetComponentInChildren<PSXCanvas>(true);
|
||||||
|
if (canvas == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"PSXUIExporter: Prefab '{prefab.name}' has no PSXCanvas component.");
|
||||||
|
fonts = new PSXFontData[0];
|
||||||
|
return new PSXCanvasData[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return CollectCanvasesInternal(new[] { canvas }, resolution, out fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal shared implementation for canvas collection.
|
||||||
|
/// Works on an explicit array of PSXCanvas components.
|
||||||
|
/// </summary>
|
||||||
|
private static PSXCanvasData[] CollectCanvasesInternal(PSXCanvas[] canvases, Vector2 resolution, out PSXFontData[] fonts)
|
||||||
|
{
|
||||||
|
List<PSXFontAsset> uniqueFonts = new List<PSXFontAsset>();
|
||||||
|
|
||||||
// First pass: collect unique fonts
|
// First pass: collect unique fonts
|
||||||
foreach (PSXCanvas canvas in canvases)
|
foreach (PSXCanvas canvas in canvases)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user