Compare commits

...

19 Commits

Author SHA1 Message Date
Jan Racek
01b636f3e2 ITS BROKEN 2026-03-29 16:14:30 +02:00
Jan Racek
61fbca17a7 Lua bytecode builder, cdrom fixes 2026-03-28 19:41:53 +01:00
275b4e891d Fixed UI ordering 2026-03-28 14:35:35 +01:00
132ab479c2 Revamped psxsplash installer 2026-03-28 13:51:55 +01:00
eff03e0e1a Back color configurable, added fps counter checkbox 2026-03-28 13:31:41 +01:00
5e862f8c0b redux launch interpreted 2026-03-28 11:58:28 +01:00
62bf7d8b2d Fixed linux dependency donwloads, fixed slow download speeds 2026-03-28 10:22:10 +01:00
Jan Racek
a251eeaed5 more fixes 2026-03-28 01:32:36 +01:00
Jan Racek
13ed569eaf cleanup 2026-03-27 21:29:24 +01:00
Jan Racek
45a552be5a memory reports 2026-03-27 19:29:41 +01:00
Jan Racek
24d0c1fa07 bugfixes 2026-03-27 18:31:35 +01:00
Jan Racek
1c48b8b425 Revamped collision system 2026-03-27 16:39:42 +01:00
Jan Racek
d29ef569b3 Broken RUntime 2026-03-27 13:47:18 +01:00
Jan Racek
6bf74fa929 Fixed ui textures 2026-03-26 20:27:23 +01:00
Jan Racek
d5be174247 Broken UI and Loading screens 2026-03-26 19:14:15 +01:00
Jan Racek
a8aa674a9c Fixed up textures in UI 2026-03-26 17:27:10 +01:00
Jan Racek
5fffcea6cf Somewhat fixed ui 2026-03-25 17:14:22 +01:00
Jan Racek
8914ba35cc Broken UI system 2026-03-25 12:25:48 +01:00
Jan Racek
bb8e0804f5 Cutscene sytstem 2026-03-24 15:50:35 +01:00
149 changed files with 10309 additions and 4531 deletions

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 45b95f68e129e6f478d509d59f39bc6e
guid: d7e9b1c3e60e2ff48be3cd61902ba6f1
folderAsset: yes
DefaultImporter:
externalObjects: {}

BIN
Data/SPLASHLICENSE.DAT Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5688ba03f531c3245a838793c0ae7f93
guid: 244f6913a02805e4aa3cebdd1240cab7
DefaultImporter:
externalObjects: {}
userData:

View File

@@ -1,872 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &283344192
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 283344197}
- component: {fileID: 283344196}
- component: {fileID: 283344195}
- component: {fileID: 283344194}
- component: {fileID: 283344193}
m_Layer: 0
m_Name: f (1)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &283344193
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 283344192}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3}
m_Name:
m_EditorClassIdentifier:
IsActive: 1
bitDepth: 8
luaFile: {fileID: 0}
previewNormals: 0
normalPreviewLength: 0.5
--- !u!65 &283344194
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 283344192}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &283344195
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 283344192}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &283344196
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 283344192}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!4 &283344197
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 283344192}
serializedVersion: 2
m_LocalRotation: {x: -0.1479161, y: 0.37224695, z: -0.33835596, w: 0.85150945}
m_LocalPosition: {x: -30.19757, y: 0, z: 8.341}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 47.226, z: -43.342}
--- !u!1 &1293128684
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1293128689}
- component: {fileID: 1293128688}
- component: {fileID: 1293128687}
- component: {fileID: 1293128686}
- component: {fileID: 1293128685}
m_Layer: 0
m_Name: f
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1293128685
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1293128684}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3}
m_Name:
m_EditorClassIdentifier:
IsActive: 1
bitDepth: 8
luaFile: {fileID: 0}
previewNormals: 0
normalPreviewLength: 0.5
--- !u!65 &1293128686
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1293128684}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &1293128687
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1293128684}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &1293128688
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1293128684}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!4 &1293128689
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1293128684}
serializedVersion: 2
m_LocalRotation: {x: -0.13355339, y: 0.35044792, z: -0.3301185, w: 0.8662399}
m_LocalPosition: {x: -30.19757, y: 0, z: 10.84006}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 44.053, z: -41.723}
--- !u!1 &1331845601
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1331845603}
- component: {fileID: 1331845602}
m_Layer: 0
m_Name: Exp
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1331845602
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331845601}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ab5195ad94fd173cfb6d48ee06eaf245, type: 3}
m_Name:
m_EditorClassIdentifier:
GTEScaling: 100
SceneLuaFile: {fileID: 0}
BSPPreviewDepth: 2
--- !u!4 &1331845603
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1331845601}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -31.5414, y: 0, z: 10.89769}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1337453585
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1337453587}
- component: {fileID: 1337453586}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &1337453586
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1337453585}
m_Enabled: 1
serializedVersion: 11
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &1337453587
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1337453585}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!1 &1388445967
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1388445970}
- component: {fileID: 1388445969}
- component: {fileID: 1388445968}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &1388445968
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1388445967}
m_Enabled: 1
--- !u!20 &1388445969
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1388445967}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &1388445970
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1388445967}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1392840323
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1392840328}
- component: {fileID: 1392840327}
- component: {fileID: 1392840326}
- component: {fileID: 1392840325}
- component: {fileID: 1392840324}
m_Layer: 0
m_Name: f (2)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1392840324
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1392840323}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3}
m_Name:
m_EditorClassIdentifier:
IsActive: 1
bitDepth: 8
luaFile: {fileID: 0}
previewNormals: 0
normalPreviewLength: 0.5
--- !u!65 &1392840325
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1392840323}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &1392840326
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1392840323}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &1392840327
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1392840323}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!4 &1392840328
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1392840323}
serializedVersion: 2
m_LocalRotation: {x: -0.409346, y: -0.7736037, z: 0.22623056, w: 0.42754278}
m_LocalPosition: {x: -30.19757, y: 2.041, z: 10.84006}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: -122.144, z: 55.77}
--- !u!1 &1513869424
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1513869429}
- component: {fileID: 1513869428}
- component: {fileID: 1513869427}
- component: {fileID: 1513869426}
- component: {fileID: 1513869425}
m_Layer: 0
m_Name: f (3)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1513869425
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1513869424}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3}
m_Name:
m_EditorClassIdentifier:
IsActive: 1
bitDepth: 8
luaFile: {fileID: 0}
previewNormals: 0
normalPreviewLength: 0.5
--- !u!65 &1513869426
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1513869424}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &1513869427
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1513869424}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &1513869428
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1513869424}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!4 &1513869429
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1513869424}
serializedVersion: 2
m_LocalRotation: {x: -0.09615398, y: 0.2551484, z: 0.3392829, w: 0.9003004}
m_LocalPosition: {x: -27.156, y: 0, z: 9.065}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: -20.259, y: 24.845, z: 36.791}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 1388445970}
- {fileID: 1337453587}
- {fileID: 1293128689}
- {fileID: 283344197}
- {fileID: 1392840328}
- {fileID: 1513869429}
- {fileID: 1331845603}

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: ab8d113c2664ebf4d899b0826cd5f10c
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,197 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Downloads and manages mkpsxiso — the tool that builds PlayStation CD images
/// from an XML catalog. Used for the ISO build target.
/// https://github.com/Lameguy64/mkpsxiso
/// </summary>
public static class MkpsxisoDownloader
{
private const string MKPSXISO_VERSION = "2.20";
private const string MKPSXISO_RELEASE_BASE =
"https://github.com/Lameguy64/mkpsxiso/releases/download/v" + MKPSXISO_VERSION + "/";
private static readonly HttpClient _http = new HttpClient();
/// <summary>
/// Install directory for mkpsxiso inside .tools/
/// </summary>
public static string MkpsxisoDir =>
Path.Combine(SplashBuildPaths.ToolsDir, "mkpsxiso");
/// <summary>
/// Path to the mkpsxiso binary.
/// </summary>
public static string MkpsxisoBinary
{
get
{
if (Application.platform == RuntimePlatform.WindowsEditor)
return Path.Combine(MkpsxisoDir, "mkpsxiso.exe");
return Path.Combine(MkpsxisoDir, "bin", "mkpsxiso");
}
}
/// <summary>
/// Returns true if mkpsxiso is installed and ready to use.
/// </summary>
public static bool IsInstalled() => File.Exists(MkpsxisoBinary);
/// <summary>
/// Downloads and installs mkpsxiso from the official GitHub releases.
/// </summary>
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
{
string archiveName;
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-win64.zip";
break;
case RuntimePlatform.LinuxEditor:
archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-Linux.zip";
break;
case RuntimePlatform.OSXEditor:
archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-Darwin.zip";
break;
default:
log?.Invoke("Unsupported platform for mkpsxiso.");
return false;
}
string downloadUrl = $"{MKPSXISO_RELEASE_BASE}{archiveName}";
log?.Invoke($"Downloading mkpsxiso: {downloadUrl}");
try
{
string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
EditorUtility.DisplayProgressBar("Downloading mkpsxiso", "Downloading...", 0.1f);
using (var client = new System.Net.WebClient())
{
client.Headers.Add("User-Agent", "SplashEdit/1.0");
client.DownloadProgressChanged += (s, e) =>
{
float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading mkpsxiso", $"Downloading... {sizeMB}", progress);
};
await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
}
log?.Invoke("Extracting...");
EditorUtility.DisplayProgressBar("Installing mkpsxiso", "Extracting...", 0.9f);
string installDir = MkpsxisoDir;
if (Directory.Exists(installDir))
Directory.Delete(installDir, true);
Directory.CreateDirectory(installDir);
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
// Fix nested directory (archives often have one extra level)
SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
try { File.Delete(tempFile); } catch { }
EditorUtility.ClearProgressBar();
if (IsInstalled())
{
// Make executable on Linux
if (Application.platform != RuntimePlatform.WindowsEditor)
{
var chmod = Process.Start("chmod", $"+x \"{MkpsxisoBinary}\"");
chmod?.WaitForExit();
}
log?.Invoke("mkpsxiso installed successfully!");
return true;
}
log?.Invoke($"mkpsxiso binary not found at: {MkpsxisoBinary}");
return false;
}
catch (Exception ex)
{
log?.Invoke($"mkpsxiso download failed: {ex.Message}");
EditorUtility.ClearProgressBar();
return false;
}
}
/// <summary>
/// Runs mkpsxiso with the given XML catalog to produce a BIN/CUE image.
/// </summary>
/// <param name="xmlPath">Path to the mkpsxiso XML catalog.</param>
/// <param name="outputBin">Override output .bin path (optional, uses XML default if null).</param>
/// <param name="outputCue">Override output .cue path (optional, uses XML default if null).</param>
/// <param name="log">Logging callback.</param>
/// <returns>True if mkpsxiso succeeded.</returns>
public static bool BuildISO(string xmlPath, string outputBin = null,
string outputCue = null, Action<string> log = null)
{
if (!IsInstalled())
{
log?.Invoke("mkpsxiso is not installed.");
return false;
}
// Build arguments
string args = $"-y \"{xmlPath}\"";
if (!string.IsNullOrEmpty(outputBin))
args += $" -o \"{outputBin}\"";
if (!string.IsNullOrEmpty(outputCue))
args += $" -c \"{outputCue}\"";
log?.Invoke($"Running: mkpsxiso {args}");
var psi = new ProcessStartInfo
{
FileName = MkpsxisoBinary,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
try
{
var process = Process.Start(psi);
string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(stdout))
log?.Invoke(stdout.Trim());
if (process.ExitCode != 0)
{
if (!string.IsNullOrEmpty(stderr))
log?.Invoke($"mkpsxiso error: {stderr.Trim()}");
log?.Invoke($"mkpsxiso exited with code {process.ExitCode}");
return false;
}
log?.Invoke("ISO image built successfully.");
return true;
}
catch (Exception ex)
{
log?.Invoke($"mkpsxiso execution failed: {ex.Message}");
return false;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 45aea686b641c474dba05b83956d8947

View File

@@ -94,34 +94,20 @@ namespace SplashEdit.EditorCode
// Step 3: Download the file
string tempFile = Path.Combine(Path.GetTempPath(), $"pcsx-redux-{latestBuildId}.zip");
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", "Downloading...", 0.1f);
using (var response = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead))
using (var client = new System.Net.WebClient())
{
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
long downloadedBytes = 0;
client.Headers.Add("User-Agent", "SplashEdit/1.0");
using (var fileStream = File.Create(tempFile))
using (var downloadStream = await response.Content.ReadAsStreamAsync())
client.DownloadProgressChanged += (s, e) =>
{
byte[] buffer = new byte[81920];
int bytesRead;
while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", $"Downloading... {sizeMB}", progress);
};
if (totalBytes.HasValue)
{
float progress = (float)downloadedBytes / totalBytes.Value;
string sizeMB = $"{downloadedBytes / (1024 * 1024)}/{totalBytes.Value / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux",
$"Downloading... {sizeMB}", progress);
}
}
}
await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
}
log?.Invoke($"Downloaded to {tempFile}");
@@ -144,6 +130,7 @@ namespace SplashEdit.EditorCode
};
var proc = Process.Start(psi);
proc?.WaitForExit();
}
else
{
@@ -151,6 +138,20 @@ namespace SplashEdit.EditorCode
log?.Invoke($"Extracted to {installDir}");
}
// Make executable
if(Application.platform == RuntimePlatform.LinuxEditor) {
var psi = new ProcessStartInfo
{
FileName = "chmod",
Arguments = $"+x \"{SplashBuildPaths.PCSXReduxBinary}\"",
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(psi);
proc?.WaitForExit();
}
// Clean up temp file
try { File.Delete(tempFile); } catch { }
@@ -164,7 +165,7 @@ namespace SplashEdit.EditorCode
else
{
// The zip might have a nested directory — try to find the exe
FixNestedDirectory(installDir);
SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
if (SplashBuildPaths.IsPCSXReduxInstalled())
{
log?.Invoke("PCSX-Redux installed successfully!");
@@ -187,29 +188,6 @@ namespace SplashEdit.EditorCode
}
}
/// <summary>
/// If the zip extracts into a nested directory, move files up.
/// </summary>
private static void FixNestedDirectory(string installDir)
{
var subdirs = Directory.GetDirectories(installDir);
if (subdirs.Length == 1)
{
string nested = subdirs[0];
foreach (string file in Directory.GetFiles(nested))
{
string dest = Path.Combine(installDir, Path.GetFileName(file));
File.Move(file, dest);
}
foreach (string dir in Directory.GetDirectories(nested))
{
string dest = Path.Combine(installDir, Path.GetFileName(dir));
Directory.Move(dir, dest);
}
try { Directory.Delete(nested); } catch { }
}
}
/// <summary>
/// Parse the latest build ID from the master manifest JSON.
/// Expected format: {"builds":[{"id":1234,...},...],...}

View File

@@ -60,6 +60,7 @@ namespace SplashEdit.EditorCode
private const int FUNC_SEEK = 0x107;
public bool IsRunning => _listenTask != null && !_listenTask.IsCompleted;
public bool HasError { get; private set; }
public PCdrvSerialHost(string portName, int baudRate, string baseDir, Action<string> log, Action<string> psxLog = null)
{
@@ -157,6 +158,7 @@ namespace SplashEdit.EditorCode
bool lastByteWasEscape = false;
var textBuffer = new StringBuilder();
int totalBytesReceived = 0;
int consecutiveErrors = 0;
DateTime lastLogTime = DateTime.Now;
_log?.Invoke("PCdrv monitor: waiting for data from PS1...");
@@ -179,6 +181,7 @@ namespace SplashEdit.EditorCode
}
int b = _port.ReadByte();
consecutiveErrors = 0;
totalBytesReceived++;
// Log first bytes received to help diagnose protocol issues
@@ -256,8 +259,16 @@ namespace SplashEdit.EditorCode
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
if (!ct.IsCancellationRequested)
_log?.Invoke($"PCdrv monitor error: {ex.Message}");
if (ct.IsCancellationRequested) break;
consecutiveErrors++;
_log?.Invoke($"PCdrv monitor error: {ex.Message}");
if (consecutiveErrors >= 3)
{
_log?.Invoke("PCdrv host: too many errors, connection lost. Stopping.");
HasError = true;
break;
}
Thread.Sleep(100); // Back off before retry
}
}
}

View File

@@ -52,17 +52,14 @@ namespace SplashEdit.EditorCode
/// </summary>
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
{
string platformSuffix;
string archiveName;
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
platformSuffix = "x86_64-pc-windows-msvc";
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.zip";
archiveName = $"psxavenc-windows.zip";
break;
case RuntimePlatform.LinuxEditor:
platformSuffix = "x86_64-unknown-linux-gnu";
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.tar.gz";
archiveName = $"psxavenc-linux.zip";
break;
default:
log?.Invoke("Only Windows and Linux are supported.");
@@ -77,29 +74,18 @@ namespace SplashEdit.EditorCode
string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
EditorUtility.DisplayProgressBar("Downloading psxavenc", "Downloading...", 0.1f);
using (var response = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead))
using (var client = new System.Net.WebClient())
{
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
long downloaded = 0;
client.Headers.Add("User-Agent", "SplashEdit/1.0");
using (var fs = File.Create(tempFile))
using (var stream = await response.Content.ReadAsStreamAsync())
client.DownloadProgressChanged += (s, e) =>
{
byte[] buffer = new byte[81920];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fs.WriteAsync(buffer, 0, bytesRead);
downloaded += bytesRead;
if (totalBytes.HasValue)
{
float progress = (float)downloaded / totalBytes.Value;
EditorUtility.DisplayProgressBar("Downloading psxavenc",
$"{downloaded / 1024}/{totalBytes.Value / 1024} KB", progress);
}
}
}
float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading psxavenc", $"Downloading... {sizeMB}", progress);
};
await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
}
log?.Invoke("Extracting...");
@@ -129,7 +115,7 @@ namespace SplashEdit.EditorCode
}
// Fix nested directory (sometimes archives have one extra level)
FixNestedDirectory(installDir);
SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
try { File.Delete(tempFile); } catch { }
@@ -158,27 +144,6 @@ namespace SplashEdit.EditorCode
}
}
private static void FixNestedDirectory(string dir)
{
// If extraction created exactly one subdirectory, flatten it
var subdirs = Directory.GetDirectories(dir);
if (subdirs.Length == 1)
{
string nested = subdirs[0];
foreach (string file in Directory.GetFiles(nested))
{
string dest = Path.Combine(dir, Path.GetFileName(file));
if (!File.Exists(dest)) File.Move(file, dest);
}
foreach (string sub in Directory.GetDirectories(nested))
{
string dest = Path.Combine(dir, Path.GetFileName(sub));
if (!Directory.Exists(dest)) Directory.Move(sub, dest);
}
try { Directory.Delete(nested, true); } catch { }
}
}
/// <summary>
/// Converts a Unity AudioClip to PS1 SPU ADPCM format using psxavenc.
/// Returns the ADPCM byte array, or null on failure.

View File

@@ -247,9 +247,25 @@ namespace SplashEdit.EditorCode
return tex;
}
// Snapshot taken at the start of each OnGUI so Layout and Repaint
// events always see the same line count (prevents "Getting control
// position in a group with only N controls" errors).
private LogLine[] _snapshot = Array.Empty<LogLine>();
private void OnGUI()
{
EnsureStyles();
// Take a snapshot once per OnGUI so Layout and Repaint see
// identical control counts even if background threads add lines.
if (Event.current.type == EventType.Layout)
{
lock (_lock)
{
_snapshot = _lines.ToArray();
}
}
DrawToolbar();
DrawConsoleOutput();
}
@@ -310,69 +326,70 @@ namespace SplashEdit.EditorCode
int selMax = Mathf.Max(_selectionAnchor, _selectionEnd);
bool hasSelection = _selectionAnchor >= 0 && _selectionEnd >= 0;
lock (_lock)
// Iterate the snapshot taken during Layout so the control count
// is stable across Layout and Repaint events.
var snapshot = _snapshot;
if (snapshot.Length == 0)
{
if (_lines.Count == 0)
GUILayout.Label("Waiting for output...", EditorStyles.centeredGreyMiniLabel);
}
for (int i = 0; i < snapshot.Length; i++)
{
var line = snapshot[i];
if (line.isError && !_showStderr) continue;
if (!line.isError && !_showStdout) continue;
if (hasFilter && line.text.ToLowerInvariant().IndexOf(filterLower, StringComparison.Ordinal) < 0)
continue;
bool selected = hasSelection && i >= selMin && i <= selMax;
GUIStyle style = selected ? _monoStyleSelected : (line.isError ? _monoStyleErr : _monoStyle);
string label = $"[{line.timestamp}] {line.text}";
GUILayout.Label(label, style);
// Handle click/right-click on last drawn rect
Rect lineRect = GUILayoutUtility.GetLastRect();
Event evt = Event.current;
if (evt.type == EventType.MouseDown && lineRect.Contains(evt.mousePosition))
{
GUILayout.Label("Waiting for output...", EditorStyles.centeredGreyMiniLabel);
}
for (int i = 0; i < _lines.Count; i++)
{
var line = _lines[i];
if (line.isError && !_showStderr) continue;
if (!line.isError && !_showStdout) continue;
if (hasFilter && line.text.ToLowerInvariant().IndexOf(filterLower, StringComparison.Ordinal) < 0)
continue;
bool selected = hasSelection && i >= selMin && i <= selMax;
GUIStyle style = selected ? _monoStyleSelected : (line.isError ? _monoStyleErr : _monoStyle);
string label = $"[{line.timestamp}] {line.text}";
GUILayout.Label(label, style);
// Handle click/right-click on last drawn rect
Rect lineRect = GUILayoutUtility.GetLastRect();
Event evt = Event.current;
if (evt.type == EventType.MouseDown && lineRect.Contains(evt.mousePosition))
if (evt.button == 0)
{
if (evt.button == 0)
if (evt.shift && _selectionAnchor >= 0)
_selectionEnd = i;
else
{
if (evt.shift && _selectionAnchor >= 0)
_selectionEnd = i;
else
{
_selectionAnchor = i;
_selectionEnd = i;
}
evt.Use();
Repaint();
_selectionAnchor = i;
_selectionEnd = i;
}
else if (evt.button == 1)
evt.Use();
Repaint();
}
else if (evt.button == 1)
{
int clickedLine = i;
bool lineInSelection = hasSelection && clickedLine >= selMin && clickedLine <= selMax;
var menu = new GenericMenu();
if (lineInSelection && selMin != selMax)
{
int clickedLine = i;
bool lineInSelection = hasSelection && clickedLine >= selMin && clickedLine <= selMax;
var menu = new GenericMenu();
if (lineInSelection && selMin != selMax)
{
menu.AddItem(new GUIContent("Copy selected lines"), false, () => CopyRange(selMin, selMax));
menu.AddSeparator("");
}
menu.AddItem(new GUIContent("Copy this line"), false, () =>
{
string text;
lock (_lock)
{
text = clickedLine < _lines.Count
? $"[{_lines[clickedLine].timestamp}] {_lines[clickedLine].text}"
: "";
}
EditorGUIUtility.systemCopyBuffer = text;
});
menu.ShowAsContext();
evt.Use();
menu.AddItem(new GUIContent("Copy selected lines"), false, () => CopyRange(selMin, selMax));
menu.AddSeparator("");
}
menu.AddItem(new GUIContent("Copy this line"), false, () =>
{
string text;
lock (_lock)
{
text = clickedLine < _lines.Count
? $"[{_lines[clickedLine].timestamp}] {_lines[clickedLine].text}"
: "";
}
EditorGUIUtility.systemCopyBuffer = text;
});
menu.ShowAsContext();
evt.Use();
}
}
}

View File

@@ -0,0 +1,188 @@
using System;
using System.IO;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Memory analysis report for a single exported scene.
/// All values are in bytes unless noted otherwise.
/// </summary>
[Serializable]
public class SceneMemoryReport
{
public string sceneName;
// ─── Main RAM ───
public long splashpackFileSize; // Total file on disc
public long splashpackLiveSize; // Bytes kept in RAM at runtime (before bulk data freed)
public int triangleCount;
public int gameObjectCount;
// ─── VRAM (1024 x 512 x 2 = 1,048,576 bytes) ───
public long framebufferSize; // 2 x W x H x 2
public long textureAtlasSize; // Sum of atlas pixel data
public long clutSize; // Sum of CLUT entries x 2
public long fontVramSize; // Custom font textures
public int atlasCount;
public int clutCount;
// ─── SPU RAM (512KB, 0x1010 reserved) ───
public long audioDataSize;
public int audioClipCount;
// ─── CD Storage ───
public long loaderPackSize;
// ─── Constants ───
public const long TOTAL_RAM = 2 * 1024 * 1024;
public const long KERNEL_RESERVED = 0x10000; // 64KB kernel area
public const long USABLE_RAM = TOTAL_RAM - KERNEL_RESERVED;
public const long TOTAL_VRAM = 1024 * 512 * 2; // 1MB
public const long TOTAL_SPU = 512 * 1024;
public const long SPU_RESERVED = 0x1010;
public const long USABLE_SPU = TOTAL_SPU - SPU_RESERVED;
// Fixed runtime overhead from C++ (renderer.hh constants, now configurable)
public static long BUMP_ALLOC_TOTAL => 2L * SplashSettings.BumpSize;
public static long OT_TOTAL => 2L * SplashSettings.OtSize * 4;
public const long VIS_REFS = 4096 * 4; // 16KB
public const long STACK_ESTIMATE = 32 * 1024; // 32KB
public const long LUA_OVERHEAD = 16 * 1024; // 16KB approximate
public const long SYSTEM_FONT_VRAM = 4 * 1024; // ~4KB
public long FixedOverhead => BUMP_ALLOC_TOTAL + OT_TOTAL + VIS_REFS + STACK_ESTIMATE + LUA_OVERHEAD;
// Heap estimate and warnings
public long EstimatedHeapFree => USABLE_RAM - TotalRamUsage;
public bool IsHeapWarning => EstimatedHeapFree < 128 * 1024; // < 128KB free
public bool IsHeapCritical => EstimatedHeapFree < 64 * 1024; // < 64KB free
/// <summary>RAM used by scene data (live portion of splashpack).</summary>
public long SceneRamUsage => splashpackLiveSize > 0 ? splashpackLiveSize : splashpackFileSize;
/// <summary>Total estimated RAM: fixed overhead + scene data. Does NOT include code/BSS.</summary>
public long TotalRamUsage => FixedOverhead + SceneRamUsage;
public long TotalVramUsed => framebufferSize + textureAtlasSize + clutSize + fontVramSize + SYSTEM_FONT_VRAM;
public long TotalSpuUsed => audioDataSize;
public long TotalDiscSize => splashpackFileSize + loaderPackSize;
public float RamPercent => Mathf.Clamp01((float)TotalRamUsage / USABLE_RAM) * 100f;
public float VramPercent => Mathf.Clamp01((float)TotalVramUsed / TOTAL_VRAM) * 100f;
public float SpuPercent => USABLE_SPU > 0 ? Mathf.Clamp01((float)TotalSpuUsed / USABLE_SPU) * 100f : 0f;
public long RamFree => USABLE_RAM - TotalRamUsage;
public long VramFree => TOTAL_VRAM - TotalVramUsed;
public long SpuFree => USABLE_SPU - TotalSpuUsed;
}
/// <summary>
/// Builds a SceneMemoryReport by reading the exported splashpack binary header
/// and the scene's VRAM/audio data.
/// </summary>
public static class SceneMemoryAnalyzer
{
/// <summary>
/// Analyze an exported scene. Call after ExportToPath().
/// </summary>
/// <param name="sceneName">Display name for the scene.</param>
/// <param name="splashpackPath">Path to the exported .splashpack file.</param>
/// <param name="loaderPackPath">Path to the loading screen file (may be null).</param>
/// <param name="atlases">Texture atlases from the export pipeline.</param>
/// <param name="audioExportSizes">Array of ADPCM byte sizes per audio clip.</param>
/// <param name="fonts">Custom font descriptors.</param>
public static SceneMemoryReport Analyze(
string sceneName,
string splashpackPath,
string loaderPackPath,
SplashEdit.RuntimeCode.TextureAtlas[] atlases,
long[] audioExportSizes,
SplashEdit.RuntimeCode.PSXFontData[] fonts,
int triangleCount = 0)
{
var r = new SceneMemoryReport { sceneName = sceneName };
// ── File sizes ──
if (File.Exists(splashpackPath))
r.splashpackFileSize = new FileInfo(splashpackPath).Length;
if (!string.IsNullOrEmpty(loaderPackPath) && File.Exists(loaderPackPath))
r.loaderPackSize = new FileInfo(loaderPackPath).Length;
r.triangleCount = triangleCount;
// ── Parse splashpack header for counts and pixelDataOffset ──
if (File.Exists(splashpackPath))
{
try { ReadHeader(splashpackPath, r); }
catch (Exception e) { Debug.LogWarning($"Memory report: failed to read header: {e.Message}"); }
}
// ── Framebuffers ──
int fbW = SplashSettings.ResolutionWidth;
int fbH = SplashSettings.ResolutionHeight;
int fbCount = SplashSettings.DualBuffering ? 2 : 1;
r.framebufferSize = fbW * fbH * 2L * fbCount;
// ── VRAM: Texture atlases + CLUTs ──
if (atlases != null)
{
r.atlasCount = atlases.Length;
foreach (var atlas in atlases)
{
r.textureAtlasSize += atlas.Width * SplashEdit.RuntimeCode.TextureAtlas.Height * 2L;
foreach (var tex in atlas.ContainedTextures)
{
if (tex.ColorPalette != null)
{
r.clutCount++;
r.clutSize += tex.ColorPalette.Count * 2L;
}
}
}
}
// ── VRAM: Custom fonts ──
if (fonts != null)
{
foreach (var font in fonts)
{
if (font.TextureHeight > 0)
r.fontVramSize += 64L * font.TextureHeight * 2; // 4bpp = 64 hwords wide
}
}
// ── SPU: Audio ──
if (audioExportSizes != null)
{
r.audioClipCount = audioExportSizes.Length;
foreach (long sz in audioExportSizes)
r.audioDataSize += sz;
}
return r;
}
private static void ReadHeader(string path, SceneMemoryReport r)
{
using (var reader = new BinaryReader(File.OpenRead(path)))
{
if (reader.BaseStream.Length < 104) return;
// Magic + version (4 bytes)
reader.ReadBytes(4);
// luaFileCount(2) + gameObjectCount(2) + textureAtlasCount(2) + clutCount(2)
reader.ReadUInt16(); // luaFileCount
r.gameObjectCount = reader.ReadUInt16();
reader.ReadUInt16(); // textureAtlasCount
reader.ReadUInt16(); // clutCount
// Skip to pixelDataOffset at byte 100
reader.BaseStream.Seek(100, SeekOrigin.Begin);
uint pixelDataOffset = reader.ReadUInt32();
r.splashpackLiveSize = pixelDataOffset > 0 ? pixelDataOffset : r.splashpackFileSize;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f68ba273eb88c3b4796e43f40b226c71

View File

@@ -41,7 +41,7 @@ namespace SplashEdit.EditorCode
case RuntimePlatform.WindowsEditor:
return Path.Combine(PCSXReduxDir, "pcsx-redux.exe");
case RuntimePlatform.LinuxEditor:
return Path.Combine(ToolsDir, "PCSX-Redux-HEAD-x86_64.AppImage");
return Path.Combine(PCSXReduxDir, "PCSX-Redux-HEAD-x86_64.AppImage");
default:
return Path.Combine(PCSXReduxDir, "pcsx-redux");
}
@@ -109,6 +109,22 @@ namespace SplashEdit.EditorCode
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.splashpack");
}
/// <summary>
/// Default license file path (SPLASHLICENSE.DAT) shipped in the package Data folder.
/// Resolved relative to the Unity project so it works on any machine.
/// </summary>
public static string DefaultLicenseFilePath =>
Path.GetFullPath(Path.Combine("Packages", "net.psxsplash.splashedit", "Data", "SPLASHLICENSE.DAT"));
/// <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>
/// ISO output path for release builds.
/// </summary>
@@ -121,6 +137,24 @@ namespace SplashEdit.EditorCode
public static string CUEOutputPath =>
Path.Combine(BuildOutputDir, "psxsplash.cue");
/// <summary>
/// XML catalog path used by mkpsxiso to build the ISO image.
/// </summary>
public static string ISOCatalogPath =>
Path.Combine(BuildOutputDir, "psxsplash.xml");
/// <summary>
/// SYSTEM.CNF file path generated for the ISO image.
/// The PS1 BIOS reads this to find and launch the executable.
/// </summary>
public static string SystemCnfPath =>
Path.Combine(BuildOutputDir, "SYSTEM.CNF");
/// <summary>
/// Checks if mkpsxiso is installed in the tools directory.
/// </summary>
public static bool IsMkpsxisoInstalled() => MkpsxisoDownloader.IsInstalled();
/// <summary>
/// Ensures the build output and tools directories exist.
/// Also appends entries to the project .gitignore if not present.
@@ -132,6 +166,45 @@ namespace SplashEdit.EditorCode
EnsureGitIgnore();
}
// ───── Lua bytecode compilation paths ─────
/// <summary>
/// Directory for Lua source files extracted during export.
/// </summary>
public static string LuaSrcDir =>
Path.Combine(BuildOutputDir, "lua_src");
/// <summary>
/// Directory for compiled Lua bytecode files.
/// </summary>
public static string LuaCompiledDir =>
Path.Combine(BuildOutputDir, "lua_compiled");
/// <summary>
/// Manifest file listing input/output pairs for the PS1 Lua compiler.
/// </summary>
public static string LuaManifestPath =>
Path.Combine(LuaSrcDir, "manifest.txt");
/// <summary>
/// Sentinel file written by luac_psx when compilation is complete.
/// Contains "OK" on success or "ERROR" on failure.
/// </summary>
public static string LuaDoneSentinel =>
Path.Combine(LuaSrcDir, "__done__");
/// <summary>
/// Path to the luac_psx PS1 compiler executable (built from tools/luac_psx/).
/// </summary>
public static string LuacPsxExePath =>
Path.Combine(NativeSourceDir, "tools", "luac_psx", "luac_psx.ps-exe");
/// <summary>
/// Path to the luac_psx tools directory (for building the compiler).
/// </summary>
public static string LuacPsxDir =>
Path.Combine(NativeSourceDir, "tools", "luac_psx");
/// <summary>
/// Checks if PCSX-Redux is installed in the tools directory.
/// </summary>

File diff suppressed because it is too large Load Diff

View File

@@ -92,29 +92,74 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetInt(Prefix + "SerialBaudRate", value);
}
// --- VRAM Layout ---
// --- VRAM Layout (hardcoded 320x240, dual-buffered, vertical) ---
public static int ResolutionWidth
{
get => EditorPrefs.GetInt(Prefix + "ResWidth", 320);
set => EditorPrefs.SetInt(Prefix + "ResWidth", value);
get => 320;
set { } // no-op, hardcoded
}
public static int ResolutionHeight
{
get => EditorPrefs.GetInt(Prefix + "ResHeight", 240);
set => EditorPrefs.SetInt(Prefix + "ResHeight", value);
get => 240;
set { } // no-op, hardcoded
}
public static bool DualBuffering
{
get => EditorPrefs.GetBool(Prefix + "DualBuffering", true);
set => EditorPrefs.SetBool(Prefix + "DualBuffering", value);
get => true;
set { } // no-op, hardcoded
}
public static bool VerticalLayout
{
get => EditorPrefs.GetBool(Prefix + "VerticalLayout", true);
set => EditorPrefs.SetBool(Prefix + "VerticalLayout", value);
get => true;
set { } // no-op, hardcoded
}
// --- Clean Build ---
public static bool CleanBuild
{
get => EditorPrefs.GetBool(Prefix + "CleanBuild", true);
set => EditorPrefs.SetBool(Prefix + "CleanBuild", value);
}
// --- Memory Overlay ---
/// <summary>
/// When enabled, compiles the runtime with a heap/RAM usage progress bar
/// and text overlay at the top-right corner of the screen.
/// Passes MEMOVERLAY=1 to the native Makefile.
/// </summary>
public static bool MemoryOverlay
{
get => EditorPrefs.GetBool(Prefix + "MemoryOverlay", false);
set => EditorPrefs.SetBool(Prefix + "MemoryOverlay", value);
}
// --- FPS Overlay ---
/// <summary>
/// When enabled, compiles the runtime with an FPS counter
/// and text overlay at the top-left corner of the screen.
/// Passes FPSOVERLAY=1 to the native Makefile.
/// </summary>
public static bool FpsOverlay
{
get => EditorPrefs.GetBool(Prefix + "FpsOverlay", false);
set => EditorPrefs.SetBool(Prefix + "FpsOverlay", value);
}
// --- Renderer sizes ---
public static int OtSize
{
get => EditorPrefs.GetInt(Prefix + "OtSize", 2048 * 4);
set => EditorPrefs.SetInt(Prefix + "OtSize", value);
}
public static int BumpSize
{
get => EditorPrefs.GetInt(Prefix + "BumpSize", 8096 * 16);
set => EditorPrefs.SetInt(Prefix + "BumpSize", value);
}
// --- Export settings ---
@@ -124,17 +169,25 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", value);
}
public static bool AutoValidateOnExport
// --- ISO Build ---
/// <summary>
/// Optional path to a Sony license file (.dat) for the ISO image.
/// If empty, the ISO will be built without license data (homebrew-only).
/// The file must be in raw 2336-byte sector format (from PsyQ SDK LCNSFILE).
/// </summary>
public static string LicenseFilePath
{
get => EditorPrefs.GetBool(Prefix + "AutoValidate", true);
set => EditorPrefs.SetBool(Prefix + "AutoValidate", value);
get => EditorPrefs.GetString(Prefix + "LicenseFilePath", SplashBuildPaths.DefaultLicenseFilePath);
set => EditorPrefs.SetString(Prefix + "LicenseFilePath", value);
}
// --- Play Mode Intercept ---
public static bool InterceptPlayMode
/// <summary>
/// Volume label for the ISO image (up to 31 characters, uppercase).
/// </summary>
public static string ISOVolumeLabel
{
get => EditorPrefs.GetBool(Prefix + "InterceptPlayMode", false);
set => EditorPrefs.SetBool(Prefix + "InterceptPlayMode", value);
get => EditorPrefs.GetString(Prefix + "ISOVolumeLabel", "PSXSPLASH");
set => EditorPrefs.SetString(Prefix + "ISOVolumeLabel", value);
}
/// <summary>
@@ -147,7 +200,9 @@ namespace SplashEdit.EditorCode
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
"GTEScaling", "AutoValidate", "InterceptPlayMode"
"GTEScaling", "AutoValidate",
"LicenseFilePath", "ISOVolumeLabel",
"OtSize", "BumpSize"
};
foreach (string key in keys)

View File

@@ -18,8 +18,8 @@ namespace SplashEdit.EditorCode
private SerializedProperty _isRepeatable;
private SerializedProperty _cooldownFrames;
private SerializedProperty _showPrompt;
private SerializedProperty _promptCanvasName;
private SerializedProperty _requireLineOfSight;
private SerializedProperty _interactionOffset;
private static readonly string[] ButtonNames =
{
@@ -34,23 +34,31 @@ namespace SplashEdit.EditorCode
_isRepeatable = serializedObject.FindProperty("isRepeatable");
_cooldownFrames = serializedObject.FindProperty("cooldownFrames");
_showPrompt = serializedObject.FindProperty("showPrompt");
_promptCanvasName = serializedObject.FindProperty("promptCanvasName");
_requireLineOfSight = serializedObject.FindProperty("requireLineOfSight");
_interactionOffset = serializedObject.FindProperty("interactionOffset");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawHeader();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.BeginHorizontal();
GUILayout.Label(EditorGUIUtility.IconContent("d_Selectable Icon"), GUILayout.Width(30), GUILayout.Height(30));
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("PSX Interactable", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.LabelField("Player interaction trigger for PS1", PSXEditorStyles.RichLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
PSXEditorStyles.EndCard();
EditorGUILayout.Space(5);
EditorGUILayout.Space(4);
_interactionFoldout = DrawFoldoutSection("Interaction Settings", _interactionFoldout, () =>
_interactionFoldout = PSXEditorStyles.DrawFoldoutCard("Interaction Settings", _interactionFoldout, () =>
{
EditorGUILayout.PropertyField(_interactionRadius);
// Button selector with visual
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Interact Button");
_interactButton.intValue = EditorGUILayout.Popup(_interactButton.intValue, ButtonNames);
@@ -63,74 +71,52 @@ namespace SplashEdit.EditorCode
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_cooldownFrames, new GUIContent("Cooldown (frames)"));
// Show cooldown in seconds
float seconds = _cooldownFrames.intValue / 60f;
EditorGUILayout.LabelField($" {seconds:F2} seconds at 60fps", EditorStyles.miniLabel);
EditorGUILayout.LabelField($"~ {seconds:F2} seconds at 60fps", EditorStyles.miniLabel);
EditorGUI.indentLevel--;
}
EditorGUILayout.PropertyField(_showPrompt);
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(_showPrompt, new GUIContent("Show Prompt Canvas"));
if (_showPrompt.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_promptCanvasName, new GUIContent("Canvas Name"));
if (string.IsNullOrEmpty(_promptCanvasName.stringValue))
{
EditorGUILayout.HelpBox(
"Enter the name of a PSXCanvas that will be shown when the player is in range and hidden when they leave.",
MessageType.Info);
}
if (_promptCanvasName.stringValue != null && _promptCanvasName.stringValue.Length > 15)
{
EditorGUILayout.HelpBox("Canvas name is limited to 15 characters.", MessageType.Warning);
}
EditorGUI.indentLevel--;
}
});
_advancedFoldout = DrawFoldoutSection("Advanced", _advancedFoldout, () =>
EditorGUILayout.Space(2);
_advancedFoldout = PSXEditorStyles.DrawFoldoutCard("Advanced", _advancedFoldout, () =>
{
EditorGUILayout.PropertyField(_requireLineOfSight);
EditorGUILayout.PropertyField(_interactionOffset);
EditorGUILayout.PropertyField(_requireLineOfSight,
new GUIContent("Require Facing",
"Player must be facing the object to interact. Uses a forward-direction check."));
});
DrawLuaEventsInfo(new[] { "onInteract" });
EditorGUILayout.Space(4);
// Lua events card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Lua Events", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.LabelField("onInteract", PSXEditorStyles.RichLabel);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
GUILayout.Label(EditorGUIUtility.IconContent("d_Selectable Icon"), GUILayout.Width(30), GUILayout.Height(30));
EditorGUILayout.BeginVertical();
GUILayout.Label("PSX Interactable", EditorStyles.boldLabel);
GUILayout.Label("Player interaction trigger for PS1", EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
private bool DrawFoldoutSection(string title, bool isExpanded, System.Action drawContent)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
isExpanded = EditorGUILayout.Foldout(isExpanded, title, true, EditorStyles.foldoutHeader);
if (isExpanded)
{
EditorGUI.indentLevel++;
drawContent?.Invoke();
EditorGUI.indentLevel--;
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(3);
return isExpanded;
}
private void DrawLuaEventsInfo(string[] events)
{
EditorGUILayout.Space(5);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Lua Events", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
foreach (var evt in events)
{
GUILayout.Label($"• {evt}", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
}
}

View File

@@ -0,0 +1,271 @@
// I raged that my scrollwheel was broken while writing this and that's why it's 2 files.
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXAudioClip component.
/// </summary>
[CustomEditor(typeof(PSXAudioClip))]
public class PSXAudioClipEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Audio Clip", PSXEditorStyles.CardHeaderStyle);
PSXAudioClip audioClip = (PSXAudioClip)target;
EditorGUILayout.BeginHorizontal();
if (audioClip.Clip != null)
PSXEditorStyles.DrawStatusBadge("Clip Set", PSXEditorStyles.Success, 70);
else
PSXEditorStyles.DrawStatusBadge("No Clip", PSXEditorStyles.Warning, 70);
if (audioClip.Loop)
PSXEditorStyles.DrawStatusBadge("Loop", PSXEditorStyles.AccentCyan, 50);
EditorGUILayout.EndHorizontal();
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Clip Settings", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("ClipName"), new GUIContent("Clip Name",
"Name used to identify this clip in Lua (Audio.Play(\"name\"))."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Clip"), new GUIContent("Audio Clip",
"Unity AudioClip to convert to PS1 SPU ADPCM format."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("SampleRate"), new GUIContent("Sample Rate",
"Target sample rate for the PS1 (lower = smaller, max 44100)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Loop"), new GUIContent("Loop",
"Whether this clip should loop when played."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("DefaultVolume"), new GUIContent("Volume",
"Default playback volume (0-127)."));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Info card
if (audioClip.Clip != null)
{
PSXEditorStyles.BeginCard();
float duration = audioClip.Clip.length;
int srcRate = audioClip.Clip.frequency;
EditorGUILayout.LabelField(
$"Source: {srcRate} Hz, {duration:F2}s, {audioClip.Clip.channels}ch\n" +
$"Target: {audioClip.SampleRate} Hz SPU ADPCM",
PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXPlayer component.
/// </summary>
[CustomEditor(typeof(PSXPlayer))]
public class PSXPlayerEditor : Editor
{
private bool _dimensionsFoldout = true;
private bool _movementFoldout = true;
private bool _navigationFoldout = true;
private bool _physicsFoldout = true;
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Player", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.LabelField("First-person player controller for PS1", PSXEditorStyles.RichLabel);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Dimensions
_dimensionsFoldout = PSXEditorStyles.DrawFoldoutCard("Player Dimensions", _dimensionsFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("playerHeight"), new GUIContent("Height",
"Camera eye height above the player's feet."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("playerRadius"), new GUIContent("Radius",
"Collision radius for wall sliding."));
});
EditorGUILayout.Space(2);
// Movement
_movementFoldout = PSXEditorStyles.DrawFoldoutCard("Movement", _movementFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("moveSpeed"), new GUIContent("Walk Speed",
"Walk speed in world units per second."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("sprintSpeed"), new GUIContent("Sprint Speed",
"Sprint speed in world units per second."));
});
EditorGUILayout.Space(2);
// Navigation
_navigationFoldout = PSXEditorStyles.DrawFoldoutCard("Navigation", _navigationFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("maxStepHeight"), new GUIContent("Max Step Height",
"Maximum height the agent can step up."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("walkableSlopeAngle"), new GUIContent("Walkable Slope",
"Maximum walkable slope angle in degrees."));
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("navCellSize"), new GUIContent("Cell Size (XZ)",
"Voxel size in XZ plane (smaller = more accurate but slower)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("navCellHeight"), new GUIContent("Cell Height",
"Voxel height (smaller = more accurate vertical resolution)."));
});
EditorGUILayout.Space(2);
// Jump & Gravity
_physicsFoldout = PSXEditorStyles.DrawFoldoutCard("Jump & Gravity", _physicsFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("jumpHeight"), new GUIContent("Jump Height",
"Peak jump height in world units."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("gravity"), new GUIContent("Gravity",
"Downward acceleration in world units per second squared."));
});
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXPortalLink component.
/// </summary>
[CustomEditor(typeof(PSXPortalLink))]
public class PSXPortalLinkEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
PSXPortalLink portal = (PSXPortalLink)target;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Portal Link", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.BeginHorizontal();
bool valid = portal.RoomA != null && portal.RoomB != null && portal.RoomA != portal.RoomB;
if (valid)
PSXEditorStyles.DrawStatusBadge("Valid", PSXEditorStyles.Success, 55);
else
PSXEditorStyles.DrawStatusBadge("Invalid", PSXEditorStyles.Error, 60);
EditorGUILayout.EndHorizontal();
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Room references card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Connected Rooms", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("RoomA"), new GUIContent("Room A",
"First room connected by this portal."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("RoomB"), new GUIContent("Room B",
"Second room connected by this portal."));
// Validation warnings
if (portal.RoomA == null || portal.RoomB == null)
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Both Room A and Room B must be assigned for export.", PSXEditorStyles.InfoBox);
}
else if (portal.RoomA == portal.RoomB)
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Room A and Room B must be different rooms.", PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Portal size card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Portal Dimensions", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("PortalSize"), new GUIContent("Size (W, H)",
"Size of the portal opening (width, height) in world units."));
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXRoom component.
/// </summary>
[CustomEditor(typeof(PSXRoom))]
public class PSXRoomEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
PSXRoom room = (PSXRoom)target;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Room", PSXEditorStyles.CardHeaderStyle);
if (!string.IsNullOrEmpty(room.RoomName))
EditorGUILayout.LabelField(room.RoomName, PSXEditorStyles.RichLabel);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Room Settings", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("RoomName"), new GUIContent("Room Name",
"Optional display name for this room (used in editor gizmos)."));
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("VolumeSize"), new GUIContent("Volume Size",
"Size of the room volume in local space."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("VolumeOffset"), new GUIContent("Volume Offset",
"Offset of the volume center relative to the transform position."));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Info card
PSXEditorStyles.BeginCard();
Bounds wb = room.GetWorldBounds();
Vector3 size = wb.size;
EditorGUILayout.LabelField(
$"World bounds: {size.x:F1} x {size.y:F1} x {size.z:F1}",
PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3fd7a7bcc7d0ff841b158f2744d48010

View File

@@ -0,0 +1,617 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
// --- Scene Preview Gizmos ---
// A single canvas-level gizmo draws all children in hierarchy order
// so depth stacking is correct (last child in hierarchy renders on top).
public static class PSXUIGizmos
{
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
static void DrawCanvasGizmo(PSXCanvas canvas, GizmoType gizmoType)
{
RectTransform canvasRt = canvas.GetComponent<RectTransform>();
if (canvasRt == null) return;
bool canvasSelected = (gizmoType & GizmoType.Selected) != 0;
// Canvas border
Vector3[] canvasCorners = new Vector3[4];
canvasRt.GetWorldCorners(canvasCorners);
Color border = canvasSelected ? Color.yellow : new Color(1, 1, 0, 0.3f);
Handles.DrawSolidRectangleWithOutline(canvasCorners, Color.clear, border);
// Draw all children in hierarchy order (first child = back, last child = front)
var children = canvas.GetComponentsInChildren<Transform>(true).Reverse();
foreach (var child in children)
{
if (child == canvas.transform) continue;
bool childSelected = Selection.Contains(child.gameObject);
var box = child.GetComponent<PSXUIBox>();
if (box != null) { DrawBox(box, childSelected); continue; }
var image = child.GetComponent<PSXUIImage>();
if (image != null) { DrawImage(image, childSelected); continue; }
var text = child.GetComponent<PSXUIText>();
if (text != null) { DrawText(text, childSelected); continue; }
var bar = child.GetComponent<PSXUIProgressBar>();
if (bar != null) { DrawProgressBar(bar, childSelected); continue; }
}
// Canvas label when selected
if (canvasSelected)
{
Vector2 res = PSXCanvas.PSXResolution;
Vector3 topMid = (canvasCorners[1] + canvasCorners[2]) * 0.5f;
string label = $"PSX Canvas: {canvas.CanvasName} ({res.x}x{res.y})";
GUIStyle style = new GUIStyle(EditorStyles.boldLabel);
style.normal.textColor = Color.yellow;
Handles.Label(topMid, label, style);
}
}
static void DrawBox(PSXUIBox box, bool selected)
{
RectTransform rt = box.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Color fill = box.BoxColor;
fill.a = selected ? 1f : 0.9f;
Color borderColor = selected ? Color.white : new Color(1, 1, 1, 0.5f);
Handles.DrawSolidRectangleWithOutline(corners, fill, borderColor);
}
static void DrawImage(PSXUIImage image, bool selected)
{
RectTransform rt = image.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
if (image.SourceTexture != null)
{
Color tint = image.TintColor;
tint.a = selected ? 1f : 0.9f;
Handles.DrawSolidRectangleWithOutline(corners, tint * 0.3f, tint);
Handles.BeginGUI();
Vector2 min = HandleUtility.WorldToGUIPoint(corners[0]);
Vector2 max = HandleUtility.WorldToGUIPoint(corners[2]);
Rect screenRect = new Rect(
Mathf.Min(min.x, max.x), Mathf.Min(min.y, max.y),
Mathf.Abs(max.x - min.x), Mathf.Abs(max.y - min.y));
if (screenRect.width > 2 && screenRect.height > 2)
{
GUI.color = new Color(tint.r, tint.g, tint.b, selected ? 1f : 0.9f);
GUI.DrawTexture(screenRect, image.SourceTexture, ScaleMode.StretchToFill);
GUI.color = Color.white;
}
Handles.EndGUI();
}
else
{
Color fill = new Color(0.4f, 0.4f, 0.8f, selected ? 0.8f : 0.6f);
Handles.DrawSolidRectangleWithOutline(corners, fill, Color.cyan);
}
}
static void DrawText(PSXUIText text, bool selected)
{
RectTransform rt = text.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Color borderColor = text.TextColor;
borderColor.a = selected ? 1f : 0.7f;
Color fill = new Color(0, 0, 0, selected ? 0.6f : 0.4f);
Handles.DrawSolidRectangleWithOutline(corners, fill, borderColor);
string label = string.IsNullOrEmpty(text.DefaultText) ? "[empty]" : text.DefaultText;
PSXFontAsset font = text.GetEffectiveFont();
int glyphW = font != null ? font.GlyphWidth : 8;
int glyphH = font != null ? font.GlyphHeight : 16;
Handles.BeginGUI();
Vector2 topLeft = HandleUtility.WorldToGUIPoint(corners[1]);
Vector2 botRight = HandleUtility.WorldToGUIPoint(corners[3]);
float rectScreenW = Mathf.Abs(botRight.x - topLeft.x);
float rectW = rt.rect.width;
float psxPixelScale = (rectW > 0.01f) ? rectScreenW / rectW : 1f;
float guiX = Mathf.Min(topLeft.x, botRight.x);
float guiY = Mathf.Min(topLeft.y, botRight.y);
float guiW = Mathf.Abs(botRight.x - topLeft.x);
float guiH = Mathf.Abs(botRight.y - topLeft.y);
Color tintColor = text.TextColor;
tintColor.a = selected ? 1f : 0.8f;
if (font != null && font.FontTexture != null && font.SourceFont != null)
{
Texture2D fontTex = font.FontTexture;
int glyphsPerRow = font.GlyphsPerRow;
float cellScreenH = glyphH * psxPixelScale;
float cursorX = guiX;
GUI.color = tintColor;
foreach (char ch in label)
{
if (ch < 32 || ch > 126) continue;
int charIdx = ch - 32;
int col = charIdx % glyphsPerRow;
int row = charIdx / glyphsPerRow;
float advance = glyphW;
if (font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length)
advance = font.AdvanceWidths[charIdx];
if (ch != ' ')
{
float uvX = (float)(col * glyphW) / fontTex.width;
float uvY = 1f - (float)((row + 1) * glyphH) / fontTex.height;
float uvW = (float)glyphW / fontTex.width;
float uvH = (float)glyphH / fontTex.height;
float spriteScreenW = advance * psxPixelScale;
Rect screenRect = new Rect(cursorX, guiY, spriteScreenW, cellScreenH);
float uvWScaled = uvW * (advance / glyphW);
Rect uvRect = new Rect(uvX, uvY, uvWScaled, uvH);
if (screenRect.xMax > guiX && screenRect.x < guiX + guiW)
GUI.DrawTextureWithTexCoords(screenRect, fontTex, uvRect);
}
cursorX += advance * psxPixelScale;
}
GUI.color = Color.white;
}
else
{
int fSize = Mathf.Clamp(Mathf.RoundToInt(glyphH * psxPixelScale * 0.75f), 6, 72);
GUIStyle style = new GUIStyle(EditorStyles.label);
style.normal.textColor = tintColor;
style.alignment = TextAnchor.UpperLeft;
style.fontSize = fSize;
style.wordWrap = false;
style.clipping = TextClipping.Clip;
Rect guiRect = new Rect(guiX, guiY, guiW, guiH);
GUI.color = tintColor;
GUI.Label(guiRect, label, style);
GUI.color = Color.white;
}
Handles.EndGUI();
}
static void DrawProgressBar(PSXUIProgressBar bar, bool selected)
{
RectTransform rt = bar.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Color bgColor = bar.BackgroundColor;
bgColor.a = selected ? 1f : 0.9f;
Handles.DrawSolidRectangleWithOutline(corners, bgColor, selected ? Color.white : new Color(1, 1, 1, 0.5f));
float t = bar.InitialValue / 100f;
if (t > 0.001f)
{
Vector3[] fillCorners = new Vector3[4];
fillCorners[0] = corners[0];
fillCorners[1] = corners[1];
fillCorners[2] = Vector3.Lerp(corners[1], corners[2], t);
fillCorners[3] = Vector3.Lerp(corners[0], corners[3], t);
Color fillColor = bar.FillColor;
fillColor.a = selected ? 1f : 0.9f;
Handles.DrawSolidRectangleWithOutline(fillCorners, fillColor, Color.clear);
}
}
}
/// <summary>
/// Custom inspector for PSXCanvas component.
/// Shows canvas name, visibility, sort order, font, and a summary of child elements.
/// </summary>
[CustomEditor(typeof(PSXCanvas))]
public class PSXCanvasEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
Vector2 res = PSXCanvas.PSXResolution;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField($"PSX Canvas ({res.x}x{res.y})", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("canvasName"), new GUIContent("Canvas Name",
"Name used from Lua: UI.FindCanvas(\"name\"). Max 24 chars."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"), new GUIContent("Start Visible",
"Whether the canvas is visible when the scene loads."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("sortOrder"), new GUIContent("Sort Order",
"Render priority (0 = back, 255 = front)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("defaultFont"), new GUIContent("Default Font",
"Default custom font for text elements. If empty, uses built-in system font (8x16)."));
PSXEditorStyles.DrawSeparator(6, 6);
if (GUILayout.Button($"Reset Canvas to {res.x}x{res.y}", PSXEditorStyles.SecondaryButton))
{
PSXCanvas.InvalidateResolutionCache();
((PSXCanvas)target).ConfigureCanvas();
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Element summary card
PSXCanvas canvas = (PSXCanvas)target;
int imageCount = canvas.GetComponentsInChildren<PSXUIImage>(true).Length;
int boxCount = canvas.GetComponentsInChildren<PSXUIBox>(true).Length;
int textCount = canvas.GetComponentsInChildren<PSXUIText>(true).Length;
int progressCount = canvas.GetComponentsInChildren<PSXUIProgressBar>(true).Length;
int total = imageCount + boxCount + textCount + progressCount;
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField(
$"Elements: {total} total\n" +
$" Images: {imageCount} | Boxes: {boxCount}\n" +
$" Texts: {textCount} | Progress Bars: {progressCount}",
PSXEditorStyles.InfoBox);
if (total > 128)
EditorGUILayout.LabelField("PS1 UI system supports max 128 elements total across all canvases.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIImage component.
/// </summary>
[CustomEditor(typeof(PSXUIImage))]
public class PSXUIImageEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Image", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"), new GUIContent("Element Name",
"Name used from Lua: UI.FindElement(canvas, \"name\"). Max 24 chars."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("sourceTexture"), new GUIContent("Source Texture",
"Texture to quantize and pack into VRAM."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("bitDepth"), new GUIContent("Bit Depth",
"VRAM storage depth. 4-bit = 16 colors, 8-bit = 256 colors, 16-bit = direct color."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("tintColor"), new GUIContent("Tint Color",
"Color multiply applied to the image (white = no tint)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.EndCard();
// Texture size warning
PSXUIImage img = (PSXUIImage)target;
if (img.SourceTexture != null)
{
if (img.SourceTexture.width > 256 || img.SourceTexture.height > 256)
{
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Texture exceeds 256x256. It will be resized during export.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
}
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIBox component.
/// </summary>
[CustomEditor(typeof(PSXUIBox))]
public class PSXUIBoxEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Box", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("boxColor"), new GUIContent("Box Color",
"Solid fill color rendered as a FastFill primitive."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIText component.
/// </summary>
[CustomEditor(typeof(PSXUIText))]
public class PSXUITextEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Text", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("defaultText"), new GUIContent("Default Text",
"Initial text content. Max 63 chars. Change at runtime via UI.SetText()."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("textColor"), new GUIContent("Text Color",
"Text render color."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontOverride"), new GUIContent("Font Override",
"Custom font for this text element. If empty, uses the canvas default font or built-in system font (8x16)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Warnings and info
PSXUIText txt = (PSXUIText)target;
if (!string.IsNullOrEmpty(txt.DefaultText) && txt.DefaultText.Length > 63)
{
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Text exceeds 63 characters and will be truncated.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
}
PSXEditorStyles.BeginCard();
PSXFontAsset font = txt.GetEffectiveFont();
if (font != null)
{
EditorGUILayout.LabelField(
$"Font: {font.name} ({font.GlyphWidth}x{font.GlyphHeight} glyphs)",
PSXEditorStyles.InfoBox);
}
else
{
EditorGUILayout.LabelField("Using built-in system font (8x16 glyphs).", PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIProgressBar component.
/// </summary>
[CustomEditor(typeof(PSXUIProgressBar))]
public class PSXUIProgressBarEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Progress Bar", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("backgroundColor"), new GUIContent("Background Color",
"Color shown behind the fill bar."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("fillColor"), new GUIContent("Fill Color",
"Color of the progress fill."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("initialValue"), new GUIContent("Initial Value",
"Starting progress (0-100). Change via UI.SetProgress()."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.DrawSeparator(6, 4);
// Preview bar
PSXUIProgressBar bar = (PSXUIProgressBar)target;
PSXEditorStyles.DrawProgressBar(bar.InitialValue / 100f, "Preview", bar.FillColor, 16);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXFontAsset ScriptableObject.
/// Shows font metrics, auto-conversion from TTF/OTF, and a preview of the glyph layout.
/// </summary>
[CustomEditor(typeof(PSXFontAsset))]
public class PSXFontAssetEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
PSXFontAsset font = (PSXFontAsset)target;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Font Asset", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Source font card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Auto-Convert from Font", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("sourceFont"), new GUIContent("Source Font (TTF/OTF)",
"Assign a Unity Font (TrueType/OpenType). Click 'Generate Bitmap' to rasterize it.\n" +
"Glyph cell dimensions are auto-derived from the font metrics."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontSize"), new GUIContent("Font Size",
"Pixel height for rasterization. Determines glyph cell height.\n" +
"Glyph cell width is auto-derived from the widest character.\n" +
"Changing this and re-generating will update both the bitmap AND the glyph dimensions."));
if (font.SourceFont != null)
{
EditorGUILayout.Space(2);
if (GUILayout.Button("Generate Bitmap from Font", PSXEditorStyles.PrimaryButton, GUILayout.Height(28)))
{
Undo.RecordObject(font, "Generate PSX Font Bitmap");
font.GenerateBitmapFromFont();
}
if (font.FontTexture == null)
EditorGUILayout.LabelField(
"Click 'Generate Bitmap' to create the font texture.\n" +
"If generation fails, check that the font's import settings have " +
"'Character' set to 'ASCII Default Set'.", PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Manual bitmap card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Manual Bitmap Source", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontTexture"), new GUIContent("Font Texture",
"256px wide bitmap. Glyphs in ASCII order from 0x20 (space). " +
"Transparent = background, opaque = foreground."));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Glyph metrics card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Glyph Metrics", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
if (font.SourceFont != null && font.FontTexture != null)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.IntField(new GUIContent("Glyph Width", "Auto-derived from font. Re-generate to change."), font.GlyphWidth);
EditorGUILayout.IntField(new GUIContent("Glyph Height", "Auto-derived from font. Re-generate to change."), font.GlyphHeight);
EditorGUI.EndDisabledGroup();
EditorGUILayout.LabelField("Glyph dimensions are auto-derived when generating from a font.\n" +
"Change the Font Size slider and re-generate to adjust.", PSXEditorStyles.InfoBox);
}
else
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphWidth"), new GUIContent("Glyph Width",
"Width of each glyph cell in pixels. Must divide 256 evenly (4, 8, 16, or 32)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphHeight"), new GUIContent("Glyph Height",
"Height of each glyph cell in pixels."));
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Layout info card
PSXEditorStyles.BeginCard();
int glyphsPerRow = font.GlyphsPerRow;
int rowCount = font.RowCount;
int totalH = font.TextureHeight;
int vramBytes = totalH * 128;
EditorGUILayout.LabelField(
$"Layout: {glyphsPerRow} glyphs/row, {rowCount} rows\n" +
$"Texture: 256 x {totalH} pixels (4bpp)\n" +
$"VRAM: {vramBytes} bytes ({vramBytes / 1024f:F1} KB)\n" +
$"Glyph cell: {font.GlyphWidth} x {font.GlyphHeight}",
PSXEditorStyles.InfoBox);
if (font.AdvanceWidths != null && font.AdvanceWidths.Length >= 95)
{
int minAdv = 255, maxAdv = 0;
for (int i = 1; i < 95; i++)
{
if (font.AdvanceWidths[i] < minAdv) minAdv = font.AdvanceWidths[i];
if (font.AdvanceWidths[i] > maxAdv) maxAdv = font.AdvanceWidths[i];
}
EditorGUILayout.LabelField(
$"Advance widths: {minAdv}-{maxAdv}px (proportional spacing stored)",
PSXEditorStyles.InfoBox);
}
else if (font.FontTexture != null)
{
EditorGUILayout.LabelField(
"No advance widths stored. Click 'Generate Bitmap' to compute them.",
PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
// Validation
if (font.FontTexture != null)
{
if (font.FontTexture.width != 256)
{
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField($"Font texture must be 256 pixels wide (currently {font.FontTexture.width}).", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
if (256 % font.GlyphWidth != 0)
{
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField($"Glyph width ({font.GlyphWidth}) must divide 256 evenly. " +
"Valid values: 4, 8, 16, 32.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
// Preview
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Preview", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
Rect previewRect = EditorGUILayout.GetControlRect(false, 64);
GUI.DrawTexture(previewRect, font.FontTexture, ScaleMode.ScaleToFit);
PSXEditorStyles.EndCard();
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 385b3916e29dc0e48b2866851d1fc1a9

789
Editor/PSXCutsceneEditor.cs Normal file
View File

@@ -0,0 +1,789 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
[CustomEditor(typeof(PSXCutsceneClip))]
public class PSXCutsceneEditor : Editor
{
// ── Preview state ──
private bool _showAudioEvents = true;
private bool _previewing;
private bool _playing;
private float _previewFrame;
private double _playStartEditorTime;
private float _playStartFrame;
private HashSet<int> _firedAudioEventIndices = new HashSet<int>();
// Saved scene-view state so we can restore after preview
private bool _hasSavedSceneView;
private Vector3 _savedPivot;
private Quaternion _savedRotation;
private float _savedSize;
// Saved object transforms
private Dictionary<string, Vector3> _savedObjectPositions = new Dictionary<string, Vector3>();
private Dictionary<string, Quaternion> _savedObjectRotations = new Dictionary<string, Quaternion>();
private Dictionary<string, bool> _savedObjectActive = new Dictionary<string, bool>();
// Audio preview
private Dictionary<string, AudioClip> _audioClipCache = new Dictionary<string, AudioClip>();
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
if (_previewing) StopPreview();
}
private void OnEditorUpdate()
{
if (!_playing) return;
PSXCutsceneClip clip = (PSXCutsceneClip)target;
double elapsed = EditorApplication.timeSinceStartup - _playStartEditorTime;
_previewFrame = _playStartFrame + (float)(elapsed * 30.0);
if (_previewFrame >= clip.DurationFrames)
{
_previewFrame = clip.DurationFrames;
_playing = false;
}
ApplyPreview(clip);
Repaint();
}
public override void OnInspectorGUI()
{
PSXCutsceneClip clip = (PSXCutsceneClip)target;
Undo.RecordObject(clip, "Edit Cutscene");
// ── Header ──
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Cutscene Settings", EditorStyles.boldLabel);
clip.CutsceneName = EditorGUILayout.TextField("Cutscene Name", clip.CutsceneName);
if (!string.IsNullOrEmpty(clip.CutsceneName) && clip.CutsceneName.Length > 24)
EditorGUILayout.HelpBox("Name exceeds 24 characters and will be truncated on export.", MessageType.Warning);
clip.DurationFrames = EditorGUILayout.IntField("Duration (frames)", clip.DurationFrames);
if (clip.DurationFrames < 1) clip.DurationFrames = 1;
float seconds = clip.DurationFrames / 30f;
EditorGUILayout.LabelField($" = {seconds:F2} seconds at 30fps", EditorStyles.miniLabel);
// ── Preview Controls ──
EditorGUILayout.Space(6);
DrawPreviewControls(clip);
// Collect scene references for validation
var exporterNames = new HashSet<string>();
var audioNames = new HashSet<string>();
var canvasNames = new HashSet<string>();
var elementNames = new Dictionary<string, HashSet<string>>(); // canvas → element names
var exporters = Object.FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
foreach (var e in exporters)
exporterNames.Add(e.gameObject.name);
var audioSources = Object.FindObjectsByType<PSXAudioClip>(FindObjectsSortMode.None);
foreach (var a in audioSources)
if (!string.IsNullOrEmpty(a.ClipName))
audioNames.Add(a.ClipName);
var canvases = Object.FindObjectsByType<PSXCanvas>(FindObjectsSortMode.None);
foreach (var c in canvases)
{
string cName = c.CanvasName ?? "";
if (!string.IsNullOrEmpty(cName))
{
canvasNames.Add(cName);
if (!elementNames.ContainsKey(cName))
elementNames[cName] = new HashSet<string>();
// Gather all UI element names under this canvas
foreach (var box in c.GetComponentsInChildren<PSXUIBox>())
if (!string.IsNullOrEmpty(box.ElementName)) elementNames[cName].Add(box.ElementName);
foreach (var txt in c.GetComponentsInChildren<PSXUIText>())
if (!string.IsNullOrEmpty(txt.ElementName)) elementNames[cName].Add(txt.ElementName);
foreach (var bar in c.GetComponentsInChildren<PSXUIProgressBar>())
if (!string.IsNullOrEmpty(bar.ElementName)) elementNames[cName].Add(bar.ElementName);
foreach (var img in c.GetComponentsInChildren<PSXUIImage>())
if (!string.IsNullOrEmpty(img.ElementName)) elementNames[cName].Add(img.ElementName);
}
}
// ── Tracks ──
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Tracks", EditorStyles.boldLabel);
if (clip.Tracks == null) clip.Tracks = new List<PSXCutsceneTrack>();
int removeTrackIdx = -1;
for (int ti = 0; ti < clip.Tracks.Count; ti++)
{
var track = clip.Tracks[ti];
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
track.TrackType = (PSXTrackType)EditorGUILayout.EnumPopup("Type", track.TrackType);
if (GUILayout.Button("Remove", GUILayout.Width(65)))
removeTrackIdx = ti;
EditorGUILayout.EndHorizontal();
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
bool isUITrack = track.IsUITrack;
bool isUIElementTrack = track.IsUIElementTrack;
if (isCameraTrack)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextField("Target", "(camera)");
EditorGUI.EndDisabledGroup();
}
else if (isUITrack)
{
track.UICanvasName = EditorGUILayout.TextField("Canvas Name", track.UICanvasName);
if (!string.IsNullOrEmpty(track.UICanvasName) && !canvasNames.Contains(track.UICanvasName))
EditorGUILayout.HelpBox($"No PSXCanvas with name '{track.UICanvasName}' in scene.", MessageType.Error);
if (isUIElementTrack)
{
track.UIElementName = EditorGUILayout.TextField("Element Name", track.UIElementName);
if (!string.IsNullOrEmpty(track.UICanvasName) && !string.IsNullOrEmpty(track.UIElementName))
{
if (elementNames.TryGetValue(track.UICanvasName, out var elNames) && !elNames.Contains(track.UIElementName))
EditorGUILayout.HelpBox($"No UI element '{track.UIElementName}' found under canvas '{track.UICanvasName}'.", MessageType.Error);
}
}
}
else
{
track.ObjectName = EditorGUILayout.TextField("Object Name", track.ObjectName);
// Validation
if (!string.IsNullOrEmpty(track.ObjectName) && !exporterNames.Contains(track.ObjectName))
EditorGUILayout.HelpBox($"No PSXObjectExporter found for '{track.ObjectName}' in scene.", MessageType.Error);
}
// ── Keyframes ──
if (track.Keyframes == null) track.Keyframes = new List<PSXKeyframe>();
EditorGUI.indentLevel++;
EditorGUILayout.LabelField($"Keyframes ({track.Keyframes.Count})", EditorStyles.miniLabel);
int removeKfIdx = -1;
for (int ki = 0; ki < track.Keyframes.Count; ki++)
{
var kf = track.Keyframes[ki];
// Row 1: frame number + interp mode + buttons
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Frame", GUILayout.Width(42));
kf.Frame = EditorGUILayout.IntField(kf.Frame, GUILayout.Width(60));
kf.Interp = (PSXInterpMode)EditorGUILayout.EnumPopup(kf.Interp, GUILayout.Width(80));
GUILayout.FlexibleSpace();
// Capture from scene
if (isCameraTrack)
{
if (GUILayout.Button("Capture Cam", GUILayout.Width(90)))
{
var sv = SceneView.lastActiveSceneView;
if (sv != null)
kf.Value = track.TrackType == PSXTrackType.CameraPosition
? sv.camera.transform.position : sv.camera.transform.eulerAngles;
else Debug.LogWarning("No active Scene View.");
}
}
else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotation))
{
// Capture from the named object in scene
if (!string.IsNullOrEmpty(track.ObjectName) && GUILayout.Button("From Object", GUILayout.Width(85)))
{
var go = GameObject.Find(track.ObjectName);
if (go != null)
kf.Value = track.TrackType == PSXTrackType.ObjectPosition
? go.transform.position : go.transform.eulerAngles;
else Debug.LogWarning($"Object '{track.ObjectName}' not found in scene.");
}
}
if (GUILayout.Button("\u2212", GUILayout.Width(22)))
removeKfIdx = ki;
EditorGUILayout.EndHorizontal();
// Row 2: value on its own line
EditorGUI.indentLevel++;
switch (track.TrackType)
{
case PSXTrackType.ObjectActive:
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
{
string label = track.TrackType == PSXTrackType.ObjectActive ? "Active" : "Visible";
bool active = EditorGUILayout.Toggle(label, kf.Value.x > 0.5f);
kf.Value = new Vector3(active ? 1f : 0f, 0, 0);
break;
}
case PSXTrackType.ObjectRotation:
case PSXTrackType.CameraRotation:
{
kf.Value = EditorGUILayout.Vector3Field("Rotation\u00b0", kf.Value);
break;
}
case PSXTrackType.UIProgress:
{
float progress = EditorGUILayout.Slider("Progress %", kf.Value.x, 0f, 100f);
kf.Value = new Vector3(progress, 0, 0);
break;
}
case PSXTrackType.UIPosition:
{
Vector2 pos = EditorGUILayout.Vector2Field("Position (px)", new Vector2(kf.Value.x, kf.Value.y));
kf.Value = new Vector3(pos.x, pos.y, 0);
break;
}
case PSXTrackType.UIColor:
{
// Show as RGB 0-255 integers
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("R", GUILayout.Width(14));
float r = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 255), GUILayout.Width(40));
EditorGUILayout.LabelField("G", GUILayout.Width(14));
float g = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.y), 0, 255), GUILayout.Width(40));
EditorGUILayout.LabelField("B", GUILayout.Width(14));
float b = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.z), 0, 255), GUILayout.Width(40));
EditorGUILayout.EndHorizontal();
kf.Value = new Vector3(r, g, b);
break;
}
default:
kf.Value = EditorGUILayout.Vector3Field("Value", kf.Value);
break;
}
EditorGUI.indentLevel--;
if (ki < track.Keyframes.Count - 1)
{
EditorGUILayout.Space(1);
var rect = EditorGUILayout.GetControlRect(false, 1);
EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f));
}
}
if (removeKfIdx >= 0) track.Keyframes.RemoveAt(removeKfIdx);
// Add keyframe buttons
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Keyframe", GUILayout.Width(90)))
{
int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0;
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = Vector3.zero });
}
if (isCameraTrack)
{
if (GUILayout.Button("+ from Scene Cam", GUILayout.Width(130)))
{
var sv = SceneView.lastActiveSceneView;
Vector3 val = Vector3.zero;
if (sv != null)
val = track.TrackType == PSXTrackType.CameraPosition
? sv.camera.transform.position : sv.camera.transform.eulerAngles;
int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0;
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val });
}
}
else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotation))
{
if (!string.IsNullOrEmpty(track.ObjectName))
{
if (GUILayout.Button("+ from Object", GUILayout.Width(110)))
{
var go = GameObject.Find(track.ObjectName);
Vector3 val = Vector3.zero;
if (go != null)
val = track.TrackType == PSXTrackType.ObjectPosition
? go.transform.position : go.transform.eulerAngles;
int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0;
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val });
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUI.indentLevel--;
EditorGUILayout.EndVertical();
EditorGUILayout.Space(2);
}
if (removeTrackIdx >= 0) clip.Tracks.RemoveAt(removeTrackIdx);
if (clip.Tracks.Count < 8)
{
if (GUILayout.Button("+ Add Track"))
clip.Tracks.Add(new PSXCutsceneTrack());
}
else
{
EditorGUILayout.HelpBox("Maximum 8 tracks per cutscene.", MessageType.Info);
}
// ── Audio Events ──
EditorGUILayout.Space(8);
_showAudioEvents = EditorGUILayout.Foldout(_showAudioEvents, "Audio Events", true);
if (_showAudioEvents)
{
if (clip.AudioEvents == null) clip.AudioEvents = new List<PSXAudioEvent>();
int removeEventIdx = -1;
for (int ei = 0; ei < clip.AudioEvents.Count; ei++)
{
var evt = clip.AudioEvents[ei];
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Frame", GUILayout.Width(42));
evt.Frame = EditorGUILayout.IntField(evt.Frame, GUILayout.Width(60));
GUILayout.FlexibleSpace();
if (GUILayout.Button("\u2212", GUILayout.Width(22)))
removeEventIdx = ei;
EditorGUILayout.EndHorizontal();
evt.ClipName = EditorGUILayout.TextField("Clip Name", evt.ClipName);
if (!string.IsNullOrEmpty(evt.ClipName) && !audioNames.Contains(evt.ClipName))
EditorGUILayout.HelpBox($"No PSXAudioClip with ClipName '{evt.ClipName}' in scene.", MessageType.Error);
evt.Volume = EditorGUILayout.IntSlider("Volume", evt.Volume, 0, 128);
evt.Pan = EditorGUILayout.IntSlider("Pan", evt.Pan, 0, 127);
EditorGUILayout.EndVertical();
}
if (removeEventIdx >= 0) clip.AudioEvents.RemoveAt(removeEventIdx);
if (clip.AudioEvents.Count < 64)
{
if (GUILayout.Button("+ Add Audio Event"))
clip.AudioEvents.Add(new PSXAudioEvent());
}
else
{
EditorGUILayout.HelpBox("Maximum 64 audio events per cutscene.", MessageType.Info);
}
}
if (GUI.changed)
EditorUtility.SetDirty(clip);
}
// =====================================================================
// Preview Controls
// =====================================================================
private void DrawPreviewControls(PSXCutsceneClip clip)
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
// Transport bar
EditorGUILayout.BeginHorizontal();
bool wasPlaying = _playing;
if (_playing)
{
if (GUILayout.Button("\u275A\u275A Pause", GUILayout.Width(70)))
_playing = false;
}
else
{
if (GUILayout.Button("\u25B6 Play", GUILayout.Width(70)))
{
if (!_previewing) StartPreview(clip);
_playing = true;
_playStartEditorTime = EditorApplication.timeSinceStartup;
_playStartFrame = _previewFrame;
_firedAudioEventIndices.Clear();
// Mark already-passed events so they won't fire again
if (clip.AudioEvents != null)
for (int i = 0; i < clip.AudioEvents.Count; i++)
if (clip.AudioEvents[i].Frame < (int)_previewFrame)
_firedAudioEventIndices.Add(i);
}
}
if (GUILayout.Button("\u25A0 Stop", GUILayout.Width(60)))
{
_playing = false;
_previewFrame = 0;
if (_previewing) StopPreview();
}
if (_previewing)
{
GUI.color = new Color(1f, 0.4f, 0.4f);
if (GUILayout.Button("End Preview", GUILayout.Width(90)))
{
_playing = false;
StopPreview();
}
GUI.color = Color.white;
}
EditorGUILayout.EndHorizontal();
// Timeline scrubber
EditorGUI.BeginChangeCheck();
float newFrame = EditorGUILayout.Slider("Frame", _previewFrame, 0, clip.DurationFrames);
if (EditorGUI.EndChangeCheck())
{
if (!_previewing) StartPreview(clip);
_previewFrame = newFrame;
_playing = false;
_firedAudioEventIndices.Clear();
ApplyPreview(clip);
}
float previewSec = _previewFrame / 30f;
EditorGUILayout.LabelField(
$" {(int)_previewFrame} / {clip.DurationFrames} ({previewSec:F2}s / {seconds(clip):F2}s)",
EditorStyles.miniLabel);
if (_previewing)
EditorGUILayout.HelpBox(
"PREVIEWING: Scene View camera & objects are being driven. " +
"Click \u201cEnd Preview\u201d or \u201cStop\u201d to restore original positions.",
MessageType.Warning);
EditorGUILayout.EndVertical();
}
private static float seconds(PSXCutsceneClip clip) => clip.DurationFrames / 30f;
// =====================================================================
// Preview Lifecycle
// =====================================================================
private void StartPreview(PSXCutsceneClip clip)
{
if (_previewing) return;
_previewing = true;
_firedAudioEventIndices.Clear();
// Save scene view camera
var sv = SceneView.lastActiveSceneView;
if (sv != null)
{
_hasSavedSceneView = true;
_savedPivot = sv.pivot;
_savedRotation = sv.rotation;
_savedSize = sv.size;
}
// Save object transforms
_savedObjectPositions.Clear();
_savedObjectRotations.Clear();
_savedObjectActive.Clear();
if (clip.Tracks != null)
{
foreach (var track in clip.Tracks)
{
if (string.IsNullOrEmpty(track.ObjectName)) continue;
bool isCam = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
if (isCam) continue;
var go = GameObject.Find(track.ObjectName);
if (go == null) continue;
if (!_savedObjectPositions.ContainsKey(track.ObjectName))
{
_savedObjectPositions[track.ObjectName] = go.transform.position;
_savedObjectRotations[track.ObjectName] = go.transform.rotation;
_savedObjectActive[track.ObjectName] = go.activeSelf;
}
}
}
// Build audio clip lookup
_audioClipCache.Clear();
var audioSources = Object.FindObjectsByType<PSXAudioClip>(FindObjectsSortMode.None);
foreach (var a in audioSources)
if (!string.IsNullOrEmpty(a.ClipName) && a.Clip != null)
_audioClipCache[a.ClipName] = a.Clip;
}
private void StopPreview()
{
if (!_previewing) return;
_previewing = false;
_playing = false;
// Restore scene view camera
if (_hasSavedSceneView)
{
var sv = SceneView.lastActiveSceneView;
if (sv != null)
{
sv.pivot = _savedPivot;
sv.rotation = _savedRotation;
sv.size = _savedSize;
sv.Repaint();
}
_hasSavedSceneView = false;
}
// Restore object transforms
foreach (var kvp in _savedObjectPositions)
{
var go = GameObject.Find(kvp.Key);
if (go == null) continue;
go.transform.position = kvp.Value;
if (_savedObjectRotations.ContainsKey(kvp.Key))
go.transform.rotation = _savedObjectRotations[kvp.Key];
if (_savedObjectActive.ContainsKey(kvp.Key))
go.SetActive(_savedObjectActive[kvp.Key]);
}
_savedObjectPositions.Clear();
_savedObjectRotations.Clear();
_savedObjectActive.Clear();
SceneView.RepaintAll();
Repaint();
}
// =====================================================================
// Apply Preview at Current Frame
// =====================================================================
private void ApplyPreview(PSXCutsceneClip clip)
{
if (!_previewing) return;
float frame = _previewFrame;
var sv = SceneView.lastActiveSceneView;
Vector3? camPos = null;
Quaternion? camRot = null;
if (clip.Tracks != null)
{
foreach (var track in clip.Tracks)
{
// Compute initial value for pre-first-keyframe blending
Vector3 initialVal = Vector3.zero;
switch (track.TrackType)
{
case PSXTrackType.CameraPosition:
if (sv != null)
// Recover position from saved pivot/rotation/size
initialVal = _savedPivot - _savedRotation * Vector3.forward * _savedSize;
break;
case PSXTrackType.CameraRotation:
initialVal = _savedRotation.eulerAngles;
break;
case PSXTrackType.ObjectPosition:
if (_savedObjectPositions.ContainsKey(track.ObjectName ?? ""))
initialVal = _savedObjectPositions[track.ObjectName];
break;
case PSXTrackType.ObjectRotation:
if (_savedObjectRotations.ContainsKey(track.ObjectName ?? ""))
initialVal = _savedObjectRotations[track.ObjectName].eulerAngles;
break;
case PSXTrackType.ObjectActive:
if (_savedObjectActive.ContainsKey(track.ObjectName ?? ""))
initialVal = new Vector3(_savedObjectActive[track.ObjectName] ? 1f : 0f, 0, 0);
break;
// UI tracks: initial values stay zero (no scene preview state to capture)
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
initialVal = new Vector3(1f, 0, 0); // assume visible by default
break;
case PSXTrackType.UIProgress:
case PSXTrackType.UIPosition:
case PSXTrackType.UIColor:
break; // zero is fine
}
Vector3 val = EvaluateTrack(track, frame, initialVal);
switch (track.TrackType)
{
case PSXTrackType.CameraPosition:
camPos = val;
break;
case PSXTrackType.CameraRotation:
camRot = Quaternion.Euler(val);
break;
case PSXTrackType.ObjectPosition:
{
var go = GameObject.Find(track.ObjectName);
if (go != null) go.transform.position = val;
break;
}
case PSXTrackType.ObjectRotation:
{
var go = GameObject.Find(track.ObjectName);
if (go != null) go.transform.rotation = Quaternion.Euler(val);
break;
}
case PSXTrackType.ObjectActive:
{
var go = GameObject.Find(track.ObjectName);
if (go != null) go.SetActive(val.x > 0.5f);
break;
}
// UI tracks: no scene preview, values are applied on PS1 only
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
case PSXTrackType.UIProgress:
case PSXTrackType.UIPosition:
case PSXTrackType.UIColor:
break;
}
}
}
// Drive scene view camera
if (sv != null && (camPos.HasValue || camRot.HasValue))
{
Vector3 pos = camPos ?? sv.camera.transform.position;
Quaternion rot = camRot ?? sv.camera.transform.rotation;
// SceneView needs pivot and rotation set — pivot = position + forward * size
sv.rotation = rot;
sv.pivot = pos + rot * Vector3.forward * sv.cameraDistance;
sv.Repaint();
}
// Fire audio events (only during playback, not scrubbing)
if (_playing && clip.AudioEvents != null)
{
for (int i = 0; i < clip.AudioEvents.Count; i++)
{
if (_firedAudioEventIndices.Contains(i)) continue;
var evt = clip.AudioEvents[i];
if (frame >= evt.Frame)
{
_firedAudioEventIndices.Add(i);
PlayAudioPreview(evt);
}
}
}
}
// =====================================================================
// Track Evaluation (linear interpolation, matching C++ runtime)
// =====================================================================
private static Vector3 EvaluateTrack(PSXCutsceneTrack track, float frame, Vector3 initialValue)
{
if (track.Keyframes == null || track.Keyframes.Count == 0)
return Vector3.zero;
// Step interpolation tracks: ObjectActive, UICanvasVisible, UIElementVisible
if (track.TrackType == PSXTrackType.ObjectActive ||
track.TrackType == PSXTrackType.UICanvasVisible ||
track.TrackType == PSXTrackType.UIElementVisible)
{
if (track.Keyframes.Count > 0 && track.Keyframes[0].Frame > 0 && frame < track.Keyframes[0].Frame)
return initialValue;
return EvaluateStep(track.Keyframes, frame);
}
// Find surrounding keyframes
PSXKeyframe before = null, after = null;
for (int i = 0; i < track.Keyframes.Count; i++)
{
if (track.Keyframes[i].Frame <= frame)
before = track.Keyframes[i];
if (track.Keyframes[i].Frame >= frame && after == null)
after = track.Keyframes[i];
}
if (before == null && after == null) return Vector3.zero;
// Pre-first-keyframe: blend from initial value to first keyframe
if (before == null && after != null && after.Frame > 0 && frame < after.Frame)
{
float rawT = frame / after.Frame;
float t = ApplyInterpCurve(rawT, after.Interp);
return Vector3.Lerp(initialValue, after.Value, t);
}
if (before == null) return after.Value;
if (after == null) return before.Value;
if (before == after) return before.Value;
float span = after.Frame - before.Frame;
float rawT2 = (frame - before.Frame) / span;
float t2 = ApplyInterpCurve(rawT2, after.Interp);
// Linear interpolation for all tracks including rotation.
// No shortest-path wrapping: a keyframe from 0 to 360 rotates the full circle.
return Vector3.Lerp(before.Value, after.Value, t2);
}
/// <summary>
/// Apply easing curve to a linear t value (0..1). Matches the C++ applyCurve().
/// </summary>
private static float ApplyInterpCurve(float t, PSXInterpMode mode)
{
switch (mode)
{
default:
case PSXInterpMode.Linear:
return t;
case PSXInterpMode.Step:
return 0f;
case PSXInterpMode.EaseIn:
return t * t;
case PSXInterpMode.EaseOut:
return t * (2f - t);
case PSXInterpMode.EaseInOut:
return t * t * (3f - 2f * t);
}
}
private static Vector3 EvaluateStep(List<PSXKeyframe> keyframes, float frame)
{
Vector3 result = Vector3.zero;
for (int i = 0; i < keyframes.Count; i++)
{
if (keyframes[i].Frame <= frame)
result = keyframes[i].Value;
}
return result;
}
// =====================================================================
// Audio Preview
// =====================================================================
private void PlayAudioPreview(PSXAudioEvent evt)
{
if (string.IsNullOrEmpty(evt.ClipName)) return;
if (!_audioClipCache.TryGetValue(evt.ClipName, out AudioClip clip)) return;
// Use Unity's editor audio playback utility via reflection
// (PlayClipAtPoint doesn't work in edit mode)
var unityEditorAssembly = typeof(AudioImporter).Assembly;
var audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
if (audioUtilClass == null) return;
// Stop any previous preview
var stopMethod = audioUtilClass.GetMethod("StopAllPreviewClips",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
stopMethod?.Invoke(null, null);
// Play the clip
var playMethod = audioUtilClass.GetMethod("PlayPreviewClip",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null, new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) }, null);
playMethod?.Invoke(null, new object[] { clip, 0, false });
}
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ad1b0e43d59aa0446b4e1d6497e8ee94

View File

@@ -15,7 +15,7 @@ namespace SplashEdit.EditorCode
// ───── Main Entry Point ─────
[MenuItem(MENU_ROOT + "SplashEdit Control Panel %#p", false, 0)]
[MenuItem(MENU_ROOT + "SplashEdit Control Panel %#l", false, 0)]
public static void OpenControlPanel()
{
SplashControlPanel.ShowWindow();
@@ -26,7 +26,7 @@ namespace SplashEdit.EditorCode
[MenuItem("GameObject/PlayStation 1/Scene Exporter", false, 10)]
public static void CreateSceneExporter(MenuCommand menuCommand)
{
var existing = Object.FindObjectOfType<PSXSceneExporter>();
var existing = Object.FindFirstObjectByType<PSXSceneExporter>();
if (existing != null)
{
EditorUtility.DisplayDialog(

View File

@@ -21,7 +21,7 @@ namespace SplashEdit.EditorCode
private int _selectedRegion = -1;
private bool _showAdvanced = false;
[MenuItem("PSX/Nav Region Builder")]
[MenuItem("PlayStation 1/Nav Region Builder")]
public static void ShowWindow()
{
GetWindow<PSXNavRegionEditor>("Nav Region Builder");

View File

@@ -2,147 +2,46 @@ using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
using System.Collections.Generic;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXObjectExporter with enhanced UX.
/// Shows mesh info, texture preview, collision visualization, and validation.
/// </summary>
[CustomEditor(typeof(PSXObjectExporter))]
[CanEditMultipleObjects]
public class PSXObjectExporterEditor : UnityEditor.Editor
{
// Serialized properties
private SerializedProperty isActiveProp;
private SerializedProperty bitDepthProp;
private SerializedProperty luaFileProp;
private SerializedProperty objectFlagsProp;
private SerializedProperty collisionTypeProp;
private SerializedProperty exportCollisionMeshProp;
private SerializedProperty customCollisionMeshProp;
private SerializedProperty collisionLayerProp;
private SerializedProperty previewNormalsProp;
private SerializedProperty normalPreviewLengthProp;
private SerializedProperty showCollisionBoundsProp;
private SerializedProperty textureProp;
// UI State
private bool showMeshInfo = true;
private bool showTextureInfo = true;
private bool showExportSettings = true;
private bool showCollisionSettings = true;
private bool showGizmoSettings = false;
private bool showValidation = true;
// Cached data
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private int triangleCount;
private int vertexCount;
private Bounds meshBounds;
private List<string> validationErrors = new List<string>();
private List<string> validationWarnings = new List<string>();
// Styles
private GUIStyle headerStyle;
private GUIStyle errorStyle;
private GUIStyle warningStyle;
// Validation
private bool _validationDirty = true;
private bool showExport = true;
private bool showCollision = true;
private void OnEnable()
{
// Get serialized properties
isActiveProp = serializedObject.FindProperty("isActive");
bitDepthProp = serializedObject.FindProperty("bitDepth");
luaFileProp = serializedObject.FindProperty("luaFile");
objectFlagsProp = serializedObject.FindProperty("objectFlags");
collisionTypeProp = serializedObject.FindProperty("collisionType");
exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh");
customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh");
collisionLayerProp = serializedObject.FindProperty("collisionLayer");
previewNormalsProp = serializedObject.FindProperty("previewNormals");
normalPreviewLengthProp = serializedObject.FindProperty("normalPreviewLength");
showCollisionBoundsProp = serializedObject.FindProperty("showCollisionBounds");
textureProp = serializedObject.FindProperty("texture");
// Cache mesh info
CacheMeshInfo();
// Defer validation to first inspector draw
_validationDirty = true;
}
private void CacheMeshInfo()
{
var exporter = target as PSXObjectExporter;
if (exporter == null) return;
meshFilter = exporter.GetComponent<MeshFilter>();
meshRenderer = exporter.GetComponent<MeshRenderer>();
if (meshFilter != null && meshFilter.sharedMesh != null)
{
var mesh = meshFilter.sharedMesh;
triangleCount = mesh.triangles.Length / 3;
vertexCount = mesh.vertexCount;
meshBounds = mesh.bounds;
}
}
private void RunValidation()
{
validationErrors.Clear();
validationWarnings.Clear();
var exporter = target as PSXObjectExporter;
if (exporter == null) return;
// Check mesh
if (meshFilter == null || meshFilter.sharedMesh == null)
{
validationErrors.Add("No mesh assigned to MeshFilter");
}
else
{
if (triangleCount > 100)
{
validationWarnings.Add($"High triangle count ({triangleCount}). PS1 recommended: <100 per object");
}
// Check vertex bounds
var mesh = meshFilter.sharedMesh;
var verts = mesh.vertices;
bool hasOutOfBounds = false;
foreach (var v in verts)
{
var world = exporter.transform.TransformPoint(v);
float scaled = Mathf.Max(Mathf.Abs(world.x), Mathf.Abs(world.y), Mathf.Abs(world.z)) * 4096f;
if (scaled > 32767f)
{
hasOutOfBounds = true;
break;
}
}
if (hasOutOfBounds)
{
validationErrors.Add("Vertices exceed PS1 coordinate limits (±8 units from origin)");
}
}
// Check renderer
if (meshRenderer == null)
{
validationWarnings.Add("No MeshRenderer - object will not be visible");
}
else if (meshRenderer.sharedMaterial == null)
{
validationWarnings.Add("No material assigned - will use default colors");
triangleCount = meshFilter.sharedMesh.triangles.Length / 3;
vertexCount = meshFilter.sharedMesh.vertexCount;
}
}
@@ -150,290 +49,140 @@ namespace SplashEdit.EditorCode
{
serializedObject.Update();
// Run deferred validation
if (_validationDirty)
{
RunValidation();
_validationDirty = false;
}
InitStyles();
// Active toggle at top
EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object"));
DrawHeader();
EditorGUILayout.Space(4);
if (!isActiveProp.boolValue)
{
EditorGUILayout.HelpBox("This object will be skipped during export.", MessageType.Info);
EditorGUILayout.LabelField("Object will be skipped during export.", PSXEditorStyles.InfoBox);
serializedObject.ApplyModifiedProperties();
return;
}
EditorGUILayout.Space(5);
DrawMeshSummary();
PSXEditorStyles.DrawSeparator(6, 6);
DrawExportSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawCollisionSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawActions();
// Mesh Info Section
DrawMeshInfoSection();
// Texture Section
DrawTextureSection();
// Export Settings Section
DrawExportSettingsSection();
// Collision Settings Section
DrawCollisionSettingsSection();
// Gizmo Settings Section
DrawGizmoSettingsSection();
// Validation Section
DrawValidationSection();
// Action Buttons
DrawActionButtons();
if (serializedObject.ApplyModifiedProperties())
{
_validationDirty = true;
}
serializedObject.ApplyModifiedProperties();
}
private void InitStyles()
private new void DrawHeader()
{
if (headerStyle == null)
{
headerStyle = new GUIStyle(EditorStyles.foldoutHeader);
}
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
if (errorStyle == null)
{
errorStyle = new GUIStyle(EditorStyles.label);
errorStyle.normal.textColor = Color.red;
}
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(isActiveProp, GUIContent.none, GUILayout.Width(18));
var exporter = target as PSXObjectExporter;
EditorGUILayout.LabelField(exporter.gameObject.name, PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.EndHorizontal();
if (warningStyle == null)
{
warningStyle = new GUIStyle(EditorStyles.label);
warningStyle.normal.textColor = new Color(1f, 0.7f, 0f);
}
EditorGUILayout.EndVertical();
}
private void DrawMeshInfoSection()
private void DrawMeshSummary()
{
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information");
if (showMeshInfo)
if (meshFilter == null || meshFilter.sharedMesh == null)
{
EditorGUI.indentLevel++;
if (meshFilter != null && meshFilter.sharedMesh != null)
{
EditorGUILayout.LabelField("Mesh", meshFilter.sharedMesh.name);
EditorGUILayout.LabelField("Triangles", triangleCount.ToString());
EditorGUILayout.LabelField("Vertices", vertexCount.ToString());
EditorGUILayout.LabelField("Bounds Size", meshBounds.size.ToString("F2"));
// Triangle budget bar
float budgetPercent = triangleCount / 100f;
Rect rect = EditorGUILayout.GetControlRect(false, 20);
EditorGUI.ProgressBar(rect, Mathf.Clamp01(budgetPercent), $"Triangle Budget: {triangleCount}/100");
}
else
{
EditorGUILayout.HelpBox("No mesh assigned", MessageType.Warning);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawTextureSection()
{
showTextureInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showTextureInfo, "Texture Settings");
if (showTextureInfo)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(textureProp, new GUIContent("Override Texture"));
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
// Show texture preview if assigned
var tex = textureProp.objectReferenceValue as Texture2D;
if (tex != null)
{
EditorGUILayout.Space(5);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
Rect previewRect = GUILayoutUtility.GetRect(64, 64, GUILayout.Width(64));
EditorGUI.DrawPreviewTexture(previewRect, tex);
GUILayout.FlexibleSpace();
}
EditorGUILayout.LabelField($"Size: {tex.width}x{tex.height}");
// VRAM estimate
int bpp = bitDepthProp.enumValueIndex == 0 ? 4 : (bitDepthProp.enumValueIndex == 1 ? 8 : 16);
int vramBytes = (tex.width * tex.height * bpp) / 8;
EditorGUILayout.LabelField($"Est. VRAM: {vramBytes} bytes ({bpp}bpp)");
}
else if (meshRenderer != null && meshRenderer.sharedMaterial != null)
{
var matTex = meshRenderer.sharedMaterial.mainTexture;
if (matTex != null)
{
EditorGUILayout.HelpBox($"Using material texture: {matTex.name}", MessageType.Info);
}
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawExportSettingsSection()
{
showExportSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showExportSettings, "Export Settings");
if (showExportSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(objectFlagsProp, new GUIContent("Object Flags"));
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
// Quick Lua file buttons
if (luaFileProp.objectReferenceValue != null)
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Edit Lua", GUILayout.Width(80)))
{
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
}
if (GUILayout.Button("Clear", GUILayout.Width(60)))
{
luaFileProp.objectReferenceValue = null;
}
}
}
else
{
if (GUILayout.Button("Create New Lua Script"))
{
CreateNewLuaScript();
}
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawCollisionSettingsSection()
{
showCollisionSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showCollisionSettings, "Collision Settings");
if (showCollisionSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Collision Type"));
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
if (collType != PSXCollisionType.None)
{
EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Collision Mesh"));
EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Collision Layer"));
// Collision info
EditorGUILayout.Space(5);
string collisionInfo = collType switch
{
PSXCollisionType.Solid => "Solid: Blocks movement, fires onCollision",
PSXCollisionType.Trigger => "Trigger: Fires onTriggerEnter/Exit, doesn't block",
PSXCollisionType.Platform => "Platform: Solid from above only",
_ => ""
};
EditorGUILayout.HelpBox(collisionInfo, MessageType.Info);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawGizmoSettingsSection()
{
showGizmoSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showGizmoSettings, "Gizmo Settings");
if (showGizmoSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(previewNormalsProp, new GUIContent("Preview Normals"));
if (previewNormalsProp.boolValue)
{
EditorGUILayout.PropertyField(normalPreviewLengthProp, new GUIContent("Normal Length"));
}
EditorGUILayout.PropertyField(showCollisionBoundsProp, new GUIContent("Show Collision Bounds"));
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawValidationSection()
{
if (validationErrors.Count == 0 && validationWarnings.Count == 0)
EditorGUILayout.LabelField("No mesh on this object.", PSXEditorStyles.InfoBox);
return;
showValidation = EditorGUILayout.BeginFoldoutHeaderGroup(showValidation, "Validation");
if (showValidation)
{
foreach (var error in validationErrors)
{
EditorGUILayout.HelpBox(error, MessageType.Error);
}
foreach (var warning in validationWarnings)
{
EditorGUILayout.HelpBox(warning, MessageType.Warning);
}
if (GUILayout.Button("Refresh Validation"))
{
CacheMeshInfo();
RunValidation();
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"{triangleCount} tris", PSXEditorStyles.RichLabel, GUILayout.Width(60));
EditorGUILayout.LabelField($"{vertexCount} verts", PSXEditorStyles.RichLabel, GUILayout.Width(70));
int subMeshCount = meshFilter.sharedMesh.subMeshCount;
if (subMeshCount > 1)
EditorGUILayout.LabelField($"{subMeshCount} submeshes", PSXEditorStyles.RichLabel, GUILayout.Width(90));
int matCount = meshRenderer != null ? meshRenderer.sharedMaterials.Length : 0;
int textured = meshRenderer != null
? meshRenderer.sharedMaterials.Count(m => m != null && m.mainTexture != null)
: 0;
if (textured > 0)
EditorGUILayout.LabelField($"{textured}/{matCount} textured", PSXEditorStyles.RichLabel);
else
EditorGUILayout.LabelField("untextured", PSXEditorStyles.RichLabel);
EditorGUILayout.EndHorizontal();
}
private void DrawActionButtons()
private void DrawExportSection()
{
EditorGUILayout.Space(10);
showExport = EditorGUILayout.Foldout(showExport, "Export", true, PSXEditorStyles.FoldoutHeader);
if (!showExport) return;
using (new EditorGUILayout.HorizontalScope())
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
if (luaFileProp.objectReferenceValue != null)
{
if (GUILayout.Button("Select Scene Exporter"))
{
var exporter = FindObjectOfType<PSXSceneExporter>();
if (exporter != null)
{
Selection.activeGameObject = exporter.gameObject;
}
else
{
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
}
if (GUILayout.Button("Open Scene Validator"))
{
PSXSceneValidatorWindow.ShowWindow();
}
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
if (GUILayout.Button("Clear", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
luaFileProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Create Lua Script", PSXEditorStyles.SecondaryButton, GUILayout.Width(130)))
CreateNewLuaScript();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
}
private void DrawCollisionSection()
{
showCollision = EditorGUILayout.Foldout(showCollision, "Collision", true, PSXEditorStyles.FoldoutHeader);
if (!showCollision) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Type"));
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
if (collType == PSXCollisionType.Static)
{
EditorGUILayout.LabelField(
"<color=#88cc88>Only bakes holes in the navregions</color>",
PSXEditorStyles.RichLabel);
}
else if (collType == PSXCollisionType.Dynamic)
{
EditorGUILayout.LabelField(
"<color=#88aaff>Runtime AABB collider. Pushes player back + fires Lua events.</color>",
PSXEditorStyles.RichLabel);
}
EditorGUI.indentLevel--;
}
private void DrawActions()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select Scene Exporter", PSXEditorStyles.SecondaryButton))
{
var se = FindFirstObjectByType<PSXSceneExporter>();
if (se != null)
Selection.activeGameObject = se.gameObject;
else
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
EditorGUILayout.EndHorizontal();
}
private void CreateNewLuaScript()
@@ -441,74 +190,58 @@ namespace SplashEdit.EditorCode
var exporter = target as PSXObjectExporter;
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script",
defaultName + ".lua",
"lua",
"Create Lua Script", defaultName + ".lua", "lua",
"Create a new Lua script for this object");
if (!string.IsNullOrEmpty(path))
if (string.IsNullOrEmpty(path)) return;
string template =
$"function onCreate(self)\nend\n\nfunction onUpdate(self, dt)\nend\n";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
string template = $@"-- Lua script for {exporter.gameObject.name}
--
-- Available globals: Entity, Vec3, Input, Timer, Camera, Audio,
-- Debug, Math, Scene, Persist
--
-- Available events:
-- onCreate(self) — called once when the object is registered
-- onUpdate(self, dt) — called every frame (dt = delta frames, usually 1)
-- onEnable(self) — called when the object becomes active
-- onDisable(self) — called when the object becomes inactive
-- onCollision(self, other) — called on collision with another object
-- onTriggerEnter(self, other)
-- onTriggerStay(self, other)
-- onTriggerExit(self, other)
-- onInteract(self) — called when the player interacts
-- onButtonPress(self, btn) — called on button press (btn = Input.CROSS etc.)
-- onButtonRelease(self, btn)
-- onDestroy(self) — called before the object is destroyed
--
-- Properties: self.position (Vec3), self.rotationY (pi-units), self.active (bool)
function onCreate(self)
-- Called once when this object is registered in the scene
end
function onUpdate(self, dt)
-- Called every frame. dt = number of elapsed frames (usually 1).
end
function onInteract(self)
-- Called when the player interacts with this object
end
";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
[MenuItem("CONTEXT/PSXObjectExporter/Copy Settings to Selected")]
private static void CopySettingsToSelected(MenuCommand command)
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
private static void DrawColliderGizmo(PSXObjectExporter exporter, GizmoType gizmoType)
{
var source = command.context as PSXObjectExporter;
if (source == null) return;
if (exporter.CollisionType != PSXCollisionType.Dynamic) return;
foreach (var go in Selection.gameObjects)
MeshFilter mf = exporter.GetComponent<MeshFilter>();
Mesh mesh = mf?.sharedMesh;
if (mesh == null) return;
Bounds local = mesh.bounds;
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
Vector3 ext = local.extents;
Vector3 center = local.center;
Vector3 aabbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 aabbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int i = 0; i < 8; i++)
{
var target = go.GetComponent<PSXObjectExporter>();
if (target != null && target != source)
{
Undo.RecordObject(target, "Copy PSX Settings");
// Copy via serialized object
EditorUtility.CopySerialized(source, target);
}
Vector3 corner = center + new Vector3(
(i & 1) != 0 ? ext.x : -ext.x,
(i & 2) != 0 ? ext.y : -ext.y,
(i & 4) != 0 ? ext.z : -ext.z
);
Vector3 world = worldMatrix.MultiplyPoint3x4(corner);
aabbMin = Vector3.Min(aabbMin, world);
aabbMax = Vector3.Max(aabbMax, world);
}
bool selected = (gizmoType & GizmoType.Selected) != 0;
Gizmos.color = selected ? new Color(0.2f, 0.8f, 1f, 0.8f) : new Color(0.2f, 0.8f, 1f, 0.3f);
Vector3 c = (aabbMin + aabbMax) * 0.5f;
Vector3 s = aabbMax - aabbMin;
Gizmos.DrawWireCube(c, s);
}
}
}

View File

@@ -1,143 +1,192 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXSceneExporter.
/// When the component is selected and fog is enabled, activates a Unity scene-view
/// fog preview that approximates the PS1 linear fog distances.
///
/// Fog distance mapping:
/// fogFarSZ = 8000 / FogDensity (GTE SZ units)
/// fogNearSZ = fogFarSZ / 3
/// SZ is 20.12 fixed-point: SZ = (unityCoord / GTEScaling) * 4096
/// => unityDist = SZ * GTEScaling / 4096
/// => Unity fog near = (8000 / (FogDensity * 3)) * GTEScaling / 4096
/// => Unity fog far = (8000 / FogDensity) * GTEScaling / 4096
/// </summary>
[CustomEditor(typeof(PSXSceneExporter))]
public class PSXSceneExporterEditor : UnityEditor.Editor
{
// Saved RenderSettings state so we can restore it on deselect.
private bool _savedFog;
private Color _savedFogColor;
private FogMode _savedFogMode;
private float _savedFogStart;
private float _savedFogEnd;
private SerializedProperty gteScalingProp;
private SerializedProperty sceneLuaProp;
private SerializedProperty fogEnabledProp;
private SerializedProperty fogColorProp;
private SerializedProperty fogDensityProp;
private SerializedProperty sceneTypeProp;
private SerializedProperty cutscenesProp;
private SerializedProperty loadingScreenProp;
private SerializedProperty previewBVHProp;
private SerializedProperty previewRoomsPortalsProp;
private SerializedProperty bvhDepthProp;
private bool _previewActive = false;
private bool showFog = true;
private bool showCutscenes = true;
private bool showDebug = false;
private void OnEnable()
{
SaveAndApplyFogPreview();
// Re-apply whenever the scene is repainted (handles inspector value changes).
EditorApplication.update += OnEditorUpdate;
gteScalingProp = serializedObject.FindProperty("GTEScaling");
sceneLuaProp = serializedObject.FindProperty("SceneLuaFile");
fogEnabledProp = serializedObject.FindProperty("FogEnabled");
fogColorProp = serializedObject.FindProperty("FogColor");
fogDensityProp = serializedObject.FindProperty("FogDensity");
sceneTypeProp = serializedObject.FindProperty("SceneType");
cutscenesProp = serializedObject.FindProperty("Cutscenes");
loadingScreenProp = serializedObject.FindProperty("LoadingScreenPrefab");
previewBVHProp = serializedObject.FindProperty("PreviewBVH");
previewRoomsPortalsProp = serializedObject.FindProperty("PreviewRoomsPortals");
bvhDepthProp = serializedObject.FindProperty("BVHPreviewDepth");
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
RestoreFog();
}
private void OnEditorUpdate()
{
// Keep the preview in sync when the user tweaks values in the inspector.
if (_previewActive)
ApplyFogPreview();
}
private void SaveAndApplyFogPreview()
{
_savedFog = RenderSettings.fog;
_savedFogColor = RenderSettings.fogColor;
_savedFogMode = RenderSettings.fogMode;
_savedFogStart = RenderSettings.fogStartDistance;
_savedFogEnd = RenderSettings.fogEndDistance;
_previewActive = true;
ApplyFogPreview();
}
private void ApplyFogPreview()
{
var exporter = (PSXSceneExporter)target;
if (exporter == null) return;
if (!exporter.FogEnabled)
{
// Fog disabled on the component - turn off the preview.
RenderSettings.fog = false;
return;
}
float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
// fogFarSZ in GTE SZ units (20.12 fp); convert to Unity world-space.
// SZ = (unityDist / GTEScaling) * 4096, so unityDist = SZ * GTEScaling / 4096
float fogFarSZ = 8000f / density;
float fogNearSZ = fogFarSZ / 3f;
float fogFarUnity = fogFarSZ * gteScale / 4096f;
float fogNearUnity = fogNearSZ * gteScale / 4096f;
RenderSettings.fog = true;
RenderSettings.fogColor = exporter.FogColor;
RenderSettings.fogMode = FogMode.Linear;
RenderSettings.fogStartDistance = fogNearUnity;
RenderSettings.fogEndDistance = fogFarUnity;
}
private void RestoreFog()
{
if (!_previewActive) return;
_previewActive = false;
RenderSettings.fog = _savedFog;
RenderSettings.fogColor = _savedFogColor;
RenderSettings.fogMode = _savedFogMode;
RenderSettings.fogStartDistance = _savedFogStart;
RenderSettings.fogEndDistance = _savedFogEnd;
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
// Show computed fog distances when fog is enabled, so the user
// can see exactly what range the preview represents.
var exporter = (PSXSceneExporter)target;
if (exporter.FogEnabled)
DrawExporterHeader();
EditorGUILayout.Space(4);
DrawSceneSettings();
PSXEditorStyles.DrawSeparator(6, 6);
DrawFogSection(exporter);
PSXEditorStyles.DrawSeparator(6, 6);
DrawCutscenesSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawLoadingSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawDebugSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawSceneStats();
serializedObject.ApplyModifiedProperties();
}
private void DrawExporterHeader()
{
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.LabelField("Scene Exporter", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.EndVertical();
}
private void DrawSceneSettings()
{
EditorGUILayout.PropertyField(sceneTypeProp, new GUIContent("Scene Type"));
bool isInterior = (PSXSceneType)sceneTypeProp.enumValueIndex == PSXSceneType.Interior;
EditorGUILayout.LabelField(
isInterior
? "<color=#88aaff>Room/portal occlusion culling.</color>"
: "<color=#88cc88>BVH frustum culling.</color>",
PSXEditorStyles.RichLabel);
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(gteScalingProp, new GUIContent("GTE Scaling"));
EditorGUILayout.PropertyField(sceneLuaProp, new GUIContent("Scene Lua"));
if (sceneLuaProp.objectReferenceValue != null)
{
EditorGUILayout.Space(4);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Fog Preview (active in Scene view)", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", EditorStyles.miniButtonLeft, GUILayout.Width(50)))
AssetDatabase.OpenAsset(sceneLuaProp.objectReferenceValue);
if (GUILayout.Button("Clear", EditorStyles.miniButtonRight, GUILayout.Width(50)))
sceneLuaProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
}
private void DrawFogSection(PSXSceneExporter exporter)
{
showFog = EditorGUILayout.Foldout(showFog, "Fog & Background", true, PSXEditorStyles.FoldoutHeader);
if (!showFog) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(fogColorProp, new GUIContent("Background Color",
"Background clear color. Also used as the fog blend target when fog is enabled."));
EditorGUILayout.PropertyField(fogEnabledProp, new GUIContent("Distance Fog"));
if (fogEnabledProp.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(fogDensityProp, new GUIContent("Density"));
float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
float fogFarUnity = (8000f / density) * gteScale / 4096f;
float fogNearUnity = fogFarUnity / 3f;
EditorGUILayout.LabelField("Near distance", $"{fogNearUnity:F1} Unity units");
EditorGUILayout.LabelField("Far distance", $"{fogFarUnity:F1} Unity units");
EditorGUILayout.LabelField("(PS1 SZ range)", $"{8000f / (density * 3f):F0} - {8000f / density:F0} GTE units");
EditorGUILayout.EndVertical();
// Keep preview applied as values may have changed.
ApplyFogPreview();
}
else
{
// Make sure preview is off when fog is disabled.
RenderSettings.fog = false;
EditorGUILayout.Space(2);
EditorGUILayout.LabelField(
$"<color=#aaaaaa>GTE range: {fogNearUnity:F1} - {fogFarUnity:F1} units | " +
$"{8000f / (density * 3f):F0} - {8000f / density:F0} SZ</color>",
PSXEditorStyles.RichLabel);
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
EditorGUI.indentLevel--;
}
private void DrawCutscenesSection()
{
showCutscenes = EditorGUILayout.Foldout(showCutscenes, "Cutscenes", true, PSXEditorStyles.FoldoutHeader);
if (!showCutscenes) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(cutscenesProp, new GUIContent("Clips"), true);
EditorGUI.indentLevel--;
}
private void DrawLoadingSection()
{
EditorGUILayout.PropertyField(loadingScreenProp, new GUIContent("Loading Screen Prefab"));
if (loadingScreenProp.objectReferenceValue != null)
{
var go = loadingScreenProp.objectReferenceValue as GameObject;
if (go != null && go.GetComponentInChildren<PSXCanvas>() == null)
{
EditorGUILayout.LabelField(
"<color=#ffaa44>Prefab has no PSXCanvas component.</color>",
PSXEditorStyles.RichLabel);
}
}
}
private void DrawDebugSection()
{
showDebug = EditorGUILayout.Foldout(showDebug, "Debug", true, PSXEditorStyles.FoldoutHeader);
if (!showDebug) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(previewBVHProp, new GUIContent("Preview BVH"));
if (previewBVHProp.boolValue)
EditorGUILayout.PropertyField(bvhDepthProp, new GUIContent("BVH Depth"));
EditorGUILayout.PropertyField(previewRoomsPortalsProp, new GUIContent("Preview Rooms/Portals"));
EditorGUI.indentLevel--;
}
private void DrawSceneStats()
{
var exporters = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
int total = exporters.Length;
int active = exporters.Count(e => e.IsActive);
int staticCol = exporters.Count(e => e.CollisionType == PSXCollisionType.Static);
int dynamicCol = exporters.Count(e => e.CollisionType == PSXCollisionType.Dynamic);
int triggerBoxes = FindObjectsByType<PSXTriggerBox>(FindObjectsSortMode.None).Length;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.LabelField(
$"<b>{active}</b>/{total} objects | <b>{staticCol}</b> static <b>{dynamicCol}</b> dynamic <b>{triggerBoxes}</b> triggers",
PSXEditorStyles.RichLabel);
EditorGUILayout.EndVertical();
}
}
}

View File

@@ -1,496 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Scene Validator Window - Validates the current scene for PS1 compatibility.
/// Checks for common issues that would cause problems on real hardware.
/// </summary>
public class PSXSceneValidatorWindow : EditorWindow
{
private Vector2 scrollPosition;
private List<ValidationResult> validationResults = new List<ValidationResult>();
private bool hasValidated = false;
private int errorCount = 0;
private int warningCount = 0;
private int infoCount = 0;
// Filter toggles
private bool showErrors = true;
private bool showWarnings = true;
private bool showInfo = true;
// PS1 Limits
private const int MAX_RECOMMENDED_TRIS_PER_OBJECT = 100;
private const int MAX_RECOMMENDED_TOTAL_TRIS = 400;
private const int MAX_VERTEX_COORD = 32767; // signed 16-bit
private const int MIN_VERTEX_COORD = -32768;
private const int VRAM_WIDTH = 1024;
private const int VRAM_HEIGHT = 512;
private static readonly Vector2 MinSize = new Vector2(500, 400);
public static void ShowWindow()
{
var window = GetWindow<PSXSceneValidatorWindow>("Scene Validator");
window.minSize = MinSize;
}
private void OnEnable()
{
validationResults.Clear();
hasValidated = false;
}
private void OnGUI()
{
DrawHeader();
DrawFilters();
DrawResults();
DrawFooter();
}
private void DrawHeader()
{
EditorGUILayout.Space(5);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("PS1 Scene Validator", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
if (GUILayout.Button("Validate Scene", GUILayout.Width(120)))
{
ValidateScene();
}
}
EditorGUILayout.Space(5);
// Summary bar
if (hasValidated)
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
{
var errorStyle = new GUIStyle(EditorStyles.label);
errorStyle.normal.textColor = errorCount > 0 ? Color.red : Color.green;
GUILayout.Label($"✗ {errorCount} Errors", errorStyle);
var warnStyle = new GUIStyle(EditorStyles.label);
warnStyle.normal.textColor = warningCount > 0 ? new Color(1f, 0.7f, 0f) : Color.green;
GUILayout.Label($"⚠ {warningCount} Warnings", warnStyle);
var infoStyle = new GUIStyle(EditorStyles.label);
infoStyle.normal.textColor = Color.cyan;
GUILayout.Label($" {infoCount} Info", infoStyle);
GUILayout.FlexibleSpace();
}
}
EditorGUILayout.Space(5);
}
private void DrawFilters()
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("Show:", GUILayout.Width(40));
showErrors = GUILayout.Toggle(showErrors, "Errors", EditorStyles.miniButtonLeft);
showWarnings = GUILayout.Toggle(showWarnings, "Warnings", EditorStyles.miniButtonMid);
showInfo = GUILayout.Toggle(showInfo, "Info", EditorStyles.miniButtonRight);
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
}
private void DrawResults()
{
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
{
scrollPosition = scrollView.scrollPosition;
if (!hasValidated)
{
EditorGUILayout.HelpBox("Click 'Validate Scene' to check for PS1 compatibility issues.", MessageType.Info);
return;
}
if (validationResults.Count == 0)
{
EditorGUILayout.HelpBox("No issues found! Your scene looks ready for PS1 export.", MessageType.Info);
return;
}
foreach (var result in validationResults)
{
if (result.Type == ValidationType.Error && !showErrors) continue;
if (result.Type == ValidationType.Warning && !showWarnings) continue;
if (result.Type == ValidationType.Info && !showInfo) continue;
DrawValidationResult(result);
}
}
}
private void DrawValidationResult(ValidationResult result)
{
MessageType msgType = result.Type switch
{
ValidationType.Error => MessageType.Error,
ValidationType.Warning => MessageType.Warning,
_ => MessageType.Info
};
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.HelpBox(result.Message, msgType);
if (result.RelatedObject != null)
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("Object:", GUILayout.Width(50));
if (GUILayout.Button(result.RelatedObject.name, EditorStyles.linkLabel))
{
Selection.activeObject = result.RelatedObject;
EditorGUIUtility.PingObject(result.RelatedObject);
}
GUILayout.FlexibleSpace();
if (!string.IsNullOrEmpty(result.FixAction))
{
if (GUILayout.Button("Fix", GUILayout.Width(50)))
{
ApplyFix(result);
}
}
}
}
}
EditorGUILayout.Space(2);
}
private void DrawFooter()
{
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Select All With Errors"))
{
var errorObjects = validationResults
.Where(r => r.Type == ValidationType.Error && r.RelatedObject != null)
.Select(r => r.RelatedObject)
.Distinct()
.ToArray();
Selection.objects = errorObjects;
}
}
}
private void ValidateScene()
{
validationResults.Clear();
errorCount = 0;
warningCount = 0;
infoCount = 0;
// Check for scene exporter
ValidateSceneExporter();
// Check all PSX objects
ValidatePSXObjects();
// Check textures and VRAM
ValidateTextures();
// Check Lua files
ValidateLuaFiles();
// Overall scene stats
ValidateSceneStats();
hasValidated = true;
Repaint();
}
private void ValidateSceneExporter()
{
var exporters = Object.FindObjectsOfType<PSXSceneExporter>();
if (exporters.Length == 0)
{
AddResult(ValidationType.Error,
"No PSXSceneExporter found in scene. Add one via GameObject > PlayStation 1 > Scene Exporter",
null, "AddExporter");
}
else if (exporters.Length > 1)
{
AddResult(ValidationType.Warning,
$"Multiple PSXSceneExporters found ({exporters.Length}). Only one is needed per scene.",
exporters[0].gameObject);
}
}
private void ValidatePSXObjects()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
if (exporters.Length == 0)
{
AddResult(ValidationType.Info,
"No objects marked for PSX export. Add PSXObjectExporter components to GameObjects you want to export.",
null);
return;
}
foreach (var exporter in exporters)
{
ValidateSingleObject(exporter);
}
}
private void ValidateSingleObject(PSXObjectExporter exporter)
{
var go = exporter.gameObject;
// Check for mesh
var meshFilter = go.GetComponent<MeshFilter>();
if (meshFilter == null || meshFilter.sharedMesh == null)
{
AddResult(ValidationType.Warning,
$"'{go.name}' has no mesh. It will be exported as an empty object.",
go);
return;
}
var mesh = meshFilter.sharedMesh;
int triCount = mesh.triangles.Length / 3;
// Check triangle count
if (triCount > MAX_RECOMMENDED_TRIS_PER_OBJECT)
{
AddResult(ValidationType.Warning,
$"'{go.name}' has {triCount} triangles (recommended max: {MAX_RECOMMENDED_TRIS_PER_OBJECT}). Consider simplifying.",
go);
}
// Check vertex coordinates for GTE limits
var vertices = mesh.vertices;
var transform = go.transform;
bool hasOutOfBounds = false;
foreach (var vert in vertices)
{
var worldPos = transform.TransformPoint(vert);
// Check if fixed-point conversion would overflow (assuming scale factor)
float scaledX = worldPos.x * 4096f; // FixedPoint<12> scale
float scaledY = worldPos.y * 4096f;
float scaledZ = worldPos.z * 4096f;
if (scaledX > MAX_VERTEX_COORD || scaledX < MIN_VERTEX_COORD ||
scaledY > MAX_VERTEX_COORD || scaledY < MIN_VERTEX_COORD ||
scaledZ > MAX_VERTEX_COORD || scaledZ < MIN_VERTEX_COORD)
{
hasOutOfBounds = true;
break;
}
}
if (hasOutOfBounds)
{
AddResult(ValidationType.Error,
$"'{go.name}' has vertices that exceed PS1 coordinate limits. Move closer to origin or scale down.",
go);
}
// Check for renderer and material
var renderer = go.GetComponent<MeshRenderer>();
if (renderer == null)
{
AddResult(ValidationType.Info,
$"'{go.name}' has no MeshRenderer. Will be exported without visual rendering.",
go);
}
else if (renderer.sharedMaterial == null)
{
AddResult(ValidationType.Warning,
$"'{go.name}' has no material assigned. Will use default colors.",
go);
}
// Check texture settings on exporter
if (exporter.texture != null)
{
ValidateTexture(exporter.texture, go);
}
}
private void ValidateTextures()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
var textures = exporters
.Where(e => e.texture != null)
.Select(e => e.texture)
.Distinct()
.ToList();
if (textures.Count == 0)
{
AddResult(ValidationType.Info,
"No textures assigned to any PSX objects. Scene will be vertex-colored only.",
null);
return;
}
// Rough VRAM estimation
int estimatedVramUsage = 0;
foreach (var tex in textures)
{
// Rough estimate: width * height * bits/8
// This is simplified - actual packing is more complex
int bitsPerPixel = 16; // Assume 16bpp worst case
estimatedVramUsage += (tex.width * tex.height * bitsPerPixel) / 8;
}
int vramTotal = VRAM_WIDTH * VRAM_HEIGHT * 2; // 16bpp
int vramAvailable = vramTotal / 2; // Assume half for framebuffers
if (estimatedVramUsage > vramAvailable)
{
AddResult(ValidationType.Warning,
$"Estimated texture VRAM usage ({estimatedVramUsage / 1024}KB) may exceed available space (~{vramAvailable / 1024}KB). " +
"Consider using lower bit depths or smaller textures.",
null);
}
}
private void ValidateTexture(Texture2D texture, GameObject relatedObject)
{
// Check power of 2
if (!Mathf.IsPowerOfTwo(texture.width) || !Mathf.IsPowerOfTwo(texture.height))
{
AddResult(ValidationType.Warning,
$"Texture '{texture.name}' dimensions ({texture.width}x{texture.height}) are not power of 2. May cause issues.",
relatedObject);
}
// Check max size
if (texture.width > 256 || texture.height > 256)
{
AddResult(ValidationType.Warning,
$"Texture '{texture.name}' is large ({texture.width}x{texture.height}). Consider using 256x256 or smaller.",
relatedObject);
}
}
private void ValidateLuaFiles()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
foreach (var exporter in exporters)
{
if (exporter.LuaFile != null)
{
// Check if Lua file exists and is valid
string path = AssetDatabase.GetAssetPath(exporter.LuaFile);
if (string.IsNullOrEmpty(path))
{
AddResult(ValidationType.Error,
$"'{exporter.name}' references an invalid Lua file.",
exporter.gameObject);
}
}
}
}
private void ValidateSceneStats()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
int totalTris = 0;
foreach (var exporter in exporters)
{
var mf = exporter.GetComponent<MeshFilter>();
if (mf != null && mf.sharedMesh != null)
{
totalTris += mf.sharedMesh.triangles.Length / 3;
}
}
AddResult(ValidationType.Info,
$"Scene statistics: {exporters.Length} objects, {totalTris} total triangles.",
null);
if (totalTris > MAX_RECOMMENDED_TOTAL_TRIS)
{
AddResult(ValidationType.Warning,
$"Total triangle count ({totalTris}) exceeds recommended maximum ({MAX_RECOMMENDED_TOTAL_TRIS}). " +
"Performance may be poor on real hardware.",
null);
}
}
private void AddResult(ValidationType type, string message, GameObject relatedObject, string fixAction = null)
{
validationResults.Add(new ValidationResult
{
Type = type,
Message = message,
RelatedObject = relatedObject,
FixAction = fixAction
});
switch (type)
{
case ValidationType.Error: errorCount++; break;
case ValidationType.Warning: warningCount++; break;
case ValidationType.Info: infoCount++; break;
}
}
private void ApplyFix(ValidationResult result)
{
switch (result.FixAction)
{
case "AddExporter":
var go = new GameObject("PSXSceneExporter");
go.AddComponent<PSXSceneExporter>();
Undo.RegisterCreatedObjectUndo(go, "Create PSX Scene Exporter");
Selection.activeGameObject = go;
ValidateScene(); // Re-validate
break;
}
}
private enum ValidationType
{
Error,
Warning,
Info
}
private class ValidationResult
{
public ValidationType Type;
public string Message;
public GameObject RelatedObject;
public string FixAction;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 0a26bf89301a2554ca287b9e28e44906

View File

@@ -1,149 +1,307 @@
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Manages downloading and updating the psxsplash native project from GitHub releases.
/// Uses the GitHub REST API (HTTP) to list releases and git to clone/checkout
/// (required for recursive submodule support).
/// </summary>
public static class PSXSplashInstaller
{
// ───── Public config ─────
public static readonly string RepoOwner = "psxsplash";
public static readonly string RepoName = "psxsplash";
public static readonly string RepoUrl = "https://github.com/psxsplash/psxsplash.git";
public static readonly string InstallPath = "Assets/psxsplash";
public static readonly string FullInstallPath;
private static readonly string GitHubApiReleasesUrl =
$"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases";
// ───── Cached release list ─────
private static List<ReleaseInfo> _cachedReleases = new List<ReleaseInfo>();
private static bool _isFetchingReleases;
/// <summary>
/// Represents a GitHub release.
/// </summary>
[Serializable]
public class ReleaseInfo
{
public string TagName; // e.g. "v1.2.0"
public string Name; // human-readable name
public string Body; // release notes (markdown)
public string PublishedAt; // ISO 8601 date
public bool IsPrerelease;
public bool IsDraft;
}
static PSXSplashInstaller()
{
FullInstallPath = Path.Combine(Application.dataPath, "psxsplash");
}
// ═══════════════════════════════════════════════════════════════
// Queries
// ═══════════════════════════════════════════════════════════════
/// <summary>Is the native project cloned on disk?</summary>
public static bool IsInstalled()
{
return Directory.Exists(FullInstallPath) &&
Directory.EnumerateFileSystemEntries(FullInstallPath).Any();
}
public static async Task<bool> Install()
{
if (IsInstalled()) return true;
/// <summary>Are we currently fetching releases from GitHub?</summary>
public static bool IsFetchingReleases => _isFetchingReleases;
/// <summary>Cached list of releases (call FetchReleasesAsync to populate).</summary>
public static IReadOnlyList<ReleaseInfo> CachedReleases => _cachedReleases;
/// <summary>
/// Returns the tag currently checked out, or null if unknown / not a git repo.
/// </summary>
public static string GetCurrentTag()
{
if (!IsInstalled()) return null;
try
{
// Create the parent directory if it doesn't exist
Directory.CreateDirectory(Application.dataPath);
// Clone the repository
var result = await RunGitCommandAsync($"clone --recursive {RepoUrl} \"{FullInstallPath}\"", Application.dataPath);
return !result.Contains("error");
string result = RunGitCommandSync("describe --tags --exact-match HEAD", FullInstallPath);
return string.IsNullOrWhiteSpace(result) ? null : result.Trim();
}
catch (Exception e)
catch
{
UnityEngine.Debug.LogError($"Failed to install PSXSplash: {e.Message}");
return false;
return null;
}
}
public static async Task<Dictionary<string, string>> GetBranchesWithLatestCommitsAsync()
{
if (!IsInstalled()) return new Dictionary<string, string>();
// ═══════════════════════════════════════════════════════════════
// Fetch Releases (HTTP — no git required)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Fetches the list of releases from the GitHub REST API.
/// Does NOT require git — uses UnityWebRequest.
/// </summary>
public static async Task<List<ReleaseInfo>> FetchReleasesAsync()
{
_isFetchingReleases = true;
try
{
// Fetch all branches and tags
await RunGitCommandAsync("fetch --all", FullInstallPath);
// Get all remote branches
var branchesOutput = await RunGitCommandAsync("branch -r", FullInstallPath);
var branches = branchesOutput.Split('\n')
.Where(b => !string.IsNullOrEmpty(b.Trim()))
.Select(b => b.Trim().Replace("origin/", ""))
.Where(b => !b.Contains("HEAD"))
.ToList();
var branchesWithCommits = new Dictionary<string, string>();
// Get the latest commit for each branch
foreach (var branch in branches)
string json = await HttpGetAsync(GitHubApiReleasesUrl);
if (string.IsNullOrEmpty(json))
{
var commitOutput = await RunGitCommandAsync($"log origin/{branch} -1 --pretty=format:%h", FullInstallPath);
if (!string.IsNullOrEmpty(commitOutput))
{
branchesWithCommits[branch] = commitOutput.Trim();
}
UnityEngine.Debug.LogWarning("[PSXSplashInstaller] Failed to fetch releases from GitHub.");
return _cachedReleases;
}
return branchesWithCommits;
}
catch (Exception e)
{
UnityEngine.Debug.LogError($"Failed to get branches: {e.Message}");
return new Dictionary<string, string>();
}
}
public static async Task<List<string>> GetReleasesAsync()
{
if (!IsInstalled()) return new List<string>();
try
{
await RunGitCommandAsync("fetch --tags", FullInstallPath);
var output = await RunGitCommandAsync("tag -l", FullInstallPath);
return output.Split('\n')
.Where(t => !string.IsNullOrEmpty(t.Trim()))
.Select(t => t.Trim())
var releases = ParseReleasesJson(json);
// Filter out drafts, sort by newest first
releases = releases
.Where(r => !r.IsDraft)
.OrderByDescending(r => r.PublishedAt)
.ToList();
_cachedReleases = releases;
return releases;
}
catch (Exception e)
catch (Exception ex)
{
UnityEngine.Debug.LogError($"Failed to get releases: {e.Message}");
return new List<string>();
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Error fetching releases: {ex.Message}");
return _cachedReleases;
}
finally
{
_isFetchingReleases = false;
}
}
public static async Task<bool> CheckoutVersionAsync(string version)
// ═══════════════════════════════════════════════════════════════
// Install / Clone at a specific release tag
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Clones the repository at the specified release tag with --recursive.
/// Uses a shallow clone (--depth 1) for speed.
/// Requires git to be installed (submodules cannot be fetched via HTTP archives).
/// </summary>
/// <param name="tag">The release tag to clone, e.g. "v1.2.0". If null, clones the default branch.</param>
/// <param name="onProgress">Optional progress callback.</param>
public static async Task<bool> InstallRelease(string tag, Action<string> onProgress = null)
{
if (!IsInstalled()) return false;
if (IsInstalled())
{
onProgress?.Invoke("Already installed. Use SwitchToRelease to change version.");
return true;
}
if (!IsGitAvailable())
{
UnityEngine.Debug.LogError(
"[PSXSplashInstaller] git is required for recursive submodule clone but was not found on PATH.\n" +
"Please install git: https://git-scm.com/downloads");
return false;
}
try
{
// If it's a branch name, checkout the branch
// If it's a commit hash, checkout the commit
var result = await RunGitCommandAsync($"checkout {version}", FullInstallPath);
var result2 = await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath);
Directory.CreateDirectory(Path.GetDirectoryName(FullInstallPath));
return !result.Contains("error") && !result2.Contains("error");
string branchArg = string.IsNullOrEmpty(tag) ? "" : $"--branch {tag}";
string cmd = $"clone --recursive --depth 1 {branchArg} {RepoUrl} \"{FullInstallPath}\"";
onProgress?.Invoke($"Cloning {RepoUrl} at {tag ?? "HEAD"}...");
string result = await RunGitCommandAsync(cmd, Application.dataPath, onProgress);
if (!IsInstalled())
{
UnityEngine.Debug.LogError("[PSXSplashInstaller] Clone completed but directory is empty.");
return false;
}
onProgress?.Invoke("Clone complete.");
return true;
}
catch (Exception e)
catch (Exception ex)
{
UnityEngine.Debug.LogError($"Failed to checkout version: {e.Message}");
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Clone failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Switches an existing clone to a different release tag.
/// Fetches tags, checks out the tag, and updates submodules recursively.
/// </summary>
public static async Task<bool> SwitchToReleaseAsync(string tag, Action<string> onProgress = null)
{
if (!IsInstalled())
{
UnityEngine.Debug.LogError("[PSXSplashInstaller] Not installed — clone first.");
return false;
}
if (!IsGitAvailable())
{
UnityEngine.Debug.LogError("[PSXSplashInstaller] git not found on PATH.");
return false;
}
try
{
onProgress?.Invoke("Fetching tags...");
await RunGitCommandAsync("fetch --tags --depth=1", FullInstallPath, onProgress);
await RunGitCommandAsync($"fetch origin tag {tag} --no-tags", FullInstallPath, onProgress);
onProgress?.Invoke($"Checking out {tag}...");
await RunGitCommandAsync($"checkout {tag}", FullInstallPath, onProgress);
onProgress?.Invoke("Updating submodules...");
await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath, onProgress);
onProgress?.Invoke($"Switched to {tag}.");
return true;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Switch failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Legacy compatibility: Install without specifying a tag (clones default branch).
/// </summary>
public static Task<bool> Install()
{
return InstallRelease(null);
}
/// <summary>
/// Fetches latest remote data (tags, branches).
/// Requires git.
/// </summary>
public static async Task<bool> FetchLatestAsync()
{
if (!IsInstalled()) return false;
try
{
var result = await RunGitCommandAsync("fetch --all", FullInstallPath);
return !result.Contains("error");
await RunGitCommandAsync("fetch --all --tags", FullInstallPath);
return true;
}
catch (Exception e)
catch (Exception ex)
{
UnityEngine.Debug.LogError($"Failed to fetch latest: {e.Message}");
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Fetch failed: {ex.Message}");
return false;
}
}
private static async Task<string> RunGitCommandAsync(string arguments, string workingDirectory)
// ═══════════════════════════════════════════════════════════════
// Git helpers
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Checks whether git is available on the system PATH.
/// </summary>
public static bool IsGitAvailable()
{
var processInfo = new ProcessStartInfo
try
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var p = Process.Start(psi))
{
p.WaitForExit(5000);
return p.ExitCode == 0;
}
}
catch
{
return false;
}
}
private static string RunGitCommandSync(string arguments, string workingDirectory)
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var process = Process.Start(psi))
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit(10000);
return output;
}
}
private static async Task<string> RunGitCommandAsync(
string arguments, string workingDirectory, Action<string> onProgress = null)
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
@@ -156,48 +314,136 @@ namespace SplashEdit.EditorCode
using (var process = new Process())
{
process.StartInfo = processInfo;
var outputBuilder = new System.Text.StringBuilder();
var errorBuilder = new System.Text.StringBuilder();
process.StartInfo = psi;
process.EnableRaisingEvents = true;
process.OutputDataReceived += (sender, e) =>
var stdout = new System.Text.StringBuilder();
var stderr = new System.Text.StringBuilder();
process.OutputDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
outputBuilder.AppendLine(e.Data);
{
stdout.AppendLine(e.Data);
onProgress?.Invoke(e.Data);
}
};
process.ErrorDataReceived += (sender, e) =>
process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
errorBuilder.AppendLine(e.Data);
{
stderr.AppendLine(e.Data);
// git writes progress to stderr (clone progress, etc.)
onProgress?.Invoke(e.Data);
}
};
var tcs = new TaskCompletionSource<int>();
process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Wait for exit with timeout
var timeout = TimeSpan.FromSeconds(30);
if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(10));
var completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
if (completedTask == timeoutTask)
{
process.WaitForExit(); // Ensure all output is processed
string output = outputBuilder.ToString();
string error = errorBuilder.ToString();
if (!string.IsNullOrEmpty(error))
{
UnityEngine.Debug.LogError($"Git error: {error}");
}
return output;
try { process.Kill(); } catch { }
throw new TimeoutException("Git command timed out after 10 minutes.");
}
int exitCode = await tcs.Task;
process.Dispose();
string output = stdout.ToString();
string error = stderr.ToString();
if (exitCode != 0)
{
UnityEngine.Debug.LogError($"[git {arguments}] exit code {exitCode}\n{error}");
}
return output + error;
}
}
// ═══════════════════════════════════════════════════════════════
// HTTP helpers (no git needed)
// ═══════════════════════════════════════════════════════════════
private static Task<string> HttpGetAsync(string url)
{
var tcs = new TaskCompletionSource<string>();
var request = UnityWebRequest.Get(url);
request.SetRequestHeader("User-Agent", "SplashEdit-Unity");
request.SetRequestHeader("Accept", "application/vnd.github.v3+json");
var op = request.SendWebRequest();
op.completed += _ =>
{
if (request.result == UnityWebRequest.Result.Success)
tcs.TrySetResult(request.downloadHandler.text);
else
{
process.Kill();
throw new TimeoutException("Git command timed out");
UnityEngine.Debug.LogWarning($"[PSXSplashInstaller] HTTP GET {url} failed: {request.error}");
tcs.TrySetResult(null);
}
request.Dispose();
};
return tcs.Task;
}
// ═══════════════════════════════════════════════════════════════
// JSON parsing (minimal, avoids external dependency)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Minimal JSON parser for the GitHub releases API response.
/// Uses Unity's JsonUtility via a wrapper since it can't parse top-level arrays.
/// </summary>
private static List<ReleaseInfo> ParseReleasesJson(string json)
{
var releases = new List<ReleaseInfo>();
string wrapped = "{\"items\":" + json + "}";
var wrapper = JsonUtility.FromJson<GitHubReleaseArrayWrapper>(wrapped);
if (wrapper?.items == null) return releases;
foreach (var item in wrapper.items)
{
releases.Add(new ReleaseInfo
{
TagName = item.tag_name ?? "",
Name = item.name ?? item.tag_name ?? "",
Body = item.body ?? "",
PublishedAt = item.published_at ?? "",
IsPrerelease = item.prerelease,
IsDraft = item.draft
});
}
return releases;
}
[Serializable]
private class GitHubReleaseArrayWrapper
{
public GitHubReleaseJson[] items;
}
[Serializable]
private class GitHubReleaseJson
{
public string tag_name;
public string name;
public string body;
public string published_at;
public bool prerelease;
public bool draft;
}
}
}

View File

@@ -0,0 +1,104 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
[CustomEditor(typeof(PSXTriggerBox))]
public class PSXTriggerBoxEditor : UnityEditor.Editor
{
private SerializedProperty sizeProp;
private SerializedProperty luaFileProp;
private void OnEnable()
{
sizeProp = serializedObject.FindProperty("size");
luaFileProp = serializedObject.FindProperty("luaFile");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Trigger Box", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(sizeProp, new GUIContent("Size"));
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
if (luaFileProp.objectReferenceValue != null)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
if (GUILayout.Button("Clear", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
luaFileProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Create Lua Script", PSXEditorStyles.SecondaryButton, GUILayout.Width(130)))
CreateNewLuaScript();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
private void CreateNewLuaScript()
{
var trigger = target as PSXTriggerBox;
string defaultName = trigger.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script", defaultName + ".lua", "lua",
"Create a new Lua script for this trigger box");
if (string.IsNullOrEmpty(path)) return;
string template =
"function onTriggerEnter(triggerIndex)\nend\n\nfunction onTriggerExit(triggerIndex)\nend\n";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
private static void DrawTriggerGizmo(PSXTriggerBox trigger, GizmoType gizmoType)
{
bool selected = (gizmoType & GizmoType.Selected) != 0;
Gizmos.color = selected ? new Color(0.2f, 1f, 0.3f, 0.8f) : new Color(0.2f, 1f, 0.3f, 0.25f);
Gizmos.matrix = trigger.transform.localToWorldMatrix;
Gizmos.DrawWireCube(Vector3.zero, trigger.Size);
if (selected)
{
Gizmos.color = new Color(0.2f, 1f, 0.3f, 0.08f);
Gizmos.DrawCube(Vector3.zero, trigger.Size);
}
Gizmos.matrix = Matrix4x4.identity;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5bdf647efcaa11a469e2e99025e3a20e

View File

@@ -13,7 +13,6 @@ namespace SplashEdit.EditorCode
private Texture2D quantizedTexture;
private Texture2D vramTexture; // VRAM representation of the texture
private List<VRAMPixel> clut; // Color Lookup Table (CLUT), stored as a 1D list
private ushort[] indexedPixelData; // Indexed pixel data for VRAM storage
private PSXBPP bpp = PSXBPP.TEX_4BIT;
private readonly int previewSize = 256;
@@ -27,19 +26,25 @@ namespace SplashEdit.EditorCode
private void OnGUI()
{
GUILayout.Label("Quantized Preview", EditorStyles.boldLabel);
GUILayout.Label("Quantized Preview", PSXEditorStyles.WindowHeader);
// Texture input field
PSXEditorStyles.BeginCard();
originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
// Dropdown for bit depth selection
bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp);
EditorGUILayout.Space(4);
// Button to generate the quantized preview
if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null)
if (GUILayout.Button("Generate Quantized Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(26)) && originalTexture != null)
{
GenerateQuantizedPreview();
}
PSXEditorStyles.EndCard();
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.BeginHorizontal();
@@ -47,8 +52,8 @@ namespace SplashEdit.EditorCode
if (originalTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("Original Texture");
DrawTexturePreview(originalTexture, previewSize, false);
GUILayout.Label("Original Texture", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(originalTexture, previewSize);
GUILayout.EndVertical();
}
@@ -56,7 +61,7 @@ namespace SplashEdit.EditorCode
if (vramTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("VRAM View (Indexed Data as 16bpp)");
GUILayout.Label("VRAM View (Indexed Data as 16bpp)", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(vramTexture, previewSize);
GUILayout.EndVertical();
}
@@ -65,7 +70,7 @@ namespace SplashEdit.EditorCode
if (quantizedTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("Quantized Texture");
GUILayout.Label("Quantized Texture", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(quantizedTexture, previewSize);
GUILayout.EndVertical();
}
@@ -75,37 +80,17 @@ namespace SplashEdit.EditorCode
// Display the Color Lookup Table (CLUT)
if (clut != null)
{
GUILayout.Label("Color Lookup Table (CLUT)");
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.Label("Color Lookup Table (CLUT)", PSXEditorStyles.SectionHeader);
DrawCLUT();
}
GUILayout.Space(10);
// Export indexed pixel data
if (indexedPixelData != null)
{
if (GUILayout.Button("Export texture data"))
{
string path = EditorUtility.SaveFilePanel("Save texture data", "", "pixel_data", "bin");
if (!string.IsNullOrEmpty(path))
{
using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
foreach (ushort value in indexedPixelData)
{
writer.Write(value);
}
}
}
}
}
PSXEditorStyles.DrawSeparator(4, 4);
// Export CLUT data
if (clut != null)
{
if (GUILayout.Button("Export CLUT data"))
if (GUILayout.Button("Export CLUT data", PSXEditorStyles.SecondaryButton, GUILayout.Height(24)))
{
string path = EditorUtility.SaveFilePanel("Save CLUT data", "", "clut_data", "bin");
@@ -139,7 +124,7 @@ namespace SplashEdit.EditorCode
clut = psxTex.ColorPalette;
}
private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true)
private void DrawTexturePreview(Texture2D texture, int size)
{
// Renders a texture preview within the editor window
Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false));

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SplashEdit.RuntimeCode;
using Unity.Collections;
@@ -19,23 +18,14 @@ namespace SplashEdit.EditorCode
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
private Vector2 scrollPosition;
private Texture2D vramImage;
private Vector2 selectedResolution = new Vector2(320, 240);
private bool dualBuffering = true;
private bool verticalLayout = true;
private static readonly Vector2 selectedResolution = new Vector2(320, 240);
private const bool dualBuffering = true;
private const bool verticalLayout = true;
private Color bufferColor1 = new Color(1, 0, 0, 0.5f);
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
private PSXData _psxData;
private static readonly Vector2[] resolutions =
{
new Vector2(256, 240), new Vector2(256, 480),
new Vector2(320, 240), new Vector2(320, 480),
new Vector2(368, 240), new Vector2(368, 480),
new Vector2(512, 240), new Vector2(512, 480),
new Vector2(640, 240), new Vector2(640, 480)
};
private static string[] resolutionsStrings => resolutions.Select(c => $"{c.x}x{c.y}").ToArray();
private PSXFontData[] _cachedFonts;
[MenuItem("PlayStation 1/VRAM Editor")]
public static void ShowWindow()
@@ -57,7 +47,9 @@ namespace SplashEdit.EditorCode
// Ensure minimum window size is applied.
this.minSize = MinSize;
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
Vector2 ignoredRes;
bool ignoredDb, ignoredVl;
_psxData = DataStorage.LoadData(out ignoredRes, out ignoredDb, out ignoredVl, out prohibitedAreas);
}
/// <summary>
@@ -144,65 +136,75 @@ namespace SplashEdit.EditorCode
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor());
}
}
vramImage.Apply();
// Prompt the user to select a file location and save the VRAM data.
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin");
if (path != string.Empty)
// Overlay custom font textures into the VRAM preview.
// Fonts live at x=960 (4bpp = 64 VRAM hwords wide), stacking from y=0.
PSXFontData[] fonts;
PSXUIExporter.CollectCanvases(selectedResolution, out fonts);
_cachedFonts = fonts;
if (fonts != null && fonts.Length > 0)
{
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
foreach (var font in fonts)
{
for (int y = 0; y < VramHeight; y++)
if (font.PixelData == null || font.PixelData.Length == 0) continue;
int vramX = font.VramX;
int vramY = font.VramY;
int texH = font.TextureHeight;
int bytesPerRow = 256 / 2; // 4bpp: 2 pixels per byte, 256 pixels wide = 128 bytes/row
// Each byte holds two 4bpp pixels. In VRAM, 4 4bpp pixels = 1 16-bit hword.
// So 256 4bpp pixels = 64 VRAM hwords.
for (int y = 0; y < texH && (vramY + y) < VramHeight; y++)
{
for (int x = 0; x < VramWidth; x++)
for (int x = 0; x < 64 && (vramX + x) < VramWidth; x++)
{
writer.Write(packed.vramPixels[x, y].Pack());
// Read 4 4bpp pixels from this VRAM hword position
int byteIdx = y * bytesPerRow + x * 2;
if (byteIdx + 1 >= font.PixelData.Length) continue;
byte b0 = font.PixelData[byteIdx];
byte b1 = font.PixelData[byteIdx + 1];
// Each byte: low nibble = first pixel, high nibble = second
// 4 pixels per hword: b0 low, b0 high, b1 low, b1 high
bool anyOpaque = ((b0 & 0x0F) | (b0 >> 4) | (b1 & 0x0F) | (b1 >> 4)) != 0;
if (anyOpaque)
{
int px = vramX + x;
int py = VramHeight - 1 - (vramY + y);
if (px < VramWidth && py >= 0)
vramImage.SetPixel(px, py, new Color(0.8f, 0.8f, 1f));
}
}
}
}
}
// Also show system font area (960, 464)-(1023, 511) = 64x48
for (int y = 464; y < 512 && y < VramHeight; y++)
{
for (int x = 960; x < 1024 && x < VramWidth; x++)
{
int py = VramHeight - 1 - y;
Color existing = vramImage.GetPixel(x, py);
if (existing.r < 0.01f && existing.g < 0.01f && existing.b < 0.01f)
vramImage.SetPixel(x, py, new Color(0.3f, 0.3f, 0.5f));
}
}
vramImage.Apply();
}
private void OnGUI()
{
GUILayout.BeginHorizontal();
GUILayout.BeginVertical();
GUILayout.Label("VRAM Editor", EditorStyles.boldLabel);
GUILayout.Label("VRAM Editor", PSXEditorStyles.WindowHeader);
GUILayout.Label("320x240, dual-buffered, vertical layout", PSXEditorStyles.InfoBox);
// Dropdown for resolution selection.
selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), resolutionsStrings)];
// Check resolution constraints for dual buffering.
bool canDBHorizontal = selectedResolution.x * 2 <= VramWidth;
bool canDBVertical = selectedResolution.y * 2 <= VramHeight;
if (canDBHorizontal || canDBVertical)
{
dualBuffering = EditorGUILayout.Toggle("Dual Buffering", dualBuffering);
}
else
{
dualBuffering = false;
}
if (canDBVertical && canDBHorizontal)
{
verticalLayout = EditorGUILayout.Toggle("Vertical", verticalLayout);
}
else if (canDBVertical)
{
verticalLayout = true;
}
else
{
verticalLayout = false;
}
GUILayout.Space(10);
GUILayout.Label("Prohibited Areas", EditorStyles.boldLabel);
GUILayout.Space(10);
PSXEditorStyles.DrawSeparator(6, 6);
GUILayout.Label("Prohibited Areas", PSXEditorStyles.SectionHeader);
GUILayout.Space(4);
scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true));
@@ -213,10 +215,7 @@ namespace SplashEdit.EditorCode
{
var area = prohibitedAreas[i];
GUI.backgroundColor = new Color(0.95f, 0.95f, 0.95f);
GUILayout.BeginVertical("box");
GUI.backgroundColor = Color.white;
PSXEditorStyles.BeginCard();
// Display fields for editing the area
area.X = EditorGUILayout.IntField("X Coordinate", area.X);
@@ -224,17 +223,16 @@ namespace SplashEdit.EditorCode
area.Width = EditorGUILayout.IntField("Width", area.Width);
area.Height = EditorGUILayout.IntField("Height", area.Height);
if (GUILayout.Button("Remove", GUILayout.Height(30)))
EditorGUILayout.Space(2);
if (GUILayout.Button("Remove", PSXEditorStyles.DangerButton, GUILayout.Height(24)))
{
toRemove.Add(i); // Mark for removal
}
prohibitedAreas[i] = area;
GUILayout.EndVertical();
GUILayout.Space(10);
PSXEditorStyles.EndCard();
GUILayout.Space(4);
}
// Remove the areas marked for deletion outside the loop to avoid skipping elements
@@ -246,19 +244,23 @@ namespace SplashEdit.EditorCode
GUILayout.EndScrollView();
GUILayout.Space(10);
if (GUILayout.Button("Add Prohibited Area"))
if (GUILayout.Button("Add Prohibited Area", PSXEditorStyles.SecondaryButton))
{
prohibitedAreas.Add(new ProhibitedArea());
}
// Button to initiate texture packing.
if (GUILayout.Button("Pack Textures"))
PSXEditorStyles.DrawSeparator(4, 4);
// Button to pack and preview VRAM layout.
if (GUILayout.Button("Pack Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(28)))
{
PackTextures();
}
// Button to save settings; saving now occurs only on button press.
if (GUILayout.Button("Save Settings"))
EditorGUILayout.Space(2);
// Button to save prohibited areas.
if (GUILayout.Button("Save Settings", PSXEditorStyles.SuccessButton, GUILayout.Height(28)))
{
_psxData.OutputResolution = selectedResolution;
_psxData.DualBuffering = dualBuffering;
@@ -297,6 +299,24 @@ namespace SplashEdit.EditorCode
EditorGUI.DrawRect(areaRect, prohibitedColor);
}
// Draw font region overlays.
if (_cachedFonts != null)
{
Color fontColor = new Color(0.2f, 0.4f, 0.9f, 0.25f);
foreach (var font in _cachedFonts)
{
if (font.PixelData == null || font.PixelData.Length == 0) continue;
Rect fontRect = new Rect(vramRect.x + font.VramX, vramRect.y + font.VramY, 64, font.TextureHeight);
EditorGUI.DrawRect(fontRect, fontColor);
GUI.Label(new Rect(fontRect.x + 2, fontRect.y + 2, 60, 16), "Font", EditorStyles.miniLabel);
}
// System font overlay
Rect sysFontRect = new Rect(vramRect.x + 960, vramRect.y + 464, 64, 48);
EditorGUI.DrawRect(sysFontRect, new Color(0.4f, 0.2f, 0.9f, 0.25f));
GUI.Label(new Rect(sysFontRect.x + 2, sysFontRect.y + 2, 60, 16), "SysFont", EditorStyles.miniLabel);
}
GUILayout.EndHorizontal();
}
}

BIN
Icons/LuaFile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 55ec05596eb659341b8fdb46cd21ab63
guid: 607cfdcd926623447afba2249593f87b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}

BIN
Icons/PSXAudioClip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5a5f4bcf472dcfc44b794a898530a6f0
guid: c1ac35b4ac561a6479df60ee4440f138
TextureImporter:
internalIDToNameTable: []
externalObjects: {}

BIN
Icons/PSXCanvas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

143
Icons/PSXCanvas.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 356cfa78fb65c4141a6163492c5a70c9
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXCutsceneClip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 2e44d4c108f1b3b4bbb11d764ee322ba
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXData.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

143
Icons/PSXData.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 56495f2f7c3b793479704907f633cc9f
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXFontAsset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

143
Icons/PSXFontAsset.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 7e7ebf02d9a128040a98c0e8a77f318b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXInteractable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 2693d84ea56d55f41841bccc513aef7a
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

BIN
Icons/PSXObjectExporter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 5dae4156e1023c34db04e1a0133e8366
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
Icons/PSXPortalLink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: e2c33bdfa2d4f6841abb6f1bd2c3ce4c
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXRoom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

143
Icons/PSXRoom.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: e2a0da16256de3a419a3848add40def9
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
Icons/PSXTriggerBox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 661ef445800490d48bb6486c6b48d7bb
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIBox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

143
Icons/PSXUIBox.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 11ce7fce378375c49a29f10d2c8e1695
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIImage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

143
Icons/PSXUIImage.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: fbae166a0556be14c906804e97f8ce15
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIProgressBar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 4ba32ceba8e78ae4dbad95d3fd57c674
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIText.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

143
Icons/PSXUIText.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 38a1d4112773e114c969699f94844e6a
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
guid: afedc2b61a424884b90aeb912c54fe50
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
guid: 873ed6988ff333343b8a535dcf4919b1
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8
guid: c732908a2854def459b9b5d59ea1d8c6
PluginImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
guid: ba66a74ed9417f2408b71435e402676b
PluginImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -1,5 +1,8 @@
namespace SplashEdit.RuntimeCode
{
// I tried to make this and now I'm scared to delete this.
/// <summary>
/// Implemented by MonoBehaviours that participate in the PSX scene export pipeline.
/// Each exportable object converts its Unity representation into PSX-ready data.

View File

@@ -137,53 +137,55 @@ namespace SplashEdit.RuntimeCode
private class Node
{
public Vector3 Point;
public int Index;
public Node Left, Right;
}
private Node root;
private List<Vector3> points;
public KDTree(List<Vector3> points)
{
this.points = points;
root = Build(points, 0);
var indexed = new List<(Vector3 point, int index)>();
for (int i = 0; i < points.Count; i++)
indexed.Add((points[i], i));
root = Build(indexed, 0);
}
private Node Build(List<Vector3> points, int depth)
private Node Build(List<(Vector3 point, int index)> items, int depth)
{
if (points.Count == 0) return null;
if (items.Count == 0) return null;
int axis = depth % 3;
points.Sort((a, b) => a[axis].CompareTo(b[axis]));
int median = points.Count / 2;
items.Sort((a, b) => a.point[axis].CompareTo(b.point[axis]));
int median = items.Count / 2;
return new Node
{
Point = points[median],
Left = Build(points.Take(median).ToList(), depth + 1),
Right = Build(points.Skip(median + 1).ToList(), depth + 1)
Point = items[median].point,
Index = items[median].index,
Left = Build(items.Take(median).ToList(), depth + 1),
Right = Build(items.Skip(median + 1).ToList(), depth + 1)
};
}
public int FindNearestIndex(Vector3 target)
{
Vector3 nearest = FindNearest(root, target, 0, root.Point);
return points.IndexOf(nearest);
return FindNearest(root, target, 0, root).Index;
}
private Vector3 FindNearest(Node node, Vector3 target, int depth, Vector3 best)
private Node FindNearest(Node node, Vector3 target, int depth, Node best)
{
if (node == null) return best;
if (Vector3.SqrMagnitude(target - node.Point) < Vector3.SqrMagnitude(target - best))
best = node.Point;
if (Vector3.SqrMagnitude(target - node.Point) < Vector3.SqrMagnitude(target - best.Point))
best = node;
int axis = depth % 3;
Node first = target[axis] < node.Point[axis] ? node.Left : node.Right;
Node second = first == node.Left ? node.Right : node.Left;
best = FindNearest(first, target, depth + 1, best);
if (Mathf.Pow(target[axis] - node.Point[axis], 2) < Vector3.SqrMagnitude(target - best))
if (Mathf.Pow(target[axis] - node.Point[axis], 2) < Vector3.SqrMagnitude(target - best.Point))
best = FindNearest(second, target, depth + 1, best);
return best;

View File

@@ -2,6 +2,7 @@ using UnityEngine;
namespace SplashEdit.RuntimeCode
{
[Icon("Packages/net.psxsplash.splashedit/Icons/LuaFile.png")]
public class LuaFile : ScriptableObject
{
[SerializeField] private string luaScript;

View File

@@ -4,8 +4,6 @@ namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Pre-converted audio clip data ready for splashpack serialization.
/// Populated by the Editor (PSXSceneExporter) so Runtime code never
/// touches PSXAudioConverter.
/// </summary>
public struct AudioClipExport
{
@@ -18,10 +16,11 @@ namespace SplashEdit.RuntimeCode
/// <summary>
/// Attach to a GameObject to include an audio clip in the PS1 build.
/// At export time, the AudioClip is converted to SPU ADPCM and packed
/// into the splashpack binary. Use Audio.Play(clipIndex) from Lua.
/// into the splashpack for runtime loading.
/// </summary>
[AddComponentMenu("PSX/Audio Source")]
public class PSXAudioSource : MonoBehaviour
[AddComponentMenu("PSX/PSX Audio Clip")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXAudioClip.png")]
public class PSXAudioClip : MonoBehaviour
{
[Tooltip("Name used to identify this clip in Lua (Audio.Play(\"name\"))." )]
public string ClipName = "";

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0da2803235be654438e86fe9d9a954d4

27
Runtime/PSXAudioEvent.cs Normal file
View File

@@ -0,0 +1,27 @@
using System;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// A frame-based audio trigger within a cutscene.
/// When the cutscene reaches this frame, the named audio clip is played.
/// </summary>
[Serializable]
public class PSXAudioEvent
{
[Tooltip("Frame at which to trigger this audio clip.")]
public int Frame;
[Tooltip("Name of the audio clip (must match a PSXAudioClip ClipName in the scene).")]
public string ClipName = "";
[Tooltip("Playback volume (0 = silent, 128 = max).")]
[Range(0, 128)]
public int Volume = 100;
[Tooltip("Stereo pan (0 = hard left, 64 = center, 127 = hard right).")]
[Range(0, 127)]
public int Pan = 64;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 264e92578fac5014aa24c1e38e116b3b

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3c4c3feb30e8c264baddc3a5e774473b

122
Runtime/PSXCanvas.cs Normal file
View File

@@ -0,0 +1,122 @@
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Marks a Unity Canvas as a PSX UI canvas for splashpack export.
/// Attach to a GameObject that also has a Unity Canvas component.
/// Children with PSXUIImage / PSXUIBox / PSXUIText / PSXUIProgressBar
/// components will be exported as UI elements in this canvas.
/// Auto-configures the Canvas to the PSX resolution from PSXData settings.
/// </summary>
[RequireComponent(typeof(Canvas))]
[DisallowMultipleComponent]
[ExecuteAlways]
[AddComponentMenu("PSX/UI/PSX Canvas")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXCanvas.png")]
public class PSXCanvas : MonoBehaviour
{
[Tooltip("Name used to reference this canvas from Lua (max 24 chars). Must be unique per scene.")]
[SerializeField] private string canvasName = "canvas";
[Tooltip("Whether this canvas is visible when the scene first loads.")]
[SerializeField] private bool startVisible = true;
[Tooltip("Render order (0 = back, higher = front). Canvases render back-to-front.")]
[Range(0, 255)]
[SerializeField] private int sortOrder = 0;
[Tooltip("Optional custom font for text elements in this canvas. If null, uses the built-in system font (8x16).")]
[SerializeField] private PSXFontAsset defaultFont;
/// <summary>Canvas name for Lua access. Truncated to 24 chars on export.</summary>
public string CanvasName => canvasName;
/// <summary>Initial visibility flag written into the splashpack.</summary>
public bool StartVisible => startVisible;
/// <summary>Sort order in 0-255 range.</summary>
public byte SortOrder => (byte)Mathf.Clamp(sortOrder, 0, 255);
/// <summary>Default font for text elements. Null = system font.</summary>
public PSXFontAsset DefaultFont => defaultFont;
/// <summary>
/// PSX target resolution read from the PSXData asset. Falls back to 320x240.
/// Cached per domain reload for efficiency.
/// </summary>
public static Vector2 PSXResolution
{
get
{
if (!s_resolutionCached)
{
s_cachedResolution = LoadResolutionFromProject();
s_resolutionCached = true;
}
return s_cachedResolution;
}
}
private static Vector2 s_cachedResolution = new Vector2(320, 240);
private static bool s_resolutionCached = false;
/// <summary>Invalidate the cached resolution (call when PSXData changes).</summary>
public static void InvalidateResolutionCache()
{
s_resolutionCached = false;
}
private static Vector2 LoadResolutionFromProject()
{
#if UNITY_EDITOR
var data = AssetDatabase.LoadAssetAtPath<PSXData>("Assets/PSXData.asset");
if (data != null)
return data.OutputResolution;
#endif
return new Vector2(320, 240);
}
private void Reset()
{
InvalidateResolutionCache();
ConfigureCanvas();
}
private void OnEnable()
{
ConfigureCanvas();
}
#if UNITY_EDITOR
private void OnValidate()
{
// Delay to avoid modifying in OnValidate directly
UnityEditor.EditorApplication.delayCall += ConfigureCanvas;
}
#endif
/// <summary>
/// Force the Canvas + CanvasScaler to match the PSX resolution from project settings.
/// </summary>
public void ConfigureCanvas()
{
if (this == null) return;
Vector2 res = PSXResolution;
Canvas canvas = GetComponent<Canvas>();
if (canvas != null)
{
canvas.renderMode = RenderMode.WorldSpace;
}
RectTransform rt = GetComponent<RectTransform>();
rt.sizeDelta = new Vector2(res.x, res.y);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dc481397cd94e03409e462478df09d58

88
Runtime/PSXCanvasData.cs Normal file
View File

@@ -0,0 +1,88 @@
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Pre-computed data for one UI canvas and its elements,
/// ready for binary serialization by <see cref="PSXSceneWriter"/>.
/// Populated by <see cref="PSXUIExporter"/> during the export pipeline.
/// </summary>
public struct PSXCanvasData
{
/// <summary>Canvas name (max 24 chars, truncated on export).</summary>
public string Name;
/// <summary>Initial visibility flag.</summary>
public bool StartVisible;
/// <summary>Sort order (0 = back, 255 = front).</summary>
public byte SortOrder;
/// <summary>Exported elements belonging to this canvas.</summary>
public PSXUIElementData[] Elements;
}
/// <summary>
/// Pre-computed data for one UI element, ready for binary serialization.
/// Matches the 48-byte on-disk element record parsed by uisystem.cpp.
/// </summary>
public struct PSXUIElementData
{
// Identity
public PSXUIElementType Type;
public bool StartVisible;
public string Name; // max 24 chars
// Layout (PS1 pixel coords, already Y-inverted)
public short X, Y, W, H;
// Anchors (8.8 fixed-point: 0=0.0, 128=0.5, 255≈1.0)
public byte AnchorMinX, AnchorMinY;
public byte AnchorMaxX, AnchorMaxY;
// Primary color (RGB)
public byte ColorR, ColorG, ColorB;
// Type-specific: Image
public byte TexpageX, TexpageY;
public ushort ClutX, ClutY;
public byte U0, V0, U1, V1;
public byte BitDepthIndex; // 0=4bit, 1=8bit, 2=16bit
// Type-specific: Progress
public byte BgR, BgG, BgB;
public byte ProgressValue;
// Type-specific: Text
public string DefaultText; // max 63 chars
public byte FontIndex; // 0 = system font, 1+ = custom font
}
/// <summary>
/// Export data for a custom font to be embedded in the splashpack.
/// </summary>
public struct PSXFontData
{
/// <summary>Source font asset (for identification/dedup).</summary>
public PSXFontAsset Source;
/// <summary>Glyph cell width in pixels.</summary>
public byte GlyphWidth;
/// <summary>Glyph cell height in pixels.</summary>
public byte GlyphHeight;
/// <summary>VRAM X position for upload (16-bit pixel units).</summary>
public ushort VramX;
/// <summary>VRAM Y position for upload (16-bit pixel units).</summary>
public ushort VramY;
/// <summary>Texture height in pixels (width is always 256 in 4bpp = 64 VRAM hwords).</summary>
public ushort TextureHeight;
/// <summary>Packed 4bpp pixel data ready for VRAM upload.</summary>
public byte[] PixelData;
/// <summary>Per-character advance widths (96 entries, ASCII 0x20-0x7F) for proportional rendering.</summary>
public byte[] AdvanceWidths;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f4142a9858eccd04eac0b826af8f3621

View File

@@ -15,7 +15,6 @@ namespace SplashEdit.RuntimeCode
Solid = 0x01,
Slope = 0x02,
Stairs = 0x04,
Trigger = 0x08,
NoWalk = 0x10,
}
@@ -84,14 +83,17 @@ namespace SplashEdit.RuntimeCode
foreach (var exporter in exporters)
{
// Dynamic objects use runtime AABB colliders, skip them
if (exporter.CollisionType == PSXCollisionType.Dynamic)
continue;
PSXCollisionType effectiveType = exporter.CollisionType;
if (effectiveType == PSXCollisionType.None)
{
if (autoIncludeSolid)
{
// Auto-include as Solid so all geometry blocks the player
effectiveType = PSXCollisionType.Solid;
effectiveType = PSXCollisionType.Static;
autoIncluded++;
}
else
@@ -100,11 +102,8 @@ namespace SplashEdit.RuntimeCode
}
}
// Get the collision mesh (custom or render mesh)
MeshFilter mf = exporter.GetComponent<MeshFilter>();
Mesh collisionMesh = exporter.CustomCollisionMesh != null
? exporter.CustomCollisionMesh
: mf?.sharedMesh;
Mesh collisionMesh = mf?.sharedMesh;
if (collisionMesh == null)
continue;
@@ -130,38 +129,25 @@ namespace SplashEdit.RuntimeCode
// Determine surface flags
byte flags = 0;
if (effectiveType == PSXCollisionType.Trigger)
// Floor-like: normal.y > cosWalkable
float dotUp = normal.y;
if (dotUp > cosWalkable)
{
flags = (byte)PSXSurfaceFlag.Trigger;
flags = (byte)PSXSurfaceFlag.Solid;
if (dotUp < 0.95f && dotUp > cosWalkable)
{
flags |= (byte)PSXSurfaceFlag.Stairs;
}
}
else if (dotUp > 0.0f)
{
flags = (byte)(PSXSurfaceFlag.Solid | PSXSurfaceFlag.Slope);
}
else
{
// Floor-like: normal.y > cosWalkable
// Note: Unity Y is up; PS1 Y is down. We export in Unity space
// and convert to PS1 space during WriteToBinary.
float dotUp = normal.y;
if (dotUp > cosWalkable)
{
flags = (byte)PSXSurfaceFlag.Solid;
// Check if stairs (tagged on exporter or steep-ish)
if (exporter.ObjectFlags.HasFlag(PSXObjectFlags.Static) &&
dotUp < 0.95f && dotUp > cosWalkable)
{
flags |= (byte)PSXSurfaceFlag.Stairs;
}
}
else if (dotUp > 0.0f)
{
// Slope too steep to walk on
flags = (byte)(PSXSurfaceFlag.Solid | PSXSurfaceFlag.Slope);
}
else
{
// Wall or ceiling
flags = (byte)PSXSurfaceFlag.Solid;
}
flags = (byte)PSXSurfaceFlag.Solid;
}
_allTriangles.Add(new CollisionTriExport

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// A cutscene asset containing keyframed tracks and audio events.
/// Create via right-click → Create → PSX → Cutscene Clip.
/// Reference these assets anywhere in the project; the exporter collects
/// all PSXCutsceneClip assets via Resources.FindObjectsOfTypeAll.
/// </summary>
[CreateAssetMenu(fileName = "NewCutscene", menuName = "PSX/Cutscene Clip", order = 100)]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXCutsceneClip.png")]
public class PSXCutsceneClip : ScriptableObject
{
[Tooltip("Name used to reference this cutscene from Lua (max 24 chars). Must be unique per scene.")]
public string CutsceneName = "cutscene";
[Tooltip("Total duration in frames at 30fps. E.g. 90 = 3 seconds.")]
public int DurationFrames = 90;
[Tooltip("Tracks driving properties over time.")]
public List<PSXCutsceneTrack> Tracks = new List<PSXCutsceneTrack>();
[Tooltip("Audio events triggered at specific frames.")]
public List<PSXAudioEvent> AudioEvents = new List<PSXAudioEvent>();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 99c0b28de0bbbf7449afc28106b605dc

View File

@@ -0,0 +1,389 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Serializes PSXCutsceneClip data into the splashpack v12 binary format.
/// Called from PSXSceneWriter.Write() after all other data sections.
/// </summary>
public static class PSXCutsceneExporter
{
// Match C++ limits
private const int MAX_CUTSCENES = 16;
private const int MAX_TRACKS = 8;
private const int MAX_KEYFRAMES = 64;
private const int MAX_AUDIO_EVENTS = 64;
private const int MAX_NAME_LEN = 24;
/// <summary>
/// Angle conversion: degrees to psyqo::Angle raw value.
/// psyqo::Angle = FixedPoint&lt;10&gt;, stored in pi-units.
/// 1.0_pi = 1024 raw = 180 degrees. So: raw = degrees * 1024 / 180.
/// </summary>
private static short DegreesToAngleRaw(float degrees)
{
float raw = degrees * 1024.0f / 180.0f;
return (short)Mathf.Clamp(Mathf.RoundToInt(raw), -32768, 32767);
}
/// <summary>
/// Write all cutscene data and return the byte position of the cutscene table
/// so the header can be backfilled.
/// </summary>
/// <param name="writer">Binary writer positioned after all prior sections.</param>
/// <param name="cutscenes">Cutscene clips to export (may be null/empty).</param>
/// <param name="exporters">Scene object exporters for name validation.</param>
/// <param name="audioSources">Audio sources for clip name → index resolution.</param>
/// <param name="gteScaling">GTE scaling factor.</param>
/// <param name="cutsceneTableStart">Returns the file position where the cutscene table starts.</param>
/// <param name="log">Optional log callback.</param>
public static void ExportCutscenes(
BinaryWriter writer,
PSXCutsceneClip[] cutscenes,
PSXObjectExporter[] exporters,
PSXAudioClip[] audioSources,
float gteScaling,
out long cutsceneTableStart,
Action<string, LogType> log = null)
{
cutsceneTableStart = 0;
if (cutscenes == null || cutscenes.Length == 0)
return;
if (cutscenes.Length > MAX_CUTSCENES)
{
log?.Invoke($"Too many cutscenes ({cutscenes.Length} > {MAX_CUTSCENES}). Only the first {MAX_CUTSCENES} will be exported.", LogType.Warning);
var trimmed = new PSXCutsceneClip[MAX_CUTSCENES];
Array.Copy(cutscenes, trimmed, MAX_CUTSCENES);
cutscenes = trimmed;
}
// Build audio source name → index lookup
Dictionary<string, int> audioNameToIndex = new Dictionary<string, int>();
if (audioSources != null)
{
for (int i = 0; i < audioSources.Length; i++)
{
if (!string.IsNullOrEmpty(audioSources[i].ClipName) && !audioNameToIndex.ContainsKey(audioSources[i].ClipName))
audioNameToIndex[audioSources[i].ClipName] = i;
}
}
AlignToFourBytes(writer);
// ── Cutscene Table ──
cutsceneTableStart = writer.BaseStream.Position;
// SPLASHPACKCutsceneEntry: 12 bytes each
// Write placeholders first, then backfill
long[] entryPositions = new long[cutscenes.Length];
for (int i = 0; i < cutscenes.Length; i++)
{
entryPositions[i] = writer.BaseStream.Position;
writer.Write((uint)0); // dataOffset placeholder
writer.Write((byte)0); // nameLen placeholder
writer.Write((byte)0); // pad
writer.Write((byte)0); // pad
writer.Write((byte)0); // pad
writer.Write((uint)0); // nameOffset placeholder
}
// ── Per-cutscene data ──
for (int ci = 0; ci < cutscenes.Length; ci++)
{
PSXCutsceneClip clip = cutscenes[ci];
AlignToFourBytes(writer);
// Record data offset
long dataPos = writer.BaseStream.Position;
// Validate and clamp
int trackCount = Mathf.Min(clip.Tracks?.Count ?? 0, MAX_TRACKS);
int audioEventCount = 0;
// Count valid audio events
List<PSXAudioEvent> validEvents = new List<PSXAudioEvent>();
if (clip.AudioEvents != null)
{
foreach (var evt in clip.AudioEvents)
{
if (audioNameToIndex.ContainsKey(evt.ClipName))
{
validEvents.Add(evt);
if (validEvents.Count >= MAX_AUDIO_EVENTS) break;
}
else
{
log?.Invoke($"Cutscene '{clip.CutsceneName}': audio event clip '{evt.ClipName}' not found in scene audio sources. Skipping.", LogType.Warning);
}
}
}
audioEventCount = validEvents.Count;
// Sort audio events by frame (required for linear scan on PS1)
validEvents.Sort((a, b) => a.Frame.CompareTo(b.Frame));
// SPLASHPACKCutscene: 12 bytes
long tracksOffsetPlaceholder;
long audioEventsOffsetPlaceholder;
writer.Write((ushort)clip.DurationFrames);
writer.Write((byte)trackCount);
writer.Write((byte)audioEventCount);
tracksOffsetPlaceholder = writer.BaseStream.Position;
writer.Write((uint)0); // tracksOffset placeholder
audioEventsOffsetPlaceholder = writer.BaseStream.Position;
writer.Write((uint)0); // audioEventsOffset placeholder
// ── Tracks ──
AlignToFourBytes(writer);
long tracksStart = writer.BaseStream.Position;
// SPLASHPACKCutsceneTrack: 12 bytes each
long[] trackObjectNameOffsets = new long[trackCount];
long[] trackKeyframesOffsets = new long[trackCount];
for (int ti = 0; ti < trackCount; ti++)
{
PSXCutsceneTrack track = clip.Tracks[ti];
string objName = GetTrackTargetName(track);
int kfCount = Mathf.Min(track.Keyframes?.Count ?? 0, MAX_KEYFRAMES);
writer.Write((byte)track.TrackType);
writer.Write((byte)kfCount);
writer.Write((byte)objName.Length);
writer.Write((byte)0); // pad
trackObjectNameOffsets[ti] = writer.BaseStream.Position;
writer.Write((uint)0); // objectNameOffset placeholder
trackKeyframesOffsets[ti] = writer.BaseStream.Position;
writer.Write((uint)0); // keyframesOffset placeholder
}
// ── Keyframe data (per track) ──
for (int ti = 0; ti < trackCount; ti++)
{
PSXCutsceneTrack track = clip.Tracks[ti];
int kfCount = Mathf.Min(track.Keyframes?.Count ?? 0, MAX_KEYFRAMES);
AlignToFourBytes(writer);
long kfStart = writer.BaseStream.Position;
// Sort keyframes by frame
var sortedKf = new List<PSXKeyframe>(track.Keyframes ?? new List<PSXKeyframe>());
sortedKf.Sort((a, b) => a.Frame.CompareTo(b.Frame));
for (int ki = 0; ki < kfCount; ki++)
{
PSXKeyframe kf = sortedKf[ki];
// Pack interp mode into upper 3 bits, frame into lower 13 bits
ushort frameAndInterp = (ushort)((((int)kf.Interp & 0x7) << 13) | (kf.Frame & 0x1FFF));
writer.Write(frameAndInterp);
switch (track.TrackType)
{
case PSXTrackType.CameraPosition:
case PSXTrackType.ObjectPosition:
{
// Position: convert to fp12, negate Y for PSX coords
float gte = gteScaling;
short px = PSXTrig.ConvertCoordinateToPSX(kf.Value.x, gte);
short py = PSXTrig.ConvertCoordinateToPSX(-kf.Value.y, gte);
short pz = PSXTrig.ConvertCoordinateToPSX(kf.Value.z, gte);
writer.Write(px);
writer.Write(py);
writer.Write(pz);
break;
}
case PSXTrackType.CameraRotation:
{
// Rotation: degrees → psyqo::Angle raw (pi-units)
short rx = DegreesToAngleRaw(kf.Value.x);
short ry = DegreesToAngleRaw(kf.Value.y);
short rz = DegreesToAngleRaw(kf.Value.z);
writer.Write(rx);
writer.Write(ry);
writer.Write(rz);
break;
}
case PSXTrackType.ObjectRotation:
{
// Full XYZ rotation in degrees -> pi-units
short rx = DegreesToAngleRaw(kf.Value.x);
short ry = DegreesToAngleRaw(kf.Value.y);
short rz = DegreesToAngleRaw(kf.Value.z);
writer.Write(rx);
writer.Write(ry);
writer.Write(rz);
break;
}
case PSXTrackType.ObjectActive:
{
writer.Write((short)(kf.Value.x > 0.5f ? 1 : 0));
writer.Write((short)0);
writer.Write((short)0);
break;
}
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
{
// Step: values[0] = 0 or 1
writer.Write((short)(kf.Value.x > 0.5f ? 1 : 0));
writer.Write((short)0);
writer.Write((short)0);
break;
}
case PSXTrackType.UIProgress:
{
// values[0] = progress 0-100 as int16
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 100));
writer.Write((short)0);
writer.Write((short)0);
break;
}
case PSXTrackType.UIPosition:
{
// values[0] = x, values[1] = y (PSX screen coordinates, raw int16)
writer.Write((short)Mathf.RoundToInt(kf.Value.x));
writer.Write((short)Mathf.RoundToInt(kf.Value.y));
writer.Write((short)0);
break;
}
case PSXTrackType.UIColor:
{
// values[0] = r, values[1] = g, values[2] = b (0-255)
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 255));
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.y), 0, 255));
writer.Write((short)Mathf.Clamp(Mathf.RoundToInt(kf.Value.z), 0, 255));
break;
}
}
}
// Backfill keyframes offset
{
long curPos = writer.BaseStream.Position;
writer.Seek((int)trackKeyframesOffsets[ti], SeekOrigin.Begin);
writer.Write((uint)kfStart);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
// ── Object / UI target name strings (per track) ──
for (int ti = 0; ti < trackCount; ti++)
{
PSXCutsceneTrack track = clip.Tracks[ti];
string objName = GetTrackTargetName(track);
if (objName.Length > 0)
{
long namePos = writer.BaseStream.Position;
byte[] nameBytes = Encoding.UTF8.GetBytes(objName);
writer.Write(nameBytes);
writer.Write((byte)0); // null terminator
long curPos = writer.BaseStream.Position;
writer.Seek((int)trackObjectNameOffsets[ti], SeekOrigin.Begin);
writer.Write((uint)namePos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
// else: objectNameOffset stays 0
}
// ── Audio events ──
AlignToFourBytes(writer);
long audioEventsStart = writer.BaseStream.Position;
foreach (var evt in validEvents)
{
int clipIdx = audioNameToIndex[evt.ClipName];
writer.Write((ushort)evt.Frame);
writer.Write((byte)clipIdx);
writer.Write((byte)Mathf.Clamp(evt.Volume, 0, 128));
writer.Write((byte)Mathf.Clamp(evt.Pan, 0, 127));
writer.Write((byte)0); // pad
writer.Write((byte)0); // pad
writer.Write((byte)0); // pad
}
// ── Cutscene name string ──
string csName = clip.CutsceneName ?? "unnamed";
if (csName.Length > MAX_NAME_LEN) csName = csName.Substring(0, MAX_NAME_LEN);
long nameStartPos = writer.BaseStream.Position;
byte[] csNameBytes = Encoding.UTF8.GetBytes(csName);
writer.Write(csNameBytes);
writer.Write((byte)0); // null terminator
// ── Backfill SPLASHPACKCutscene offsets ──
{
long curPos = writer.BaseStream.Position;
// tracksOffset
writer.Seek((int)tracksOffsetPlaceholder, SeekOrigin.Begin);
writer.Write((uint)tracksStart);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
{
long curPos = writer.BaseStream.Position;
// audioEventsOffset
writer.Seek((int)audioEventsOffsetPlaceholder, SeekOrigin.Begin);
writer.Write((uint)(audioEventCount > 0 ? audioEventsStart : 0));
writer.Seek((int)curPos, SeekOrigin.Begin);
}
// ── Backfill cutscene table entry ──
{
long curPos = writer.BaseStream.Position;
writer.Seek((int)entryPositions[ci], SeekOrigin.Begin);
writer.Write((uint)dataPos); // dataOffset
writer.Write((byte)csNameBytes.Length); // nameLen
writer.Write((byte)0); // pad
writer.Write((byte)0); // pad
writer.Write((byte)0); // pad
writer.Write((uint)nameStartPos); // nameOffset
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
log?.Invoke($"{cutscenes.Length} cutscene(s) exported.", LogType.Log);
}
private static void AlignToFourBytes(BinaryWriter writer)
{
long pos = writer.BaseStream.Position;
int padding = (int)(4 - (pos % 4)) % 4;
if (padding > 0)
writer.Write(new byte[padding]);
}
/// <summary>
/// Get the target name string for a track.
/// Camera tracks: empty. Object tracks: ObjectName.
/// UICanvasVisible: UICanvasName.
/// UI element tracks: "UICanvasName/UIElementName".
/// </summary>
private static string GetTrackTargetName(PSXCutsceneTrack track)
{
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
if (isCameraTrack) return "";
string name;
if (track.IsUIElementTrack)
name = (track.UICanvasName ?? "") + "/" + (track.UIElementName ?? "");
else if (track.IsUITrack)
name = track.UICanvasName ?? "";
else
name = track.ObjectName ?? "";
if (name.Length > MAX_NAME_LEN)
name = name.Substring(0, MAX_NAME_LEN);
return name;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d915e0da5e09ea348a020d077f0725da

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// A single track within a cutscene, driving one property on one target.
/// </summary>
[Serializable]
public class PSXCutsceneTrack
{
[Tooltip("What property this track drives.")]
public PSXTrackType TrackType;
[Tooltip("Target GameObject name (must match a PSXObjectExporter). Leave empty for camera/UI tracks.")]
public string ObjectName = "";
[Tooltip("For UI tracks: canvas name (e.g. 'hud'). Used by UICanvasVisible and to resolve elements.")]
public string UICanvasName = "";
[Tooltip("For UI element tracks: element name within the canvas. Used by UIElementVisible, UIProgress, UIPosition, UIColor.")]
public string UIElementName = "";
[Tooltip("Keyframes for this track. Sort by frame number.")]
public List<PSXKeyframe> Keyframes = new List<PSXKeyframe>();
/// <summary>Returns true if this track type targets a UI canvas or element.</summary>
public bool IsUITrack => TrackType >= PSXTrackType.UICanvasVisible && TrackType <= PSXTrackType.UIColor;
/// <summary>Returns true if this track type targets a UI element (not just a canvas).</summary>
public bool IsUIElementTrack => TrackType >= PSXTrackType.UIElementVisible && TrackType <= PSXTrackType.UIColor;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c289e12325ed44499d49bc7a570c8de

View File

@@ -5,6 +5,7 @@ namespace SplashEdit.RuntimeCode
{
[CreateAssetMenu(fileName = "PSXData", menuName = "PSXSplash/PS1 Project Data")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXData.png")]
public class PSXData : ScriptableObject
{

458
Runtime/PSXFontAsset.cs Normal file
View File

@@ -0,0 +1,458 @@
using UnityEngine;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SplashEdit.RuntimeCode
{
[CreateAssetMenu(fileName = "New PSXFont", menuName = "PSX/Font Asset")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXFontAsset.png")]
public class PSXFontAsset : ScriptableObject
{
[Header("Source - Option A: TrueType/OTF Font")]
[Tooltip("Assign a Unity Font asset (TTF/OTF). Click 'Generate Bitmap' to rasterize.")]
[SerializeField] private Font sourceFont;
[Tooltip("Font size in pixels. Larger = more detail but uses more VRAM.\n" +
"The actual glyph cell size is auto-computed to fit within PS1 texture page limits.")]
[Range(6, 32)]
[SerializeField] private int fontSize = 16;
[Header("Source - Option B: Manual Bitmap")]
[Tooltip("Font bitmap texture. Must be 256 pixels wide.\n" +
"Glyphs in ASCII order from 0x20, transparent = bg, opaque = fg.")]
[SerializeField] private Texture2D fontTexture;
[Header("Glyph Metrics")]
[Tooltip("Width of each glyph cell (auto-set from font, editable for manual bitmap).\n" +
"Must divide 256 evenly: 4, 8, 16, or 32.")]
[SerializeField] private int glyphWidth = 8;
[Tooltip("Height of each glyph cell (auto-set from font, editable for manual bitmap).")]
[SerializeField] private int glyphHeight = 16;
[HideInInspector]
[SerializeField] private byte[] storedAdvanceWidths;
// Valid glyph widths: must divide 256 evenly for PSYQo texture UV wrapping.
private static readonly int[] ValidGlyphWidths = { 4, 8, 16, 32 };
// PS1 texture page is 256 pixels tall. Font texture MUST fit in one page.
private const int MAX_TEXTURE_PAGE_HEIGHT = 256;
public Font SourceFont => sourceFont;
public int FontSize => fontSize;
public Texture2D FontTexture => fontTexture;
public int GlyphWidth => glyphWidth;
public int GlyphHeight => glyphHeight;
/// <summary>Per-character advance widths (96 entries, ASCII 0x20-0x7F). Computed during generation.</summary>
public byte[] AdvanceWidths => storedAdvanceWidths;
public int GlyphsPerRow => 256 / glyphWidth;
public int RowCount => Mathf.CeilToInt(95f / GlyphsPerRow);
public int TextureHeight => RowCount * glyphHeight;
#if UNITY_EDITOR
public void GenerateBitmapFromFont()
{
if (sourceFont == null)
{
Debug.LogWarning("PSXFontAsset: No source font assigned.");
return;
}
// ── Step 1: Populate the font atlas ──
string ascii = "";
for (int c = 0x20; c <= 0x7E; c++) ascii += (char)c;
sourceFont.RequestCharactersInTexture(ascii, fontSize, FontStyle.Normal);
// ── Step 2: Get readable copy of atlas texture ──
// For non-dynamic fonts, the atlas may only be populated at the native size.
// Try the requested size first, then fall back to size=0.
Texture fontTex = sourceFont.material != null ? sourceFont.material.mainTexture : null;
if (fontTex == null || fontTex.width == 0 || fontTex.height == 0)
{
// Retry with size=0 (native size) for non-dynamic fonts
sourceFont.RequestCharactersInTexture(ascii, 0, FontStyle.Normal);
fontTex = sourceFont.material != null ? sourceFont.material.mainTexture : null;
}
if (fontTex == null)
{
Debug.LogError("PSXFontAsset: Font atlas is null. Set Character to 'ASCII Default Set' in font import settings.");
return;
}
int fontTexW = fontTex.width;
int fontTexH = fontTex.height;
if (fontTexW == 0 || fontTexH == 0)
{
Debug.LogError("PSXFontAsset: Font atlas has zero dimensions. Try re-importing the font with 'ASCII Default Set'.");
return;
}
Color[] fontPixels;
{
RenderTexture rt = RenderTexture.GetTemporary(fontTexW, fontTexH, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(fontTex, rt);
RenderTexture prev = RenderTexture.active;
RenderTexture.active = rt;
Texture2D readable = new Texture2D(fontTexW, fontTexH, TextureFormat.RGBA32, false);
readable.ReadPixels(new Rect(0, 0, fontTexW, fontTexH), 0, 0);
readable.Apply();
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
fontPixels = readable.GetPixels();
DestroyImmediate(readable);
}
// Verify atlas isn't blank
bool hasAnyPixel = false;
for (int i = 0; i < fontPixels.Length && !hasAnyPixel; i++)
{
if (fontPixels[i].a > 0.1f) hasAnyPixel = true;
}
if (!hasAnyPixel)
{
Debug.LogError("PSXFontAsset: Font atlas is blank. Set Character to 'ASCII Default Set' in font import settings.");
return;
}
// ── Step 3: Get character info ──
// Non-dynamic fonts only respond to size=0 or their native size.
// Dynamic fonts respond to any size.
CharacterInfo[] charInfos = new CharacterInfo[95];
bool[] charValid = new bool[95];
int validCount = 0;
int workingSize = fontSize;
// Try requested fontSize first
for (int c = 0x20; c <= 0x7E; c++)
{
int idx = c - 0x20;
if (sourceFont.GetCharacterInfo((char)c, out charInfos[idx], fontSize, FontStyle.Normal))
{
charValid[idx] = true;
validCount++;
}
}
// If that failed, try size=0 (non-dynamic fonts need this)
if (validCount == 0)
{
sourceFont.RequestCharactersInTexture(ascii, 0, FontStyle.Normal);
for (int c = 0x20; c <= 0x7E; c++)
{
int idx = c - 0x20;
if (sourceFont.GetCharacterInfo((char)c, out charInfos[idx], 0, FontStyle.Normal))
{
charValid[idx] = true;
validCount++;
}
}
if (validCount > 0)
{
workingSize = 0;
}
}
// Last resort: read characterInfo array directly
if (validCount == 0 && sourceFont.characterInfo != null)
{
foreach (CharacterInfo fci in sourceFont.characterInfo)
{
int c = fci.index;
if (c >= 0x20 && c <= 0x7E)
{
charInfos[c - 0x20] = fci;
charValid[c - 0x20] = true;
validCount++;
}
}
}
if (validCount == 0)
{
Debug.LogError("PSXFontAsset: Could not get character info from font.");
return;
}
// ── Step 4: Choose glyph cell dimensions ──
// Constraints:
// - glyphWidth must divide 256 (valid: 4, 8, 16, 32)
// - ceil(95 / (256/glyphWidth)) * glyphHeight <= 256 (must fit in one texture page)
// - glyphHeight in [4, 32]
// Strategy: pick the smallest valid width where everything fits.
// Glyphs that exceed the cell are scaled to fit.
int measuredMaxW = 0, measuredMaxH = 0;
for (int idx = 1; idx < 95; idx++) // skip space
{
if (!charValid[idx]) continue;
CharacterInfo ci = charInfos[idx];
int pw = Mathf.Abs(ci.maxX - ci.minX);
int ph = Mathf.Abs(ci.maxY - ci.minY);
if (pw > measuredMaxW) measuredMaxW = pw;
if (ph > measuredMaxH) measuredMaxH = ph;
}
// Target height based on measured glyphs + margin
int targetH = Mathf.Clamp(measuredMaxH + 2, 4, 32);
// Find the best valid width: start from the IDEAL (closest to measured width)
// and go smaller only if the texture wouldn't fit in 256px vertically.
// This maximizes glyph quality by using the widest cells that fit.
int bestW = -1, bestH = -1;
// Find ideal: smallest valid width >= measured glyph width
int idealIdx = ValidGlyphWidths.Length - 1; // default to largest (32)
for (int i = 0; i < ValidGlyphWidths.Length; i++)
{
if (ValidGlyphWidths[i] >= measuredMaxW)
{
idealIdx = i;
break;
}
}
// Try from ideal downward until we find one that fits
for (int i = idealIdx; i >= 0; i--)
{
int vw = ValidGlyphWidths[i];
int perRow = 256 / vw;
int rows = Mathf.CeilToInt(95f / perRow);
int totalH = rows * targetH;
if (totalH <= MAX_TEXTURE_PAGE_HEIGHT)
{
bestW = vw;
bestH = targetH;
break;
}
}
// If nothing fits even at width=4, clamp height
if (bestW < 0)
{
bestW = 4;
int rows4 = Mathf.CeilToInt(95f / 64); // 64 per row at width 4
bestH = Mathf.Clamp(MAX_TEXTURE_PAGE_HEIGHT / rows4, 4, 32);
Debug.LogWarning($"PSXFontAsset: Font too large for PS1 texture page. " +
$"Clamping to {bestW}x{bestH} cells.");
}
glyphWidth = bestW;
glyphHeight = bestH;
int texW = 256;
int glyphsPerRow = texW / glyphWidth;
int rowCount = Mathf.CeilToInt(95f / glyphsPerRow);
int texH = rowCount * glyphHeight;
// Compute baseline metrics for proper vertical positioning.
// Characters sit on a common baseline. Ascenders go up, descenders go down.
int maxAscender = 0; // highest point above baseline (positive)
int maxDescender = 0; // lowest point below baseline (negative)
for (int idx = 1; idx < 95; idx++)
{
if (!charValid[idx]) continue;
CharacterInfo ci = charInfos[idx];
if (ci.maxY > maxAscender) maxAscender = ci.maxY;
if (ci.minY < maxDescender) maxDescender = ci.minY;
}
int totalFontH = maxAscender - maxDescender;
// Vertical scale only if font exceeds cell height
float vScale = 1f;
int usableH = glyphHeight - 2;
if (totalFontH > usableH)
vScale = (float)usableH / totalFontH;
// NO horizontal scaling. Glyphs rendered at native width, left-aligned.
// This makes the native advance widths match the bitmap exactly for
// proportional rendering. Characters wider than cell get clipped (rare).
// ── Step 5: Render glyphs into grid ──
// Each glyph is LEFT-ALIGNED at native width for proportional rendering.
// The advance widths from CharacterInfo match native glyph proportions.
Texture2D bmp = new Texture2D(texW, texH, TextureFormat.RGBA32, false);
bmp.filterMode = FilterMode.Point;
bmp.wrapMode = TextureWrapMode.Clamp;
Color[] clearPixels = new Color[texW * texH];
bmp.SetPixels(clearPixels);
int renderedCount = 0;
for (int idx = 0; idx < 95; idx++)
{
if (!charValid[idx]) continue;
CharacterInfo ci = charInfos[idx];
int col = idx % glyphsPerRow;
int row = idx / glyphsPerRow;
int cellX = col * glyphWidth;
int cellY = row * glyphHeight;
int gw = Mathf.Abs(ci.maxX - ci.minX);
int gh = Mathf.Abs(ci.maxY - ci.minY);
if (gw <= 0 || gh <= 0) continue;
// Use all four UV corners to handle atlas rotation.
// Unity's atlas packer can rotate glyphs 90 degrees to pack efficiently.
// Wide characters like 'm' and 'M' are commonly rotated.
// Bilinear interpolation across the UV quad handles any orientation.
Vector2 uvBL = ci.uvBottomLeft;
Vector2 uvBR = ci.uvBottomRight;
Vector2 uvTL = ci.uvTopLeft;
Vector2 uvTR = ci.uvTopRight;
// Native width (clipped to cell), scaled height
int renderW = Mathf.Min(gw, glyphWidth);
int renderH = Mathf.Max(1, Mathf.RoundToInt(gh * vScale));
// Y offset: baseline positioning
int baselineFromTop = 1 + Mathf.RoundToInt(maxAscender * vScale);
int glyphTopFromBaseline = Mathf.RoundToInt(ci.maxY * vScale);
int offsetY = baselineFromTop - glyphTopFromBaseline;
if (offsetY < 0) offsetY = 0;
// Include left bearing so glyph sits at correct position within
// the advance space. Negative bearing (left overhang) clamped to 0.
int offsetX = Mathf.Max(0, ci.minX);
bool anyPixel = false;
for (int py = 0; py < renderH && (offsetY + py) < glyphHeight; py++)
{
for (int px = 0; px < renderW && (offsetX + px) < glyphWidth; px++)
{
// Scale to fit if glyph wider than cell, 1:1 otherwise
float srcU = (px + 0.5f) / renderW;
float srcV = (py + 0.5f) / renderH;
// Bilinear interpolation across the UV quad (handles rotation)
// Bottom edge: lerp BL->BR by srcU
// Top edge: lerp TL->TR by srcU
// Then lerp bottom->top by (1-srcV) for top-down rendering
float t = 1f - srcV; // 0=bottom, 1=top -> invert for top-down
float u = Mathf.Lerp(
Mathf.Lerp(uvBL.x, uvBR.x, srcU),
Mathf.Lerp(uvTL.x, uvTR.x, srcU), t);
float v = Mathf.Lerp(
Mathf.Lerp(uvBL.y, uvBR.y, srcU),
Mathf.Lerp(uvTL.y, uvTR.y, srcU), t);
int sx = Mathf.Clamp(Mathf.FloorToInt(u * fontTexW), 0, fontTexW - 1);
int sy = Mathf.Clamp(Mathf.FloorToInt(v * fontTexH), 0, fontTexH - 1);
Color sc = fontPixels[sy * fontTexW + sx];
if (sc.a <= 0.3f) continue;
int outX = cellX + offsetX + px;
int outY = texH - 1 - (cellY + offsetY + py);
if (outX >= 0 && outX < texW && outY >= 0 && outY < texH)
{
bmp.SetPixel(outX, outY, Color.white);
anyPixel = true;
}
}
}
if (anyPixel) renderedCount++;
}
bmp.Apply();
if (renderedCount == 0)
{
Debug.LogError("PSXFontAsset: Generated bitmap is empty.");
DestroyImmediate(bmp);
return;
}
// Store advance widths from the same CharacterInfo used for rendering.
// This guarantees advances match the bitmap glyphs exactly.
storedAdvanceWidths = new byte[96];
for (int idx = 0; idx < 96; idx++)
{
if (idx < 95 && charValid[idx])
{
CharacterInfo ci = charInfos[idx];
storedAdvanceWidths[idx] = (byte)Mathf.Clamp(Mathf.CeilToInt(ci.advance), 1, 255);
}
else
{
storedAdvanceWidths[idx] = (byte)glyphWidth; // fallback
}
}
// ── Step 6: Save ──
string path = AssetDatabase.GetAssetPath(this);
if (string.IsNullOrEmpty(path))
{
fontTexture = bmp;
return;
}
string dir = System.IO.Path.GetDirectoryName(path);
string texPath = dir + "/" + name + "_bitmap.png";
System.IO.File.WriteAllBytes(texPath, bmp.EncodeToPNG());
DestroyImmediate(bmp);
AssetDatabase.ImportAsset(texPath, ImportAssetOptions.ForceUpdate);
TextureImporter importer = AssetImporter.GetAtPath(texPath) as TextureImporter;
if (importer != null)
{
importer.textureType = TextureImporterType.Default;
importer.filterMode = FilterMode.Point;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.isReadable = true;
importer.npotScale = TextureImporterNPOTScale.None;
importer.mipmapEnabled = false;
importer.alphaIsTransparency = true;
importer.SaveAndReimport();
}
fontTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(texPath);
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
}
#endif
public byte[] ConvertTo4BPP()
{
if (fontTexture == null) return null;
int texW = 256;
int texH = TextureHeight;
int bytesPerRow = texW / 2;
byte[] result = new byte[bytesPerRow * texH];
Color[] pixels = fontTexture.GetPixels(0, 0, fontTexture.width, fontTexture.height);
int srcW = fontTexture.width;
int srcH = fontTexture.height;
for (int y = 0; y < texH; y++)
{
for (int x = 0; x < texW; x += 2)
{
byte lo = SamplePixel(pixels, srcW, srcH, x, y);
byte hi = SamplePixel(pixels, srcW, srcH, x + 1, y);
result[y * bytesPerRow + x / 2] = (byte)(lo | (hi << 4));
}
}
return result;
}
private byte SamplePixel(Color[] pixels, int srcW, int srcH, int x, int y)
{
if (x >= srcW || y >= srcH) return 0;
int srcY = srcH - 1 - y; // top-down (PS1) to bottom-up (Unity)
if (srcY < 0 || srcY >= srcH) return 0;
Color c = pixels[srcY * srcW + x];
return c.a > 0.5f ? (byte)1 : (byte)0;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b5bc0d8f6252841439e81a8406c37859

View File

@@ -8,6 +8,7 @@ namespace SplashEdit.RuntimeCode
/// the onInteract Lua event fires.
/// </summary>
[RequireComponent(typeof(PSXObjectExporter))]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXInteractable.png")]
public class PSXInteractable : MonoBehaviour
{
[Header("Interaction Settings")]
@@ -15,7 +16,7 @@ namespace SplashEdit.RuntimeCode
[SerializeField] private float interactionRadius = 2.0f;
[Tooltip("Button that triggers interaction (0-15, matches PS1 button mapping)")]
[SerializeField] private int interactButton = 5; // Default to Cross button
[SerializeField] private int interactButton = 14; // Default to Cross button
[Tooltip("Can this object be interacted with multiple times?")]
[SerializeField] private bool isRepeatable = true;
@@ -23,39 +24,35 @@ namespace SplashEdit.RuntimeCode
[Tooltip("Cooldown between interactions (in frames, 60 = 1 second at NTSC)")]
[SerializeField] private ushort cooldownFrames = 30;
[Tooltip("Show interaction prompt when in range (requires UI system)")]
[SerializeField] private bool showPrompt = true;
[Tooltip("Show a UI canvas when the player is in range")]
[SerializeField] private bool showPrompt = false;
[Tooltip("Name of the PSXCanvas to show when the player is in range")]
[SerializeField] private string promptCanvasName = "";
[Header("Advanced")]
[Tooltip("Require line-of-sight to player for interaction")]
[Tooltip("Require the player to be facing this object to interact")]
[SerializeField] private bool requireLineOfSight = false;
[Tooltip("Custom interaction point offset from object center")]
[SerializeField] private Vector3 interactionOffset = Vector3.zero;
// Public accessors for export
public float InteractionRadius => interactionRadius;
public int InteractButton => interactButton;
public bool IsRepeatable => isRepeatable;
public ushort CooldownFrames => cooldownFrames;
public bool ShowPrompt => showPrompt;
public string PromptCanvasName => promptCanvasName;
public bool RequireLineOfSight => requireLineOfSight;
public Vector3 InteractionOffset => interactionOffset;
private void OnDrawGizmosSelected()
{
// Draw interaction radius
Gizmos.color = new Color(1f, 1f, 0f, 0.3f); // Yellow, semi-transparent
Vector3 center = transform.position + interactionOffset;
Vector3 center = transform.position;
Gizmos.DrawWireSphere(center, interactionRadius);
// Draw filled sphere with lower alpha
Gizmos.color = new Color(1f, 1f, 0f, 0.1f);
Gizmos.DrawSphere(center, interactionRadius);
// Draw interaction point
Gizmos.color = Color.yellow;
Gizmos.DrawSphere(center, 0.1f);
}
}
}

15
Runtime/PSXInterpMode.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Per-keyframe interpolation mode. Must match the C++ InterpMode enum in cutscene.hh.
/// Packed into the upper 3 bits of the 16-bit frame field on export.
/// </summary>
public enum PSXInterpMode : byte
{
Linear = 0,
Step = 1,
EaseIn = 2,
EaseOut = 3,
EaseInOut = 4,
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f925971a446a614a9f4f3e61f2395c0

26
Runtime/PSXKeyframe.cs Normal file
View File

@@ -0,0 +1,26 @@
using System;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// A single keyframe in a cutscene track.
/// Value interpretation depends on track type:
/// CameraPosition / ObjectPosition: Unity world-space position (x, y, z)
/// CameraRotation: Euler angles in degrees (x=pitch, y=yaw, z=roll)
/// ObjectRotation: y component = rotation in degrees
/// ObjectActive: x component = 0.0 (inactive) or 1.0 (active)
/// </summary>
[Serializable]
public class PSXKeyframe
{
[Tooltip("Frame number (0 = start of cutscene). At 30fps, frame 30 = 1 second.")]
public int Frame;
[Tooltip("Keyframe value. Interpretation depends on track type.")]
public Vector3 Value;
[Tooltip("Interpolation mode from this keyframe to the next.")]
public PSXInterpMode Interp = PSXInterpMode.Linear;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1b2679967786bcf4e81793694cd3dfeb

View File

@@ -76,6 +76,8 @@ namespace SplashEdit.RuntimeCode
finalColor += lightContribution;
}
// Clamp to 0.8 to leave headroom for PS1 2x color blending mode,
// which doubles vertex colors. Without this cap, bright areas would clip.
finalColor.r = Mathf.Clamp(finalColor.r, 0.0f, 0.8f);
finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f);
finalColor.b = Mathf.Clamp(finalColor.b, 0.0f, 0.8f);

Some files were not shown because too many files have changed in this diff Show More