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 fileFormatVersion: 2
guid: 45b95f68e129e6f478d509d59f39bc6e guid: d7e9b1c3e60e2ff48be3cd61902ba6f1
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

BIN
Data/SPLASHLICENSE.DAT Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 5688ba03f531c3245a838793c0ae7f93 guid: 244f6913a02805e4aa3cebdd1240cab7
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
userData: 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 // Step 3: Download the file
string tempFile = Path.Combine(Path.GetTempPath(), $"pcsx-redux-{latestBuildId}.zip"); string tempFile = Path.Combine(Path.GetTempPath(), $"pcsx-redux-{latestBuildId}.zip");
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", "Downloading...", 0.1f); 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(); client.Headers.Add("User-Agent", "SplashEdit/1.0");
long? totalBytes = response.Content.Headers.ContentLength;
long downloadedBytes = 0; client.DownloadProgressChanged += (s, e) =>
using (var fileStream = File.Create(tempFile))
using (var downloadStream = await response.Content.ReadAsStreamAsync())
{ {
byte[] buffer = new byte[81920]; float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
int bytesRead; string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0) EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", $"Downloading... {sizeMB}", progress);
{ };
await fileStream.WriteAsync(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
if (totalBytes.HasValue) await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
{
float progress = (float)downloadedBytes / totalBytes.Value;
string sizeMB = $"{downloadedBytes / (1024 * 1024)}/{totalBytes.Value / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux",
$"Downloading... {sizeMB}", progress);
}
}
}
} }
log?.Invoke($"Downloaded to {tempFile}"); log?.Invoke($"Downloaded to {tempFile}");
@@ -144,6 +130,7 @@ namespace SplashEdit.EditorCode
}; };
var proc = Process.Start(psi); var proc = Process.Start(psi);
proc?.WaitForExit(); proc?.WaitForExit();
} }
else else
{ {
@@ -151,6 +138,20 @@ namespace SplashEdit.EditorCode
log?.Invoke($"Extracted to {installDir}"); 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 // Clean up temp file
try { File.Delete(tempFile); } catch { } try { File.Delete(tempFile); } catch { }
@@ -164,7 +165,7 @@ namespace SplashEdit.EditorCode
else else
{ {
// The zip might have a nested directory — try to find the exe // The zip might have a nested directory — try to find the exe
FixNestedDirectory(installDir); SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
if (SplashBuildPaths.IsPCSXReduxInstalled()) if (SplashBuildPaths.IsPCSXReduxInstalled())
{ {
log?.Invoke("PCSX-Redux installed successfully!"); 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> /// <summary>
/// Parse the latest build ID from the master manifest JSON. /// Parse the latest build ID from the master manifest JSON.
/// Expected format: {"builds":[{"id":1234,...},...],...} /// Expected format: {"builds":[{"id":1234,...},...],...}

View File

@@ -60,6 +60,7 @@ namespace SplashEdit.EditorCode
private const int FUNC_SEEK = 0x107; private const int FUNC_SEEK = 0x107;
public bool IsRunning => _listenTask != null && !_listenTask.IsCompleted; 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) 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; bool lastByteWasEscape = false;
var textBuffer = new StringBuilder(); var textBuffer = new StringBuilder();
int totalBytesReceived = 0; int totalBytesReceived = 0;
int consecutiveErrors = 0;
DateTime lastLogTime = DateTime.Now; DateTime lastLogTime = DateTime.Now;
_log?.Invoke("PCdrv monitor: waiting for data from PS1..."); _log?.Invoke("PCdrv monitor: waiting for data from PS1...");
@@ -179,6 +181,7 @@ namespace SplashEdit.EditorCode
} }
int b = _port.ReadByte(); int b = _port.ReadByte();
consecutiveErrors = 0;
totalBytesReceived++; totalBytesReceived++;
// Log first bytes received to help diagnose protocol issues // Log first bytes received to help diagnose protocol issues
@@ -256,8 +259,16 @@ namespace SplashEdit.EditorCode
catch (OperationCanceledException) { break; } catch (OperationCanceledException) { break; }
catch (Exception ex) catch (Exception ex)
{ {
if (!ct.IsCancellationRequested) if (ct.IsCancellationRequested) break;
_log?.Invoke($"PCdrv monitor error: {ex.Message}"); 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> /// </summary>
public static async Task<bool> DownloadAndInstall(Action<string> log = null) public static async Task<bool> DownloadAndInstall(Action<string> log = null)
{ {
string platformSuffix;
string archiveName; string archiveName;
switch (Application.platform) switch (Application.platform)
{ {
case RuntimePlatform.WindowsEditor: case RuntimePlatform.WindowsEditor:
platformSuffix = "x86_64-pc-windows-msvc"; archiveName = $"psxavenc-windows.zip";
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.zip";
break; break;
case RuntimePlatform.LinuxEditor: case RuntimePlatform.LinuxEditor:
platformSuffix = "x86_64-unknown-linux-gnu"; archiveName = $"psxavenc-linux.zip";
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.tar.gz";
break; break;
default: default:
log?.Invoke("Only Windows and Linux are supported."); log?.Invoke("Only Windows and Linux are supported.");
@@ -77,29 +74,18 @@ namespace SplashEdit.EditorCode
string tempFile = Path.Combine(Path.GetTempPath(), archiveName); string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
EditorUtility.DisplayProgressBar("Downloading psxavenc", "Downloading...", 0.1f); 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(); client.Headers.Add("User-Agent", "SplashEdit/1.0");
long? totalBytes = response.Content.Headers.ContentLength;
long downloaded = 0;
using (var fs = File.Create(tempFile)) client.DownloadProgressChanged += (s, e) =>
using (var stream = await response.Content.ReadAsStreamAsync())
{ {
byte[] buffer = new byte[81920]; float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
int bytesRead; string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) EditorUtility.DisplayProgressBar("Downloading psxavenc", $"Downloading... {sizeMB}", progress);
{ };
await fs.WriteAsync(buffer, 0, bytesRead);
downloaded += bytesRead; await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
if (totalBytes.HasValue)
{
float progress = (float)downloaded / totalBytes.Value;
EditorUtility.DisplayProgressBar("Downloading psxavenc",
$"{downloaded / 1024}/{totalBytes.Value / 1024} KB", progress);
}
}
}
} }
log?.Invoke("Extracting..."); log?.Invoke("Extracting...");
@@ -129,7 +115,7 @@ namespace SplashEdit.EditorCode
} }
// Fix nested directory (sometimes archives have one extra level) // Fix nested directory (sometimes archives have one extra level)
FixNestedDirectory(installDir); SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
try { File.Delete(tempFile); } catch { } 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> /// <summary>
/// Converts a Unity AudioClip to PS1 SPU ADPCM format using psxavenc. /// Converts a Unity AudioClip to PS1 SPU ADPCM format using psxavenc.
/// Returns the ADPCM byte array, or null on failure. /// Returns the ADPCM byte array, or null on failure.

View File

@@ -247,9 +247,25 @@ namespace SplashEdit.EditorCode
return tex; 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() private void OnGUI()
{ {
EnsureStyles(); 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(); DrawToolbar();
DrawConsoleOutput(); DrawConsoleOutput();
} }
@@ -310,69 +326,70 @@ namespace SplashEdit.EditorCode
int selMax = Mathf.Max(_selectionAnchor, _selectionEnd); int selMax = Mathf.Max(_selectionAnchor, _selectionEnd);
bool hasSelection = _selectionAnchor >= 0 && _selectionEnd >= 0; 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); if (evt.button == 0)
}
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.shift && _selectionAnchor >= 0)
_selectionEnd = i;
else
{ {
if (evt.shift && _selectionAnchor >= 0) _selectionAnchor = i;
_selectionEnd = i; _selectionEnd = i;
else
{
_selectionAnchor = i;
_selectionEnd = i;
}
evt.Use();
Repaint();
} }
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; menu.AddItem(new GUIContent("Copy selected lines"), false, () => CopyRange(selMin, selMax));
bool lineInSelection = hasSelection && clickedLine >= selMin && clickedLine <= selMax; menu.AddSeparator("");
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 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: case RuntimePlatform.WindowsEditor:
return Path.Combine(PCSXReduxDir, "pcsx-redux.exe"); return Path.Combine(PCSXReduxDir, "pcsx-redux.exe");
case RuntimePlatform.LinuxEditor: case RuntimePlatform.LinuxEditor:
return Path.Combine(ToolsDir, "PCSX-Redux-HEAD-x86_64.AppImage"); return Path.Combine(PCSXReduxDir, "PCSX-Redux-HEAD-x86_64.AppImage");
default: default:
return Path.Combine(PCSXReduxDir, "pcsx-redux"); return Path.Combine(PCSXReduxDir, "pcsx-redux");
} }
@@ -109,6 +109,22 @@ namespace SplashEdit.EditorCode
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.splashpack"); 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> /// <summary>
/// ISO output path for release builds. /// ISO output path for release builds.
/// </summary> /// </summary>
@@ -121,6 +137,24 @@ namespace SplashEdit.EditorCode
public static string CUEOutputPath => public static string CUEOutputPath =>
Path.Combine(BuildOutputDir, "psxsplash.cue"); 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> /// <summary>
/// Ensures the build output and tools directories exist. /// Ensures the build output and tools directories exist.
/// Also appends entries to the project .gitignore if not present. /// Also appends entries to the project .gitignore if not present.
@@ -132,6 +166,45 @@ namespace SplashEdit.EditorCode
EnsureGitIgnore(); 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> /// <summary>
/// Checks if PCSX-Redux is installed in the tools directory. /// Checks if PCSX-Redux is installed in the tools directory.
/// </summary> /// </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); set => EditorPrefs.SetInt(Prefix + "SerialBaudRate", value);
} }
// --- VRAM Layout --- // --- VRAM Layout (hardcoded 320x240, dual-buffered, vertical) ---
public static int ResolutionWidth public static int ResolutionWidth
{ {
get => EditorPrefs.GetInt(Prefix + "ResWidth", 320); get => 320;
set => EditorPrefs.SetInt(Prefix + "ResWidth", value); set { } // no-op, hardcoded
} }
public static int ResolutionHeight public static int ResolutionHeight
{ {
get => EditorPrefs.GetInt(Prefix + "ResHeight", 240); get => 240;
set => EditorPrefs.SetInt(Prefix + "ResHeight", value); set { } // no-op, hardcoded
} }
public static bool DualBuffering public static bool DualBuffering
{ {
get => EditorPrefs.GetBool(Prefix + "DualBuffering", true); get => true;
set => EditorPrefs.SetBool(Prefix + "DualBuffering", value); set { } // no-op, hardcoded
} }
public static bool VerticalLayout public static bool VerticalLayout
{ {
get => EditorPrefs.GetBool(Prefix + "VerticalLayout", true); get => true;
set => EditorPrefs.SetBool(Prefix + "VerticalLayout", value); 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 --- // --- Export settings ---
@@ -124,17 +169,25 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", value); 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); get => EditorPrefs.GetString(Prefix + "LicenseFilePath", SplashBuildPaths.DefaultLicenseFilePath);
set => EditorPrefs.SetBool(Prefix + "AutoValidate", value); set => EditorPrefs.SetString(Prefix + "LicenseFilePath", value);
} }
// --- Play Mode Intercept --- /// <summary>
public static bool InterceptPlayMode /// Volume label for the ISO image (up to 31 characters, uppercase).
/// </summary>
public static string ISOVolumeLabel
{ {
get => EditorPrefs.GetBool(Prefix + "InterceptPlayMode", false); get => EditorPrefs.GetString(Prefix + "ISOVolumeLabel", "PSXSPLASH");
set => EditorPrefs.SetBool(Prefix + "InterceptPlayMode", value); set => EditorPrefs.SetString(Prefix + "ISOVolumeLabel", value);
} }
/// <summary> /// <summary>
@@ -147,7 +200,9 @@ namespace SplashEdit.EditorCode
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath", "Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate", "PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout", "ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
"GTEScaling", "AutoValidate", "InterceptPlayMode" "GTEScaling", "AutoValidate",
"LicenseFilePath", "ISOVolumeLabel",
"OtSize", "BumpSize"
}; };
foreach (string key in keys) foreach (string key in keys)

View File

@@ -12,21 +12,21 @@ namespace SplashEdit.EditorCode
{ {
private bool _interactionFoldout = true; private bool _interactionFoldout = true;
private bool _advancedFoldout = false; private bool _advancedFoldout = false;
private SerializedProperty _interactionRadius; private SerializedProperty _interactionRadius;
private SerializedProperty _interactButton; private SerializedProperty _interactButton;
private SerializedProperty _isRepeatable; private SerializedProperty _isRepeatable;
private SerializedProperty _cooldownFrames; private SerializedProperty _cooldownFrames;
private SerializedProperty _showPrompt; private SerializedProperty _showPrompt;
private SerializedProperty _promptCanvasName;
private SerializedProperty _requireLineOfSight; private SerializedProperty _requireLineOfSight;
private SerializedProperty _interactionOffset;
private static readonly string[] ButtonNames =
private static readonly string[] ButtonNames =
{ {
"Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left", "Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left",
"L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square" "L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"
}; };
private void OnEnable() private void OnEnable()
{ {
_interactionRadius = serializedObject.FindProperty("interactionRadius"); _interactionRadius = serializedObject.FindProperty("interactionRadius");
@@ -34,103 +34,89 @@ namespace SplashEdit.EditorCode
_isRepeatable = serializedObject.FindProperty("isRepeatable"); _isRepeatable = serializedObject.FindProperty("isRepeatable");
_cooldownFrames = serializedObject.FindProperty("cooldownFrames"); _cooldownFrames = serializedObject.FindProperty("cooldownFrames");
_showPrompt = serializedObject.FindProperty("showPrompt"); _showPrompt = serializedObject.FindProperty("showPrompt");
_promptCanvasName = serializedObject.FindProperty("promptCanvasName");
_requireLineOfSight = serializedObject.FindProperty("requireLineOfSight"); _requireLineOfSight = serializedObject.FindProperty("requireLineOfSight");
_interactionOffset = serializedObject.FindProperty("interactionOffset");
} }
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
serializedObject.Update(); serializedObject.Update();
DrawHeader(); // Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal();
GUILayout.Label(EditorGUIUtility.IconContent("d_Selectable Icon"), GUILayout.Width(30), GUILayout.Height(30));
_interactionFoldout = DrawFoldoutSection("Interaction Settings", _interactionFoldout, () => 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(4);
_interactionFoldout = PSXEditorStyles.DrawFoldoutCard("Interaction Settings", _interactionFoldout, () =>
{ {
EditorGUILayout.PropertyField(_interactionRadius); EditorGUILayout.PropertyField(_interactionRadius);
// Button selector with visual
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Interact Button"); EditorGUILayout.PrefixLabel("Interact Button");
_interactButton.intValue = EditorGUILayout.Popup(_interactButton.intValue, ButtonNames); _interactButton.intValue = EditorGUILayout.Popup(_interactButton.intValue, ButtonNames);
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
EditorGUILayout.PropertyField(_isRepeatable); EditorGUILayout.PropertyField(_isRepeatable);
if (_isRepeatable.boolValue) if (_isRepeatable.boolValue)
{ {
EditorGUI.indentLevel++; EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_cooldownFrames, new GUIContent("Cooldown (frames)")); EditorGUILayout.PropertyField(_cooldownFrames, new GUIContent("Cooldown (frames)"));
// Show cooldown in seconds
float seconds = _cooldownFrames.intValue / 60f; 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.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--; EditorGUI.indentLevel--;
} }
EditorGUILayout.PropertyField(_showPrompt);
}); });
_advancedFoldout = DrawFoldoutSection("Advanced", _advancedFoldout, () => EditorGUILayout.Space(2);
_advancedFoldout = PSXEditorStyles.DrawFoldoutCard("Advanced", _advancedFoldout, () =>
{ {
EditorGUILayout.PropertyField(_requireLineOfSight); EditorGUILayout.PropertyField(_requireLineOfSight,
EditorGUILayout.PropertyField(_interactionOffset); 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(); 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 ───── // ───── 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() public static void OpenControlPanel()
{ {
SplashControlPanel.ShowWindow(); SplashControlPanel.ShowWindow();
@@ -26,7 +26,7 @@ namespace SplashEdit.EditorCode
[MenuItem("GameObject/PlayStation 1/Scene Exporter", false, 10)] [MenuItem("GameObject/PlayStation 1/Scene Exporter", false, 10)]
public static void CreateSceneExporter(MenuCommand menuCommand) public static void CreateSceneExporter(MenuCommand menuCommand)
{ {
var existing = Object.FindObjectOfType<PSXSceneExporter>(); var existing = Object.FindFirstObjectByType<PSXSceneExporter>();
if (existing != null) if (existing != null)
{ {
EditorUtility.DisplayDialog( EditorUtility.DisplayDialog(

View File

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

View File

@@ -2,513 +2,246 @@ using UnityEngine;
using UnityEditor; using UnityEditor;
using SplashEdit.RuntimeCode; using SplashEdit.RuntimeCode;
using System.Linq; using System.Linq;
using System.Collections.Generic;
namespace SplashEdit.EditorCode namespace SplashEdit.EditorCode
{ {
/// <summary>
/// Custom inspector for PSXObjectExporter with enhanced UX.
/// Shows mesh info, texture preview, collision visualization, and validation.
/// </summary>
[CustomEditor(typeof(PSXObjectExporter))] [CustomEditor(typeof(PSXObjectExporter))]
[CanEditMultipleObjects] [CanEditMultipleObjects]
public class PSXObjectExporterEditor : UnityEditor.Editor public class PSXObjectExporterEditor : UnityEditor.Editor
{ {
// Serialized properties
private SerializedProperty isActiveProp; private SerializedProperty isActiveProp;
private SerializedProperty bitDepthProp; private SerializedProperty bitDepthProp;
private SerializedProperty luaFileProp; private SerializedProperty luaFileProp;
private SerializedProperty objectFlagsProp;
private SerializedProperty collisionTypeProp; 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 MeshFilter meshFilter;
private MeshRenderer meshRenderer; private MeshRenderer meshRenderer;
private int triangleCount; private int triangleCount;
private int vertexCount; private int vertexCount;
private Bounds meshBounds;
private List<string> validationErrors = new List<string>(); private bool showExport = true;
private List<string> validationWarnings = new List<string>(); private bool showCollision = true;
// Styles
private GUIStyle headerStyle;
private GUIStyle errorStyle;
private GUIStyle warningStyle;
// Validation
private bool _validationDirty = true;
private void OnEnable() private void OnEnable()
{ {
// Get serialized properties
isActiveProp = serializedObject.FindProperty("isActive"); isActiveProp = serializedObject.FindProperty("isActive");
bitDepthProp = serializedObject.FindProperty("bitDepth"); bitDepthProp = serializedObject.FindProperty("bitDepth");
luaFileProp = serializedObject.FindProperty("luaFile"); luaFileProp = serializedObject.FindProperty("luaFile");
objectFlagsProp = serializedObject.FindProperty("objectFlags");
collisionTypeProp = serializedObject.FindProperty("collisionType"); 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(); CacheMeshInfo();
// Defer validation to first inspector draw
_validationDirty = true;
} }
private void CacheMeshInfo() private void CacheMeshInfo()
{ {
var exporter = target as PSXObjectExporter; var exporter = target as PSXObjectExporter;
if (exporter == null) return; if (exporter == null) return;
meshFilter = exporter.GetComponent<MeshFilter>(); meshFilter = exporter.GetComponent<MeshFilter>();
meshRenderer = exporter.GetComponent<MeshRenderer>(); meshRenderer = exporter.GetComponent<MeshRenderer>();
if (meshFilter != null && meshFilter.sharedMesh != null) if (meshFilter != null && meshFilter.sharedMesh != null)
{ {
var mesh = meshFilter.sharedMesh; triangleCount = meshFilter.sharedMesh.triangles.Length / 3;
triangleCount = mesh.triangles.Length / 3; vertexCount = meshFilter.sharedMesh.vertexCount;
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");
}
}
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
serializedObject.Update(); serializedObject.Update();
// Run deferred validation DrawHeader();
if (_validationDirty) EditorGUILayout.Space(4);
{
RunValidation();
_validationDirty = false;
}
InitStyles();
// Active toggle at top
EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object"));
if (!isActiveProp.boolValue) 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(); serializedObject.ApplyModifiedProperties();
return; return;
} }
EditorGUILayout.Space(5); DrawMeshSummary();
PSXEditorStyles.DrawSeparator(6, 6);
// Mesh Info Section DrawExportSection();
DrawMeshInfoSection(); PSXEditorStyles.DrawSeparator(6, 6);
DrawCollisionSection();
// Texture Section PSXEditorStyles.DrawSeparator(6, 6);
DrawTextureSection(); DrawActions();
// Export Settings Section serializedObject.ApplyModifiedProperties();
DrawExportSettingsSection();
// Collision Settings Section
DrawCollisionSettingsSection();
// Gizmo Settings Section
DrawGizmoSettingsSection();
// Validation Section
DrawValidationSection();
// Action Buttons
DrawActionButtons();
if (serializedObject.ApplyModifiedProperties())
{
_validationDirty = true;
}
} }
private void InitStyles() private new void DrawHeader()
{ {
if (headerStyle == null) EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
{
headerStyle = new GUIStyle(EditorStyles.foldoutHeader); EditorGUILayout.BeginHorizontal();
} EditorGUILayout.PropertyField(isActiveProp, GUIContent.none, GUILayout.Width(18));
var exporter = target as PSXObjectExporter;
if (errorStyle == null) EditorGUILayout.LabelField(exporter.gameObject.name, PSXEditorStyles.CardHeaderStyle);
{ EditorGUILayout.EndHorizontal();
errorStyle = new GUIStyle(EditorStyles.label);
errorStyle.normal.textColor = Color.red; EditorGUILayout.EndVertical();
}
if (warningStyle == null)
{
warningStyle = new GUIStyle(EditorStyles.label);
warningStyle.normal.textColor = new Color(1f, 0.7f, 0f);
}
} }
private void DrawMeshInfoSection() private void DrawMeshSummary()
{ {
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information"); if (meshFilter == null || meshFilter.sharedMesh == null)
if (showMeshInfo)
{ {
EditorGUI.indentLevel++; EditorGUILayout.LabelField("No mesh on this object.", PSXEditorStyles.InfoBox);
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)
return; 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")) EditorGUILayout.BeginHorizontal();
{ GUILayout.Space(EditorGUI.indentLevel * 15);
var exporter = FindObjectOfType<PSXSceneExporter>(); if (GUILayout.Button("Edit", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
if (exporter != null) AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
{ if (GUILayout.Button("Clear", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
Selection.activeGameObject = exporter.gameObject; luaFileProp.objectReferenceValue = null;
} GUILayout.FlexibleSpace();
else EditorGUILayout.EndHorizontal();
{
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
}
if (GUILayout.Button("Open Scene Validator"))
{
PSXSceneValidatorWindow.ShowWindow();
}
} }
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() private void CreateNewLuaScript()
{ {
var exporter = target as PSXObjectExporter; var exporter = target as PSXObjectExporter;
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_"); string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject( string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script", "Create Lua Script", defaultName + ".lua", "lua",
defaultName + ".lua",
"lua",
"Create a new Lua script for this object"); "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} luaFileProp.objectReferenceValue = luaFile;
-- serializedObject.ApplyModifiedProperties();
-- 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();
}
} }
} }
[MenuItem("CONTEXT/PSXObjectExporter/Copy Settings to Selected")] [DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
private static void CopySettingsToSelected(MenuCommand command) private static void DrawColliderGizmo(PSXObjectExporter exporter, GizmoType gizmoType)
{ {
var source = command.context as PSXObjectExporter; if (exporter.CollisionType != PSXCollisionType.Dynamic) return;
if (source == null) return;
MeshFilter mf = exporter.GetComponent<MeshFilter>();
foreach (var go in Selection.gameObjects) 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>(); Vector3 corner = center + new Vector3(
if (target != null && target != source) (i & 1) != 0 ? ext.x : -ext.x,
{ (i & 2) != 0 ? ext.y : -ext.y,
Undo.RecordObject(target, "Copy PSX Settings"); (i & 4) != 0 ? ext.z : -ext.z
// Copy via serialized object );
EditorUtility.CopySerialized(source, target); 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 UnityEngine;
using UnityEditor; using UnityEditor;
using SplashEdit.RuntimeCode; using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode 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))] [CustomEditor(typeof(PSXSceneExporter))]
public class PSXSceneExporterEditor : UnityEditor.Editor public class PSXSceneExporterEditor : UnityEditor.Editor
{ {
// Saved RenderSettings state so we can restore it on deselect. private SerializedProperty gteScalingProp;
private bool _savedFog; private SerializedProperty sceneLuaProp;
private Color _savedFogColor; private SerializedProperty fogEnabledProp;
private FogMode _savedFogMode; private SerializedProperty fogColorProp;
private float _savedFogStart; private SerializedProperty fogDensityProp;
private float _savedFogEnd; 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() private void OnEnable()
{ {
SaveAndApplyFogPreview(); gteScalingProp = serializedObject.FindProperty("GTEScaling");
// Re-apply whenever the scene is repainted (handles inspector value changes). sceneLuaProp = serializedObject.FindProperty("SceneLuaFile");
EditorApplication.update += OnEditorUpdate; 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() 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() public override void OnInspectorGUI()
{ {
serializedObject.Update(); 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; 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.BeginHorizontal();
EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Space(EditorGUI.indentLevel * 15);
GUILayout.Label("Fog Preview (active in Scene view)", EditorStyles.boldLabel); 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; float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10); int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
float fogFarUnity = (8000f / density) * gteScale / 4096f; float fogFarUnity = (8000f / density) * gteScale / 4096f;
float fogNearUnity = fogFarUnity / 3f; float fogNearUnity = fogFarUnity / 3f;
EditorGUILayout.LabelField("Near distance", $"{fogNearUnity:F1} Unity units"); EditorGUILayout.Space(2);
EditorGUILayout.LabelField("Far distance", $"{fogFarUnity:F1} Unity units"); EditorGUILayout.LabelField(
EditorGUILayout.LabelField("(PS1 SZ range)", $"{8000f / (density * 3f):F0} - {8000f / density:F0} GTE units"); $"<color=#aaaaaa>GTE range: {fogNearUnity:F1} - {fogFarUnity:F1} units | " +
EditorGUILayout.EndVertical(); $"{8000f / (density * 3f):F0} - {8000f / density:F0} SZ</color>",
PSXEditorStyles.RichLabel);
// Keep preview applied as values may have changed. EditorGUI.indentLevel--;
ApplyFogPreview();
}
else
{
// Make sure preview is off when fog is disabled.
RenderSettings.fog = false;
} }
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;
using UnityEngine.Networking;
using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System;
namespace SplashEdit.EditorCode 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 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 RepoUrl = "https://github.com/psxsplash/psxsplash.git";
public static readonly string InstallPath = "Assets/psxsplash"; public static readonly string InstallPath = "Assets/psxsplash";
public static readonly string FullInstallPath; 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() static PSXSplashInstaller()
{ {
FullInstallPath = Path.Combine(Application.dataPath, "psxsplash"); FullInstallPath = Path.Combine(Application.dataPath, "psxsplash");
} }
// ═══════════════════════════════════════════════════════════════
// Queries
// ═══════════════════════════════════════════════════════════════
/// <summary>Is the native project cloned on disk?</summary>
public static bool IsInstalled() public static bool IsInstalled()
{ {
return Directory.Exists(FullInstallPath) && return Directory.Exists(FullInstallPath) &&
Directory.EnumerateFileSystemEntries(FullInstallPath).Any(); Directory.EnumerateFileSystemEntries(FullInstallPath).Any();
} }
public static async Task<bool> Install() /// <summary>Are we currently fetching releases from GitHub?</summary>
{ public static bool IsFetchingReleases => _isFetchingReleases;
if (IsInstalled()) return true;
/// <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 try
{ {
// Create the parent directory if it doesn't exist string result = RunGitCommandSync("describe --tags --exact-match HEAD", FullInstallPath);
Directory.CreateDirectory(Application.dataPath); return string.IsNullOrWhiteSpace(result) ? null : result.Trim();
// Clone the repository
var result = await RunGitCommandAsync($"clone --recursive {RepoUrl} \"{FullInstallPath}\"", Application.dataPath);
return !result.Contains("error");
} }
catch (Exception e) catch
{ {
UnityEngine.Debug.LogError($"Failed to install PSXSplash: {e.Message}"); return null;
return false;
} }
} }
public static async Task<Dictionary<string, string>> GetBranchesWithLatestCommitsAsync() // ═══════════════════════════════════════════════════════════════
{ // Fetch Releases (HTTP — no git required)
if (!IsInstalled()) return new Dictionary<string, string>(); // ═══════════════════════════════════════════════════════════════
/// <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 try
{ {
// Fetch all branches and tags string json = await HttpGetAsync(GitHubApiReleasesUrl);
await RunGitCommandAsync("fetch --all", FullInstallPath); if (string.IsNullOrEmpty(json))
// 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)
{ {
var commitOutput = await RunGitCommandAsync($"log origin/{branch} -1 --pretty=format:%h", FullInstallPath); UnityEngine.Debug.LogWarning("[PSXSplashInstaller] Failed to fetch releases from GitHub.");
if (!string.IsNullOrEmpty(commitOutput)) return _cachedReleases;
{
branchesWithCommits[branch] = commitOutput.Trim();
}
} }
return branchesWithCommits; var releases = ParseReleasesJson(json);
} // Filter out drafts, sort by newest first
catch (Exception e) releases = releases
{ .Where(r => !r.IsDraft)
UnityEngine.Debug.LogError($"Failed to get branches: {e.Message}"); .OrderByDescending(r => r.PublishedAt)
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())
.ToList(); .ToList();
_cachedReleases = releases;
return releases;
} }
catch (Exception e) catch (Exception ex)
{ {
UnityEngine.Debug.LogError($"Failed to get releases: {e.Message}"); UnityEngine.Debug.LogError($"[PSXSplashInstaller] Error fetching releases: {ex.Message}");
return new List<string>(); 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 try
{ {
// If it's a branch name, checkout the branch Directory.CreateDirectory(Path.GetDirectoryName(FullInstallPath));
// 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);
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; 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() public static async Task<bool> FetchLatestAsync()
{ {
if (!IsInstalled()) return false; if (!IsInstalled()) return false;
try try
{ {
var result = await RunGitCommandAsync("fetch --all", FullInstallPath); await RunGitCommandAsync("fetch --all --tags", FullInstallPath);
return !result.Contains("error"); 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; 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", FileName = "git",
Arguments = arguments, Arguments = arguments,
@@ -156,48 +314,136 @@ namespace SplashEdit.EditorCode
using (var process = new Process()) using (var process = new Process())
{ {
process.StartInfo = processInfo; process.StartInfo = psi;
var outputBuilder = new System.Text.StringBuilder(); process.EnableRaisingEvents = true;
var errorBuilder = new System.Text.StringBuilder();
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)) if (!string.IsNullOrEmpty(e.Data))
outputBuilder.AppendLine(e.Data); {
stdout.AppendLine(e.Data);
onProgress?.Invoke(e.Data);
}
}; };
process.ErrorDataReceived += (s, e) =>
process.ErrorDataReceived += (sender, e) =>
{ {
if (!string.IsNullOrEmpty(e.Data)) 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.Start();
process.BeginOutputReadLine(); process.BeginOutputReadLine();
process.BeginErrorReadLine(); process.BeginErrorReadLine();
// Wait for exit with timeout var timeoutTask = Task.Delay(TimeSpan.FromMinutes(10));
var timeout = TimeSpan.FromSeconds(30); var completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
if (completedTask == timeoutTask)
{ {
process.WaitForExit(); // Ensure all output is processed try { process.Kill(); } catch { }
throw new TimeoutException("Git command timed out after 10 minutes.");
string output = outputBuilder.ToString();
string error = errorBuilder.ToString();
if (!string.IsNullOrEmpty(error))
{
UnityEngine.Debug.LogError($"Git error: {error}");
}
return output;
} }
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 else
{ {
process.Kill(); UnityEngine.Debug.LogWarning($"[PSXSplashInstaller] HTTP GET {url} failed: {request.error}");
throw new TimeoutException("Git command timed out"); 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 quantizedTexture;
private Texture2D vramTexture; // VRAM representation of the texture private Texture2D vramTexture; // VRAM representation of the texture
private List<VRAMPixel> clut; // Color Lookup Table (CLUT), stored as a 1D list 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 PSXBPP bpp = PSXBPP.TEX_4BIT;
private readonly int previewSize = 256; private readonly int previewSize = 256;
@@ -27,19 +26,25 @@ namespace SplashEdit.EditorCode
private void OnGUI() private void OnGUI()
{ {
GUILayout.Label("Quantized Preview", EditorStyles.boldLabel); GUILayout.Label("Quantized Preview", PSXEditorStyles.WindowHeader);
// Texture input field // Texture input field
PSXEditorStyles.BeginCard();
originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false); originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
// Dropdown for bit depth selection // Dropdown for bit depth selection
bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp); bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp);
EditorGUILayout.Space(4);
// Button to generate the quantized preview // 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(); GenerateQuantizedPreview();
} }
PSXEditorStyles.EndCard();
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
@@ -47,8 +52,8 @@ namespace SplashEdit.EditorCode
if (originalTexture != null) if (originalTexture != null)
{ {
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Label("Original Texture"); GUILayout.Label("Original Texture", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(originalTexture, previewSize, false); DrawTexturePreview(originalTexture, previewSize);
GUILayout.EndVertical(); GUILayout.EndVertical();
} }
@@ -56,7 +61,7 @@ namespace SplashEdit.EditorCode
if (vramTexture != null) if (vramTexture != null)
{ {
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Label("VRAM View (Indexed Data as 16bpp)"); GUILayout.Label("VRAM View (Indexed Data as 16bpp)", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(vramTexture, previewSize); DrawTexturePreview(vramTexture, previewSize);
GUILayout.EndVertical(); GUILayout.EndVertical();
} }
@@ -65,7 +70,7 @@ namespace SplashEdit.EditorCode
if (quantizedTexture != null) if (quantizedTexture != null)
{ {
GUILayout.BeginVertical(); GUILayout.BeginVertical();
GUILayout.Label("Quantized Texture"); GUILayout.Label("Quantized Texture", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(quantizedTexture, previewSize); DrawTexturePreview(quantizedTexture, previewSize);
GUILayout.EndVertical(); GUILayout.EndVertical();
} }
@@ -75,37 +80,17 @@ namespace SplashEdit.EditorCode
// Display the Color Lookup Table (CLUT) // Display the Color Lookup Table (CLUT)
if (clut != null) if (clut != null)
{ {
GUILayout.Label("Color Lookup Table (CLUT)"); PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.Label("Color Lookup Table (CLUT)", PSXEditorStyles.SectionHeader);
DrawCLUT(); DrawCLUT();
} }
GUILayout.Space(10); PSXEditorStyles.DrawSeparator(4, 4);
// 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);
}
}
}
}
}
// Export CLUT data // Export CLUT data
if (clut != null) 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"); string path = EditorUtility.SaveFilePanel("Save CLUT data", "", "clut_data", "bin");
@@ -139,7 +124,7 @@ namespace SplashEdit.EditorCode
clut = psxTex.ColorPalette; 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 // Renders a texture preview within the editor window
Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false)); Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false));

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using SplashEdit.RuntimeCode; using SplashEdit.RuntimeCode;
using Unity.Collections; using Unity.Collections;
@@ -19,23 +18,14 @@ namespace SplashEdit.EditorCode
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>(); private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
private Vector2 scrollPosition; private Vector2 scrollPosition;
private Texture2D vramImage; private Texture2D vramImage;
private Vector2 selectedResolution = new Vector2(320, 240); private static readonly Vector2 selectedResolution = new Vector2(320, 240);
private bool dualBuffering = true; private const bool dualBuffering = true;
private bool verticalLayout = true; private const bool verticalLayout = true;
private Color bufferColor1 = new Color(1, 0, 0, 0.5f); private Color bufferColor1 = new Color(1, 0, 0, 0.5f);
private Color bufferColor2 = new Color(0, 1, 0, 0.5f); private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
private Color prohibitedColor = new Color(1, 0, 0, 0.3f); private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
private PSXData _psxData; private PSXData _psxData;
private PSXFontData[] _cachedFonts;
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();
[MenuItem("PlayStation 1/VRAM Editor")] [MenuItem("PlayStation 1/VRAM Editor")]
public static void ShowWindow() public static void ShowWindow()
@@ -57,7 +47,9 @@ namespace SplashEdit.EditorCode
// Ensure minimum window size is applied. // Ensure minimum window size is applied.
this.minSize = MinSize; 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> /// <summary>
@@ -144,65 +136,75 @@ namespace SplashEdit.EditorCode
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor()); 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. // Overlay custom font textures into the VRAM preview.
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); // Fonts live at x=960 (4bpp = 64 VRAM hwords wide), stacking from y=0.
PSXFontData[] fonts;
if (path != string.Empty) 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() private void OnGUI()
{ {
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
GUILayout.BeginVertical(); 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. PSXEditorStyles.DrawSeparator(6, 6);
selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), resolutionsStrings)]; GUILayout.Label("Prohibited Areas", PSXEditorStyles.SectionHeader);
GUILayout.Space(4);
// 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);
scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true)); scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true));
@@ -213,10 +215,7 @@ namespace SplashEdit.EditorCode
{ {
var area = prohibitedAreas[i]; var area = prohibitedAreas[i];
GUI.backgroundColor = new Color(0.95f, 0.95f, 0.95f); PSXEditorStyles.BeginCard();
GUILayout.BeginVertical("box");
GUI.backgroundColor = Color.white;
// Display fields for editing the area // Display fields for editing the area
area.X = EditorGUILayout.IntField("X Coordinate", area.X); area.X = EditorGUILayout.IntField("X Coordinate", area.X);
@@ -224,17 +223,16 @@ namespace SplashEdit.EditorCode
area.Width = EditorGUILayout.IntField("Width", area.Width); area.Width = EditorGUILayout.IntField("Width", area.Width);
area.Height = EditorGUILayout.IntField("Height", area.Height); area.Height = EditorGUILayout.IntField("Height", area.Height);
EditorGUILayout.Space(2);
if (GUILayout.Button("Remove", GUILayout.Height(30))) if (GUILayout.Button("Remove", PSXEditorStyles.DangerButton, GUILayout.Height(24)))
{ {
toRemove.Add(i); // Mark for removal toRemove.Add(i); // Mark for removal
} }
prohibitedAreas[i] = area; prohibitedAreas[i] = area;
GUILayout.EndVertical(); PSXEditorStyles.EndCard();
GUILayout.Space(10); GUILayout.Space(4);
} }
// Remove the areas marked for deletion outside the loop to avoid skipping elements // Remove the areas marked for deletion outside the loop to avoid skipping elements
@@ -246,19 +244,23 @@ namespace SplashEdit.EditorCode
GUILayout.EndScrollView(); GUILayout.EndScrollView();
GUILayout.Space(10); GUILayout.Space(10);
if (GUILayout.Button("Add Prohibited Area")) if (GUILayout.Button("Add Prohibited Area", PSXEditorStyles.SecondaryButton))
{ {
prohibitedAreas.Add(new ProhibitedArea()); prohibitedAreas.Add(new ProhibitedArea());
} }
// Button to initiate texture packing. PSXEditorStyles.DrawSeparator(4, 4);
if (GUILayout.Button("Pack Textures"))
// Button to pack and preview VRAM layout.
if (GUILayout.Button("Pack Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(28)))
{ {
PackTextures(); PackTextures();
} }
// Button to save settings; saving now occurs only on button press. EditorGUILayout.Space(2);
if (GUILayout.Button("Save Settings"))
// Button to save prohibited areas.
if (GUILayout.Button("Save Settings", PSXEditorStyles.SuccessButton, GUILayout.Height(28)))
{ {
_psxData.OutputResolution = selectedResolution; _psxData.OutputResolution = selectedResolution;
_psxData.DualBuffering = dualBuffering; _psxData.DualBuffering = dualBuffering;
@@ -297,6 +299,24 @@ namespace SplashEdit.EditorCode
EditorGUI.DrawRect(areaRect, prohibitedColor); 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(); 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 fileFormatVersion: 2
guid: 55ec05596eb659341b8fdb46cd21ab63 guid: 607cfdcd926623447afba2249593f87b
TextureImporter: TextureImporter:
internalIDToNameTable: [] internalIDToNameTable: []
externalObjects: {} 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 fileFormatVersion: 2
guid: 5a5f4bcf472dcfc44b794a898530a6f0 guid: c1ac35b4ac561a6479df60ee4440f138
TextureImporter: TextureImporter:
internalIDToNameTable: [] internalIDToNameTable: []
externalObjects: {} 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 fileFormatVersion: 2
guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 guid: afedc2b61a424884b90aeb912c54fe50
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,6 @@ namespace SplashEdit.RuntimeCode
{ {
/// <summary> /// <summary>
/// Pre-converted audio clip data ready for splashpack serialization. /// Pre-converted audio clip data ready for splashpack serialization.
/// Populated by the Editor (PSXSceneExporter) so Runtime code never
/// touches PSXAudioConverter.
/// </summary> /// </summary>
public struct AudioClipExport public struct AudioClipExport
{ {
@@ -18,10 +16,11 @@ namespace SplashEdit.RuntimeCode
/// <summary> /// <summary>
/// Attach to a GameObject to include an audio clip in the PS1 build. /// 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 /// 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> /// </summary>
[AddComponentMenu("PSX/Audio Source")] [AddComponentMenu("PSX/PSX Audio Clip")]
public class PSXAudioSource : MonoBehaviour [Icon("Packages/net.psxsplash.splashedit/Icons/PSXAudioClip.png")]
public class PSXAudioClip : MonoBehaviour
{ {
[Tooltip("Name used to identify this clip in Lua (Audio.Play(\"name\"))." )] [Tooltip("Name used to identify this clip in Lua (Audio.Play(\"name\"))." )]
public string ClipName = ""; 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, Solid = 0x01,
Slope = 0x02, Slope = 0x02,
Stairs = 0x04, Stairs = 0x04,
Trigger = 0x08,
NoWalk = 0x10, NoWalk = 0x10,
} }
@@ -84,14 +83,17 @@ namespace SplashEdit.RuntimeCode
foreach (var exporter in exporters) foreach (var exporter in exporters)
{ {
// Dynamic objects use runtime AABB colliders, skip them
if (exporter.CollisionType == PSXCollisionType.Dynamic)
continue;
PSXCollisionType effectiveType = exporter.CollisionType; PSXCollisionType effectiveType = exporter.CollisionType;
if (effectiveType == PSXCollisionType.None) if (effectiveType == PSXCollisionType.None)
{ {
if (autoIncludeSolid) if (autoIncludeSolid)
{ {
// Auto-include as Solid so all geometry blocks the player effectiveType = PSXCollisionType.Static;
effectiveType = PSXCollisionType.Solid;
autoIncluded++; autoIncluded++;
} }
else else
@@ -100,11 +102,8 @@ namespace SplashEdit.RuntimeCode
} }
} }
// Get the collision mesh (custom or render mesh)
MeshFilter mf = exporter.GetComponent<MeshFilter>(); MeshFilter mf = exporter.GetComponent<MeshFilter>();
Mesh collisionMesh = exporter.CustomCollisionMesh != null Mesh collisionMesh = mf?.sharedMesh;
? exporter.CustomCollisionMesh
: mf?.sharedMesh;
if (collisionMesh == null) if (collisionMesh == null)
continue; continue;
@@ -130,38 +129,25 @@ namespace SplashEdit.RuntimeCode
// Determine surface flags // Determine surface flags
byte flags = 0; 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 else
{ {
// Floor-like: normal.y > cosWalkable flags = (byte)PSXSurfaceFlag.Solid;
// 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;
}
} }
_allTriangles.Add(new CollisionTriExport _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")] [CreateAssetMenu(fileName = "PSXData", menuName = "PSXSplash/PS1 Project Data")]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXData.png")]
public class PSXData : ScriptableObject 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,30 +8,31 @@ namespace SplashEdit.RuntimeCode
/// the onInteract Lua event fires. /// the onInteract Lua event fires.
/// </summary> /// </summary>
[RequireComponent(typeof(PSXObjectExporter))] [RequireComponent(typeof(PSXObjectExporter))]
[Icon("Packages/net.psxsplash.splashedit/Icons/PSXInteractable.png")]
public class PSXInteractable : MonoBehaviour public class PSXInteractable : MonoBehaviour
{ {
[Header("Interaction Settings")] [Header("Interaction Settings")]
[Tooltip("Distance within which the player can interact with this object")] [Tooltip("Distance within which the player can interact with this object")]
[SerializeField] private float interactionRadius = 2.0f; [SerializeField] private float interactionRadius = 2.0f;
[Tooltip("Button that triggers interaction (0-15, matches PS1 button mapping)")] [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?")] [Tooltip("Can this object be interacted with multiple times?")]
[SerializeField] private bool isRepeatable = true; [SerializeField] private bool isRepeatable = true;
[Tooltip("Cooldown between interactions (in frames, 60 = 1 second at NTSC)")] [Tooltip("Cooldown between interactions (in frames, 60 = 1 second at NTSC)")]
[SerializeField] private ushort cooldownFrames = 30; [SerializeField] private ushort cooldownFrames = 30;
[Tooltip("Show interaction prompt when in range (requires UI system)")] [Tooltip("Show a UI canvas when the player is in range")]
[SerializeField] private bool showPrompt = true; [SerializeField] private bool showPrompt = false;
[Tooltip("Name of the PSXCanvas to show when the player is in range")]
[SerializeField] private string promptCanvasName = "";
[Header("Advanced")] [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; [SerializeField] private bool requireLineOfSight = false;
[Tooltip("Custom interaction point offset from object center")]
[SerializeField] private Vector3 interactionOffset = Vector3.zero;
// Public accessors for export // Public accessors for export
public float InteractionRadius => interactionRadius; public float InteractionRadius => interactionRadius;
@@ -39,23 +40,19 @@ namespace SplashEdit.RuntimeCode
public bool IsRepeatable => isRepeatable; public bool IsRepeatable => isRepeatable;
public ushort CooldownFrames => cooldownFrames; public ushort CooldownFrames => cooldownFrames;
public bool ShowPrompt => showPrompt; public bool ShowPrompt => showPrompt;
public string PromptCanvasName => promptCanvasName;
public bool RequireLineOfSight => requireLineOfSight; public bool RequireLineOfSight => requireLineOfSight;
public Vector3 InteractionOffset => interactionOffset;
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {
// Draw interaction radius // Draw interaction radius
Gizmos.color = new Color(1f, 1f, 0f, 0.3f); // Yellow, semi-transparent 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); Gizmos.DrawWireSphere(center, interactionRadius);
// Draw filled sphere with lower alpha // Draw filled sphere with lower alpha
Gizmos.color = new Color(1f, 1f, 0f, 0.1f); Gizmos.color = new Color(1f, 1f, 0f, 0.1f);
Gizmos.DrawSphere(center, interactionRadius); 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; 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.r = Mathf.Clamp(finalColor.r, 0.0f, 0.8f);
finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f); finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f);
finalColor.b = Mathf.Clamp(finalColor.b, 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