Compare commits
13 Commits
main
...
4aa4e49424
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aa4e49424 | ||
| 53e993f58e | |||
| 0d1e363dbb | |||
| ac0e4d8420 | |||
| 9af5d7dd1a | |||
| dc9bfcb155 | |||
| 849e221b32 | |||
| 2013e31b04 | |||
|
|
4cebe93c34 | ||
|
|
551eb4c0de | ||
|
|
ecb1422937 | ||
| a07a715d19 | |||
| b3da188438 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -26,7 +26,6 @@
|
|||||||
*.hdr filter=lfs diff=lfs merge=lfs -text
|
*.hdr filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.apng filter=lfs diff=lfs merge=lfs -text
|
*.apng filter=lfs diff=lfs merge=lfs -text
|
||||||
*.atsc filter=lfs diff=lfs merge=lfs -text
|
*.atsc filter=lfs diff=lfs merge=lfs -text
|
||||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
872
Debug.unity
Normal file
872
Debug.unity
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
%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}
|
||||||
7
Debug.unity.meta
Normal file
7
Debug.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ab8d113c2664ebf4d899b0826cd5f10c
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: d64fb2a2412d7958ca13d15956c4182b
|
guid: f7b9a2e33a4c4754997cf0dd0f20acc8
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|||||||
8
Editor/Core.meta
Normal file
8
Editor/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8e74ebc4b575d27499f7abd4d82b8849
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
263
Editor/Core/PCSXReduxDownloader.cs
Normal file
263
Editor/Core/PCSXReduxDownloader.cs
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads and installs PCSX-Redux from the official distrib.app CDN.
|
||||||
|
/// Mirrors the logic from pcsx-redux.js (the official download script).
|
||||||
|
///
|
||||||
|
/// Flow: fetch platform manifest → find latest build ID → fetch build manifest →
|
||||||
|
/// get download URL → download zip → extract to .tools/pcsx-redux/
|
||||||
|
/// </summary>
|
||||||
|
public static class PCSXReduxDownloader
|
||||||
|
{
|
||||||
|
private const string MANIFEST_BASE = "https://distrib.app/storage/manifests/pcsx-redux/";
|
||||||
|
|
||||||
|
private static readonly HttpClient _http;
|
||||||
|
|
||||||
|
static PCSXReduxDownloader()
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = System.Net.DecompressionMethods.GZip
|
||||||
|
| System.Net.DecompressionMethods.Deflate
|
||||||
|
};
|
||||||
|
_http = new HttpClient(handler);
|
||||||
|
_http.Timeout = TimeSpan.FromSeconds(60);
|
||||||
|
_http.DefaultRequestHeaders.UserAgent.ParseAdd("SplashEdit/1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the platform variant string for the current platform.
|
||||||
|
/// </summary>
|
||||||
|
private static string GetPlatformVariant()
|
||||||
|
{
|
||||||
|
switch (Application.platform)
|
||||||
|
{
|
||||||
|
case RuntimePlatform.WindowsEditor:
|
||||||
|
return "dev-win-cli-x64";
|
||||||
|
case RuntimePlatform.LinuxEditor:
|
||||||
|
return "dev-linux-x64";
|
||||||
|
default:
|
||||||
|
return "dev-win-cli-x64";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads and installs PCSX-Redux to .tools/pcsx-redux/.
|
||||||
|
/// Shows progress bar during download.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
|
||||||
|
{
|
||||||
|
string variant = GetPlatformVariant();
|
||||||
|
log?.Invoke($"Platform variant: {variant}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Step 1: Fetch the master manifest to get the latest build ID
|
||||||
|
string manifestUrl = $"{MANIFEST_BASE}{variant}/manifest.json";
|
||||||
|
log?.Invoke($"Fetching manifest: {manifestUrl}");
|
||||||
|
string manifestJson = await _http.GetStringAsync(manifestUrl);
|
||||||
|
|
||||||
|
// Parse the latest build ID from the manifest.
|
||||||
|
// The manifest is JSON with a "builds" array. We want the highest ID.
|
||||||
|
// Simple JSON parsing without dependencies:
|
||||||
|
int latestBuildId = ParseLatestBuildId(manifestJson);
|
||||||
|
if (latestBuildId < 0)
|
||||||
|
{
|
||||||
|
log?.Invoke("Failed to parse build ID from manifest.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log?.Invoke($"Latest build ID: {latestBuildId}");
|
||||||
|
|
||||||
|
// Step 2: Fetch the specific build manifest
|
||||||
|
string buildManifestUrl = $"{MANIFEST_BASE}{variant}/manifest-{latestBuildId}.json";
|
||||||
|
log?.Invoke($"Fetching build manifest...");
|
||||||
|
string buildManifestJson = await _http.GetStringAsync(buildManifestUrl);
|
||||||
|
|
||||||
|
// Parse the download path
|
||||||
|
string downloadPath = ParseDownloadPath(buildManifestJson);
|
||||||
|
if (string.IsNullOrEmpty(downloadPath))
|
||||||
|
{
|
||||||
|
log?.Invoke("Failed to parse download path from build manifest.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string downloadUrl = $"https://distrib.app{downloadPath}";
|
||||||
|
log?.Invoke($"Downloading: {downloadUrl}");
|
||||||
|
|
||||||
|
// Step 3: Download the file
|
||||||
|
string tempFile = Path.Combine(Path.GetTempPath(), $"pcsx-redux-{latestBuildId}.zip");
|
||||||
|
|
||||||
|
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", "Downloading...", 0.1f);
|
||||||
|
|
||||||
|
using (var response = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
long? totalBytes = response.Content.Headers.ContentLength;
|
||||||
|
long downloadedBytes = 0;
|
||||||
|
|
||||||
|
using (var fileStream = File.Create(tempFile))
|
||||||
|
using (var downloadStream = await response.Content.ReadAsStreamAsync())
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[81920];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
await fileStream.WriteAsync(buffer, 0, bytesRead);
|
||||||
|
downloadedBytes += bytesRead;
|
||||||
|
|
||||||
|
if (totalBytes.HasValue)
|
||||||
|
{
|
||||||
|
float progress = (float)downloadedBytes / totalBytes.Value;
|
||||||
|
string sizeMB = $"{downloadedBytes / (1024 * 1024)}/{totalBytes.Value / (1024 * 1024)} MB";
|
||||||
|
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux",
|
||||||
|
$"Downloading... {sizeMB}", progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke($"Downloaded to {tempFile}");
|
||||||
|
EditorUtility.DisplayProgressBar("Installing PCSX-Redux", "Extracting...", 0.9f);
|
||||||
|
|
||||||
|
// Step 4: Extract
|
||||||
|
string installDir = SplashBuildPaths.PCSXReduxDir;
|
||||||
|
if (Directory.Exists(installDir))
|
||||||
|
Directory.Delete(installDir, true);
|
||||||
|
Directory.CreateDirectory(installDir);
|
||||||
|
|
||||||
|
if (Application.platform == RuntimePlatform.LinuxEditor && tempFile.EndsWith(".tar.gz"))
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "tar",
|
||||||
|
Arguments = $"xzf \"{tempFile}\" -C \"{installDir}\" --strip-components=1",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
var proc = Process.Start(psi);
|
||||||
|
proc?.WaitForExit();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
|
||||||
|
log?.Invoke($"Extracted to {installDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
try { File.Delete(tempFile); } catch { }
|
||||||
|
|
||||||
|
// Step 5: Verify
|
||||||
|
if (SplashBuildPaths.IsPCSXReduxInstalled())
|
||||||
|
{
|
||||||
|
log?.Invoke("PCSX-Redux installed successfully!");
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The zip might have a nested directory — try to find the exe
|
||||||
|
FixNestedDirectory(installDir);
|
||||||
|
if (SplashBuildPaths.IsPCSXReduxInstalled())
|
||||||
|
{
|
||||||
|
log?.Invoke("PCSX-Redux installed successfully!");
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke("Installation completed but PCSX-Redux binary not found at expected path.");
|
||||||
|
log?.Invoke($"Expected: {SplashBuildPaths.PCSXReduxBinary}");
|
||||||
|
log?.Invoke($"Check: {installDir}");
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log?.Invoke($"Download failed: {ex.Message}");
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the zip extracts into a nested directory, move files up.
|
||||||
|
/// </summary>
|
||||||
|
private static void FixNestedDirectory(string installDir)
|
||||||
|
{
|
||||||
|
var subdirs = Directory.GetDirectories(installDir);
|
||||||
|
if (subdirs.Length == 1)
|
||||||
|
{
|
||||||
|
string nested = subdirs[0];
|
||||||
|
foreach (string file in Directory.GetFiles(nested))
|
||||||
|
{
|
||||||
|
string dest = Path.Combine(installDir, Path.GetFileName(file));
|
||||||
|
File.Move(file, dest);
|
||||||
|
}
|
||||||
|
foreach (string dir in Directory.GetDirectories(nested))
|
||||||
|
{
|
||||||
|
string dest = Path.Combine(installDir, Path.GetFileName(dir));
|
||||||
|
Directory.Move(dir, dest);
|
||||||
|
}
|
||||||
|
try { Directory.Delete(nested); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the latest build ID from the master manifest JSON.
|
||||||
|
/// Expected format: {"builds":[{"id":1234,...},...],...}
|
||||||
|
/// distrib.app returns builds sorted newest-first, so we take the first.
|
||||||
|
/// Falls back to scanning all IDs if the "builds" section isn't found.
|
||||||
|
/// </summary>
|
||||||
|
private static int ParseLatestBuildId(string json)
|
||||||
|
{
|
||||||
|
// Fast path: find the first "id" inside "builds" array
|
||||||
|
int buildsIdx = json.IndexOf("\"builds\"", StringComparison.Ordinal);
|
||||||
|
int startPos = buildsIdx >= 0 ? buildsIdx : 0;
|
||||||
|
|
||||||
|
string searchToken = "\"id\":";
|
||||||
|
int idx = json.IndexOf(searchToken, startPos, StringComparison.Ordinal);
|
||||||
|
if (idx < 0) return -1;
|
||||||
|
|
||||||
|
int pos = idx + searchToken.Length;
|
||||||
|
while (pos < json.Length && char.IsWhiteSpace(json[pos])) pos++;
|
||||||
|
|
||||||
|
int numStart = pos;
|
||||||
|
while (pos < json.Length && char.IsDigit(json[pos])) pos++;
|
||||||
|
|
||||||
|
if (pos > numStart && int.TryParse(json.Substring(numStart, pos - numStart), out int id))
|
||||||
|
return id;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the download path from a build-specific manifest.
|
||||||
|
/// Expected format: {...,"path":"/storage/builds/..."}
|
||||||
|
/// </summary>
|
||||||
|
private static string ParseDownloadPath(string json)
|
||||||
|
{
|
||||||
|
string searchToken = "\"path\":";
|
||||||
|
int idx = json.IndexOf(searchToken, StringComparison.Ordinal);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
int pos = idx + searchToken.Length;
|
||||||
|
while (pos < json.Length && char.IsWhiteSpace(json[pos])) pos++;
|
||||||
|
|
||||||
|
if (pos >= json.Length || json[pos] != '"') return null;
|
||||||
|
pos++; // skip opening quote
|
||||||
|
|
||||||
|
int pathStart = pos;
|
||||||
|
while (pos < json.Length && json[pos] != '"') pos++;
|
||||||
|
|
||||||
|
return json.Substring(pathStart, pos - pathStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/PCSXReduxDownloader.cs.meta
Normal file
2
Editor/Core/PCSXReduxDownloader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3eaffbb1caed9648b5b57d211ead4d6
|
||||||
769
Editor/Core/PCdrvSerialHost.cs
Normal file
769
Editor/Core/PCdrvSerialHost.cs
Normal file
@@ -0,0 +1,769 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PCdrv Host — serves files to a PS1 over serial (Unirom/NOTPSXSerial protocol).
|
||||||
|
///
|
||||||
|
/// PCdrv uses MIPS `break` instructions to request file I/O from the host.
|
||||||
|
/// For real hardware running Unirom, we must:
|
||||||
|
/// 1. Enter debug mode (DEBG/OKAY) — installs Unirom's kernel-resident SIO handler
|
||||||
|
/// 2. Continue execution (CONT/OKAY)
|
||||||
|
/// 3. Monitor serial for escape sequences: 0x00 followed by 'p' = PCDrv command
|
||||||
|
/// 4. Handle file operations (init, open, read, close, seek, etc.)
|
||||||
|
///
|
||||||
|
/// Without entering debug mode first, `break` instructions cause an unhandled
|
||||||
|
/// "BP break (0x9)" crash because no handler is registered.
|
||||||
|
///
|
||||||
|
/// Protocol based on NOTPSXSerial: https://github.com/JonathanDotCel/NOTPSXSerial
|
||||||
|
/// </summary>
|
||||||
|
public class PCdrvSerialHost : IDisposable
|
||||||
|
{
|
||||||
|
private SerialPort _port;
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
private Task _listenTask;
|
||||||
|
private readonly string _portName;
|
||||||
|
private readonly int _baudRate;
|
||||||
|
private readonly string _baseDir;
|
||||||
|
private readonly Action<string> _log;
|
||||||
|
private readonly Action<string> _psxLog;
|
||||||
|
|
||||||
|
// File handle table (1-indexed, handles are not recycled)
|
||||||
|
private readonly List<PCFile> _files = new List<PCFile>();
|
||||||
|
|
||||||
|
private class PCFile
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
public FileStream Stream;
|
||||||
|
public int Handle;
|
||||||
|
public bool Closed;
|
||||||
|
public FileAccess Mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol escape char — PCDrv commands are prefixed with 0x00 + 'p'
|
||||||
|
private const byte ESCAPE_CHAR = 0x00;
|
||||||
|
|
||||||
|
// PCDrv function codes (from Unirom kernel)
|
||||||
|
private const int FUNC_INIT = 0x101;
|
||||||
|
private const int FUNC_CREAT = 0x102;
|
||||||
|
private const int FUNC_OPEN = 0x103;
|
||||||
|
private const int FUNC_CLOSE = 0x104;
|
||||||
|
private const int FUNC_READ = 0x105;
|
||||||
|
private const int FUNC_WRITE = 0x106;
|
||||||
|
private const int FUNC_SEEK = 0x107;
|
||||||
|
|
||||||
|
public bool IsRunning => _listenTask != null && !_listenTask.IsCompleted;
|
||||||
|
|
||||||
|
public PCdrvSerialHost(string portName, int baudRate, string baseDir, Action<string> log, Action<string> psxLog = null)
|
||||||
|
{
|
||||||
|
_portName = portName;
|
||||||
|
_baudRate = baudRate;
|
||||||
|
_baseDir = baseDir;
|
||||||
|
_log = log;
|
||||||
|
_psxLog = psxLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a new serial port and begins the monitor/PCdrv loop.
|
||||||
|
/// Note: DEBG must have been sent BEFORE the exe was uploaded (via UniromUploader.UploadExeForPCdrv).
|
||||||
|
/// Use the Start(SerialPort) overload to pass an already-open port from the uploader.
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (IsRunning) return;
|
||||||
|
|
||||||
|
_port = new SerialPort(_portName, _baudRate)
|
||||||
|
{
|
||||||
|
ReadTimeout = 5000,
|
||||||
|
WriteTimeout = 5000,
|
||||||
|
StopBits = StopBits.Two,
|
||||||
|
Parity = Parity.None,
|
||||||
|
DataBits = 8,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
DtrEnable = true,
|
||||||
|
RtsEnable = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_port.Open();
|
||||||
|
_log?.Invoke($"PCdrv host: opened {_portName} @ {_baudRate}");
|
||||||
|
_log?.Invoke($"PCdrv host: serving files from {_baseDir}");
|
||||||
|
|
||||||
|
StartMonitorLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the PCDrv monitor loop on an already-open serial port.
|
||||||
|
/// Use this after UniromUploader.UploadExeForPCdrv() which sends DEBG → SEXE
|
||||||
|
/// and returns the open port. The debug hooks are already installed and the
|
||||||
|
/// exe is already running — we just need to listen for escape sequences.
|
||||||
|
/// </summary>
|
||||||
|
public void Start(SerialPort openPort)
|
||||||
|
{
|
||||||
|
if (IsRunning) return;
|
||||||
|
|
||||||
|
_port = openPort;
|
||||||
|
_log?.Invoke($"PCdrv host: serving files from {_baseDir}");
|
||||||
|
_log?.Invoke("PCdrv host: monitoring for PCDrv requests...");
|
||||||
|
|
||||||
|
StartMonitorLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartMonitorLoop()
|
||||||
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_listenTask = Task.Run(() => MonitorLoop(_cts.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
try { _listenTask?.Wait(2000); } catch { }
|
||||||
|
|
||||||
|
foreach (var f in _files)
|
||||||
|
{
|
||||||
|
if (!f.Closed && f.Stream != null)
|
||||||
|
{
|
||||||
|
try { f.Stream.Close(); f.Stream.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_files.Clear();
|
||||||
|
|
||||||
|
if (_port != null && _port.IsOpen)
|
||||||
|
{
|
||||||
|
try { _port.Close(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
_port?.Dispose();
|
||||||
|
_port = null;
|
||||||
|
_log?.Invoke("PCdrv host stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Stop();
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Monitor loop — reads serial byte-by-byte looking for escape sequences
|
||||||
|
// Matches NOTPSXSerial's Bridge.MonitorSerial()
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void MonitorLoop(CancellationToken ct)
|
||||||
|
{
|
||||||
|
bool lastByteWasEscape = false;
|
||||||
|
var textBuffer = new StringBuilder();
|
||||||
|
int totalBytesReceived = 0;
|
||||||
|
DateTime lastLogTime = DateTime.Now;
|
||||||
|
|
||||||
|
_log?.Invoke("PCdrv monitor: waiting for data from PS1...");
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_port.BytesToRead == 0)
|
||||||
|
{
|
||||||
|
// Flush any accumulated text output periodically
|
||||||
|
if (textBuffer.Length > 0 && (DateTime.Now - lastLogTime).TotalMilliseconds > 100)
|
||||||
|
{
|
||||||
|
EmitPsxLine(textBuffer.ToString());
|
||||||
|
textBuffer.Clear();
|
||||||
|
lastLogTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
Thread.Sleep(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int b = _port.ReadByte();
|
||||||
|
totalBytesReceived++;
|
||||||
|
|
||||||
|
// Log first bytes received to help diagnose protocol issues
|
||||||
|
if (totalBytesReceived <= 32)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv monitor: byte #{totalBytesReceived} = 0x{b:X2} ('{(b >= 0x20 && b < 0x7F ? (char)b : '.')}')");
|
||||||
|
}
|
||||||
|
else if (totalBytesReceived == 33)
|
||||||
|
{
|
||||||
|
_log?.Invoke("PCdrv monitor: (suppressing per-byte logging, check PS1> lines for output)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastByteWasEscape)
|
||||||
|
{
|
||||||
|
lastByteWasEscape = false;
|
||||||
|
|
||||||
|
// Flush any text before handling escape
|
||||||
|
if (textBuffer.Length > 0)
|
||||||
|
{
|
||||||
|
EmitPsxLine(textBuffer.ToString());
|
||||||
|
textBuffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == ESCAPE_CHAR)
|
||||||
|
{
|
||||||
|
// Double escape = literal 0x00 in output, ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 'p')
|
||||||
|
{
|
||||||
|
// PCDrv command incoming
|
||||||
|
_log?.Invoke("PCdrv monitor: got escape+p → PCDrv command!");
|
||||||
|
HandlePCDrvCommand(ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unknown escape sequence — log it
|
||||||
|
_log?.Invoke($"PCdrv monitor: unknown escape seq: 0x00 + 0x{b:X2} ('{(b >= 0x20 && b < 0x7F ? (char)b : '.')}')");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == ESCAPE_CHAR)
|
||||||
|
{
|
||||||
|
lastByteWasEscape = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular byte — this is printf output from the PS1
|
||||||
|
if (b == '\n' || b == '\r')
|
||||||
|
{
|
||||||
|
if (textBuffer.Length > 0)
|
||||||
|
{
|
||||||
|
EmitPsxLine(textBuffer.ToString());
|
||||||
|
textBuffer.Clear();
|
||||||
|
lastLogTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (b >= 0x20 && b < 0x7F)
|
||||||
|
{
|
||||||
|
textBuffer.Append((char)b);
|
||||||
|
// Flush long lines immediately
|
||||||
|
if (textBuffer.Length >= 200)
|
||||||
|
{
|
||||||
|
EmitPsxLine(textBuffer.ToString());
|
||||||
|
textBuffer.Clear();
|
||||||
|
lastLogTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else: non-printable byte that's not escape, skip
|
||||||
|
}
|
||||||
|
catch (TimeoutException) { }
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!ct.IsCancellationRequested)
|
||||||
|
_log?.Invoke($"PCdrv monitor error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// PCDrv command dispatcher
|
||||||
|
// Matches NOTPSXSerial's PCDrv.ReadCommand()
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void HandlePCDrvCommand(CancellationToken ct)
|
||||||
|
{
|
||||||
|
int funcCode = ReadInt32(ct);
|
||||||
|
|
||||||
|
switch (funcCode)
|
||||||
|
{
|
||||||
|
case FUNC_INIT: HandleInit(); break;
|
||||||
|
case FUNC_CREAT: HandleCreate(ct); break;
|
||||||
|
case FUNC_OPEN: HandleOpen(ct); break;
|
||||||
|
case FUNC_CLOSE: HandleClose(ct); break;
|
||||||
|
case FUNC_READ: HandleRead(ct); break;
|
||||||
|
case FUNC_WRITE: HandleWrite(ct); break;
|
||||||
|
case FUNC_SEEK: HandleSeek(ct); break;
|
||||||
|
default:
|
||||||
|
_log?.Invoke($"PCdrv: unknown function 0x{funcCode:X}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Individual PCDrv handlers — match NOTPSXSerial's PCDrv.cs
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void HandleInit()
|
||||||
|
{
|
||||||
|
_log?.Invoke("PCdrv: INIT");
|
||||||
|
SendString("OKAY");
|
||||||
|
_port.Write(new byte[] { 0 }, 0, 1); // null terminator expected by Unirom
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleOpen(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Unirom sends: we respond OKAY first, then read filename + mode
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
string filename = ReadNullTermString(ct);
|
||||||
|
int modeParam = ReadInt32(ct);
|
||||||
|
|
||||||
|
// Log raw bytes for debugging garbled filenames
|
||||||
|
_log?.Invoke($"PCdrv: OPEN \"{filename}\" mode={modeParam} (len={filename.Length}, hex={BitConverter.ToString(System.Text.Encoding.ASCII.GetBytes(filename))})");
|
||||||
|
|
||||||
|
// Check if already open
|
||||||
|
var existing = FindOpenFile(filename);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: already open, handle={existing.Handle}");
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(existing.Handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fullPath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fullPath = ResolvePath(filename);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: invalid filename \"{filename}\": {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: file not found: {fullPath}");
|
||||||
|
SendString("NOPE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fs = new FileStream(fullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||||
|
int handle = NextHandle();
|
||||||
|
_files.Add(new PCFile { Name = filename, Stream = fs, Handle = handle, Closed = false, Mode = FileAccess.ReadWrite });
|
||||||
|
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(handle);
|
||||||
|
_log?.Invoke($"PCdrv: opened handle={handle}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: open failed: {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCreate(CancellationToken ct)
|
||||||
|
{
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
string filename = ReadNullTermString(ct);
|
||||||
|
int parameters = ReadInt32(ct);
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: CREAT \"{filename}\" params={parameters}");
|
||||||
|
|
||||||
|
var existing = FindOpenFile(filename);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(existing.Handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fullPath;
|
||||||
|
try { fullPath = ResolvePath(filename); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: invalid filename \"{filename}\": {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create or truncate the file
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
var temp = File.Create(fullPath);
|
||||||
|
temp.Flush(); temp.Close(); temp.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var fs = new FileStream(fullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||||
|
int handle = NextHandle();
|
||||||
|
_files.Add(new PCFile { Name = filename, Stream = fs, Handle = handle, Closed = false, Mode = FileAccess.ReadWrite });
|
||||||
|
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(handle);
|
||||||
|
_log?.Invoke($"PCdrv: created handle={handle}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: create failed: {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleClose(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Unirom sends: we respond OKAY first, then read handle + 2 unused params
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
int handle = ReadInt32(ct);
|
||||||
|
int _unused1 = ReadInt32(ct);
|
||||||
|
int _unused2 = ReadInt32(ct);
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: CLOSE handle={handle}");
|
||||||
|
|
||||||
|
var f = FindOpenFile(handle);
|
||||||
|
if (f == null)
|
||||||
|
{
|
||||||
|
// No such file — "great success" per NOTPSXSerial
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
f.Stream.Close();
|
||||||
|
f.Stream.Dispose();
|
||||||
|
f.Closed = true;
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(handle);
|
||||||
|
_log?.Invoke($"PCdrv: closed handle={handle}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: close error: {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRead(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Unirom sends: we respond OKAY first, then read handle + len + memaddr
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
int handle = ReadInt32(ct);
|
||||||
|
int length = ReadInt32(ct);
|
||||||
|
int memAddr = ReadInt32(ct); // for debugging only
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: READ handle={handle} len=0x{length:X} memAddr=0x{memAddr:X}");
|
||||||
|
|
||||||
|
var f = FindOpenFile(handle);
|
||||||
|
if (f == null)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: no file with handle {handle}");
|
||||||
|
SendString("NOPE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = new byte[length];
|
||||||
|
int bytesRead = f.Stream.Read(data, 0, length);
|
||||||
|
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(data.Length);
|
||||||
|
|
||||||
|
// Checksum (simple byte sum, forced V3 = true per NOTPSXSerial)
|
||||||
|
uint checksum = CalculateChecksum(data);
|
||||||
|
WriteUInt32(checksum);
|
||||||
|
|
||||||
|
// Send data using chunked writer (with per-chunk ack for V2+)
|
||||||
|
WriteDataChunked(data);
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: sent {bytesRead} bytes for handle={handle}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: read error: {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleWrite(CancellationToken ct)
|
||||||
|
{
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
int handle = ReadInt32(ct);
|
||||||
|
int length = ReadInt32(ct);
|
||||||
|
int memAddr = ReadInt32(ct);
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: WRITE handle={handle} len={length}");
|
||||||
|
|
||||||
|
var f = FindOpenFile(handle);
|
||||||
|
if (f == null)
|
||||||
|
{
|
||||||
|
SendString("NOPE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
// Read data from PSX
|
||||||
|
byte[] data = ReadBytes(length, ct);
|
||||||
|
|
||||||
|
f.Stream.Write(data, 0, length);
|
||||||
|
f.Stream.Flush();
|
||||||
|
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32(length);
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: wrote {length} bytes to handle={handle}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSeek(CancellationToken ct)
|
||||||
|
{
|
||||||
|
SendString("OKAY");
|
||||||
|
|
||||||
|
int handle = ReadInt32(ct);
|
||||||
|
int offset = ReadInt32(ct);
|
||||||
|
int whence = ReadInt32(ct);
|
||||||
|
|
||||||
|
_log?.Invoke($"PCdrv: SEEK handle={handle} offset={offset} whence={whence}");
|
||||||
|
|
||||||
|
var f = FindOpenFile(handle);
|
||||||
|
if (f == null)
|
||||||
|
{
|
||||||
|
SendString("NOPE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeekOrigin origin = whence switch
|
||||||
|
{
|
||||||
|
0 => SeekOrigin.Begin,
|
||||||
|
1 => SeekOrigin.Current,
|
||||||
|
2 => SeekOrigin.End,
|
||||||
|
_ => SeekOrigin.Begin
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
long newPos = f.Stream.Seek(offset, origin);
|
||||||
|
SendString("OKAY");
|
||||||
|
WriteInt32((int)newPos);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log?.Invoke($"PCdrv: seek error: {ex.Message}");
|
||||||
|
SendString("NOPE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// PS1 output routing
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Routes PS1 printf output to PSXConsoleWindow (via _psxLog) if available,
|
||||||
|
/// otherwise falls back to the control panel log.
|
||||||
|
/// </summary>
|
||||||
|
private void EmitPsxLine(string text)
|
||||||
|
{
|
||||||
|
if (_psxLog != null)
|
||||||
|
_psxLog.Invoke(text);
|
||||||
|
else
|
||||||
|
_log?.Invoke($"PS1> {text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Chunked data write — matches NOTPSXSerial's WriteBytes()
|
||||||
|
// Sends data in 2048-byte chunks; for protocol V2+ Unirom
|
||||||
|
// responds with CHEK/MORE/ERR! per chunk.
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void WriteDataChunked(byte[] data)
|
||||||
|
{
|
||||||
|
int chunkSize = 2048;
|
||||||
|
for (int i = 0; i < data.Length; i += chunkSize)
|
||||||
|
{
|
||||||
|
int thisChunk = Math.Min(chunkSize, data.Length - i);
|
||||||
|
_port.Write(data, i, thisChunk);
|
||||||
|
|
||||||
|
// Wait for bytes to drain
|
||||||
|
while (_port.BytesToWrite > 0)
|
||||||
|
Thread.Sleep(0);
|
||||||
|
|
||||||
|
// V2 protocol: wait for CHEK, send chunk checksum, wait for MORE
|
||||||
|
// For now, handle this if present
|
||||||
|
if (_port.BytesToRead >= 4)
|
||||||
|
{
|
||||||
|
string resp = ReadFixedString(4);
|
||||||
|
if (resp == "CHEK")
|
||||||
|
{
|
||||||
|
ulong chunkSum = 0;
|
||||||
|
for (int j = 0; j < thisChunk; j++)
|
||||||
|
chunkSum += data[i + j];
|
||||||
|
_port.Write(BitConverter.GetBytes((uint)chunkSum), 0, 4);
|
||||||
|
Thread.Sleep(1);
|
||||||
|
|
||||||
|
// Wait for MORE or ERR!
|
||||||
|
string ack = WaitFor4CharResponse(5000);
|
||||||
|
if (ack == "ERR!")
|
||||||
|
{
|
||||||
|
_log?.Invoke("PCdrv: chunk checksum error, retrying...");
|
||||||
|
i -= chunkSize; // retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// File handle helpers
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private int NextHandle() => _files.Count + 1;
|
||||||
|
|
||||||
|
private PCFile FindOpenFile(string name)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _files.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_files[i].Closed && _files[i].Name.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return _files[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PCFile FindOpenFile(int handle)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _files.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_files[i].Closed && _files[i].Handle == handle)
|
||||||
|
return _files[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolvePath(string filename)
|
||||||
|
{
|
||||||
|
// Strip leading slashes and backslashes
|
||||||
|
filename = filename.TrimStart('/', '\\');
|
||||||
|
return Path.Combine(_baseDir, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Low-level serial I/O
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private int ReadInt32(CancellationToken ct)
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[4];
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
buf[i] = (byte)ReadByteBlocking(ct);
|
||||||
|
return BitConverter.ToInt32(buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint ReadUInt32(CancellationToken ct)
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[4];
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
buf[i] = (byte)ReadByteBlocking(ct);
|
||||||
|
return BitConverter.ToUInt32(buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ReadBytes(int count, CancellationToken ct)
|
||||||
|
{
|
||||||
|
byte[] data = new byte[count];
|
||||||
|
int pos = 0;
|
||||||
|
while (pos < count)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (_port.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
int read = _port.Read(data, pos, Math.Min(count - pos, _port.BytesToRead));
|
||||||
|
pos += read;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ReadByteBlocking(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_port.BytesToRead > 0)
|
||||||
|
return _port.ReadByte();
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
throw new OperationCanceledException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadNullTermString(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int b = ReadByteBlocking(ct);
|
||||||
|
if (b == 0) break;
|
||||||
|
sb.Append((char)b);
|
||||||
|
if (sb.Length > 255) break;
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendString(string s)
|
||||||
|
{
|
||||||
|
byte[] data = Encoding.ASCII.GetBytes(s);
|
||||||
|
_port.Write(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteInt32(int value)
|
||||||
|
{
|
||||||
|
_port.Write(BitConverter.GetBytes(value), 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteUInt32(uint value)
|
||||||
|
{
|
||||||
|
_port.Write(BitConverter.GetBytes(value), 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadFixedString(int count)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (_port.BytesToRead > 0)
|
||||||
|
sb.Append((char)_port.ReadByte());
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string WaitFor4CharResponse(int timeoutMs)
|
||||||
|
{
|
||||||
|
string buffer = "";
|
||||||
|
DateTime deadline = DateTime.Now.AddMilliseconds(timeoutMs);
|
||||||
|
while (DateTime.Now < deadline)
|
||||||
|
{
|
||||||
|
if (_port.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
buffer += (char)_port.ReadByte();
|
||||||
|
if (buffer.Length > 4)
|
||||||
|
buffer = buffer.Substring(buffer.Length - 4);
|
||||||
|
if (buffer == "MORE" || buffer == "ERR!")
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint CalculateChecksum(byte[] data)
|
||||||
|
{
|
||||||
|
// Force V3-style for PCDrv reads (per NOTPSXSerial: forceProtocolV3=true)
|
||||||
|
uint sum = 0;
|
||||||
|
for (int i = 0; i < data.Length; i++)
|
||||||
|
sum += data[i];
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/PCdrvSerialHost.cs.meta
Normal file
2
Editor/Core/PCdrvSerialHost.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d27c6a94b1c1f07418799b65d13f7097
|
||||||
318
Editor/Core/PSXAudioConverter.cs
Normal file
318
Editor/Core/PSXAudioConverter.cs
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using Debug = UnityEngine.Debug;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads psxavenc and converts WAV audio to PS1 SPU ADPCM format.
|
||||||
|
/// psxavenc is the standard tool for PS1 audio encoding from the
|
||||||
|
/// WonderfulToolchain project.
|
||||||
|
/// </summary>
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public static class PSXAudioConverter
|
||||||
|
{
|
||||||
|
static PSXAudioConverter()
|
||||||
|
{
|
||||||
|
// Register the converter delegate so Runtime code can call it
|
||||||
|
// without directly referencing this Editor assembly.
|
||||||
|
PSXSceneExporter.AudioConvertDelegate = ConvertToADPCM;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string PSXAVENC_VERSION = "v0.3.1";
|
||||||
|
private const string PSXAVENC_RELEASE_BASE =
|
||||||
|
"https://github.com/WonderfulToolchain/psxavenc/releases/download/";
|
||||||
|
|
||||||
|
private static readonly HttpClient _http = new HttpClient();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the psxavenc binary inside .tools/
|
||||||
|
/// </summary>
|
||||||
|
public static string PsxavencBinary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(SplashBuildPaths.ToolsDir, "psxavenc");
|
||||||
|
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||||
|
return Path.Combine(dir, "psxavenc.exe");
|
||||||
|
return Path.Combine(dir, "psxavenc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsInstalled() => File.Exists(PsxavencBinary);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads and installs psxavenc from the official GitHub releases.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
|
||||||
|
{
|
||||||
|
string platformSuffix;
|
||||||
|
string archiveName;
|
||||||
|
switch (Application.platform)
|
||||||
|
{
|
||||||
|
case RuntimePlatform.WindowsEditor:
|
||||||
|
platformSuffix = "x86_64-pc-windows-msvc";
|
||||||
|
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.zip";
|
||||||
|
break;
|
||||||
|
case RuntimePlatform.LinuxEditor:
|
||||||
|
platformSuffix = "x86_64-unknown-linux-gnu";
|
||||||
|
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.tar.gz";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log?.Invoke("Only Windows and Linux are supported.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string downloadUrl = $"{PSXAVENC_RELEASE_BASE}{PSXAVENC_VERSION}/{archiveName}";
|
||||||
|
log?.Invoke($"Downloading psxavenc: {downloadUrl}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
|
||||||
|
EditorUtility.DisplayProgressBar("Downloading psxavenc", "Downloading...", 0.1f);
|
||||||
|
|
||||||
|
using (var response = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
long? totalBytes = response.Content.Headers.ContentLength;
|
||||||
|
long downloaded = 0;
|
||||||
|
|
||||||
|
using (var fs = File.Create(tempFile))
|
||||||
|
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[81920];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
await fs.WriteAsync(buffer, 0, bytesRead);
|
||||||
|
downloaded += bytesRead;
|
||||||
|
if (totalBytes.HasValue)
|
||||||
|
{
|
||||||
|
float progress = (float)downloaded / totalBytes.Value;
|
||||||
|
EditorUtility.DisplayProgressBar("Downloading psxavenc",
|
||||||
|
$"{downloaded / 1024}/{totalBytes.Value / 1024} KB", progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke("Extracting...");
|
||||||
|
EditorUtility.DisplayProgressBar("Installing psxavenc", "Extracting...", 0.9f);
|
||||||
|
|
||||||
|
string installDir = Path.Combine(SplashBuildPaths.ToolsDir, "psxavenc");
|
||||||
|
if (Directory.Exists(installDir))
|
||||||
|
Directory.Delete(installDir, true);
|
||||||
|
Directory.CreateDirectory(installDir);
|
||||||
|
|
||||||
|
if (tempFile.EndsWith(".zip"))
|
||||||
|
{
|
||||||
|
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// tar.gz extraction — use system tar
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "tar",
|
||||||
|
Arguments = $"xzf \"{tempFile}\" -C \"{installDir}\" --strip-components=1",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
var proc = Process.Start(psi);
|
||||||
|
proc.WaitForExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix nested directory (sometimes archives have one extra level)
|
||||||
|
FixNestedDirectory(installDir);
|
||||||
|
|
||||||
|
try { File.Delete(tempFile); } catch { }
|
||||||
|
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
|
||||||
|
if (IsInstalled())
|
||||||
|
{
|
||||||
|
// Make executable on Linux
|
||||||
|
if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||||
|
{
|
||||||
|
var chmod = Process.Start("chmod", $"+x \"{PsxavencBinary}\"");
|
||||||
|
chmod?.WaitForExit();
|
||||||
|
}
|
||||||
|
log?.Invoke("psxavenc installed successfully!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke($"psxavenc binary not found at: {PsxavencBinary}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log?.Invoke($"psxavenc download failed: {ex.Message}");
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FixNestedDirectory(string dir)
|
||||||
|
{
|
||||||
|
// If extraction created exactly one subdirectory, flatten it
|
||||||
|
var subdirs = Directory.GetDirectories(dir);
|
||||||
|
if (subdirs.Length == 1)
|
||||||
|
{
|
||||||
|
string nested = subdirs[0];
|
||||||
|
foreach (string file in Directory.GetFiles(nested))
|
||||||
|
{
|
||||||
|
string dest = Path.Combine(dir, Path.GetFileName(file));
|
||||||
|
if (!File.Exists(dest)) File.Move(file, dest);
|
||||||
|
}
|
||||||
|
foreach (string sub in Directory.GetDirectories(nested))
|
||||||
|
{
|
||||||
|
string dest = Path.Combine(dir, Path.GetFileName(sub));
|
||||||
|
if (!Directory.Exists(dest)) Directory.Move(sub, dest);
|
||||||
|
}
|
||||||
|
try { Directory.Delete(nested, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a Unity AudioClip to PS1 SPU ADPCM format using psxavenc.
|
||||||
|
/// Returns the ADPCM byte array, or null on failure.
|
||||||
|
/// </summary>
|
||||||
|
public static byte[] ConvertToADPCM(AudioClip clip, int targetSampleRate, bool loop)
|
||||||
|
{
|
||||||
|
if (!IsInstalled())
|
||||||
|
{
|
||||||
|
Debug.LogError("[SplashEdit] psxavenc not installed. Install it from the Setup tab.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clip == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[SplashEdit] AudioClip is null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export Unity AudioClip to a temporary WAV file
|
||||||
|
string tempWav = Path.Combine(Path.GetTempPath(), $"psx_audio_{clip.name}.wav");
|
||||||
|
string tempVag = Path.Combine(Path.GetTempPath(), $"psx_audio_{clip.name}.vag");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExportWav(clip, tempWav);
|
||||||
|
|
||||||
|
// Run psxavenc: convert WAV to SPU ADPCM
|
||||||
|
// -t spu: raw SPU ADPCM output (no header, ready for DMA upload)
|
||||||
|
// -f <rate>: target sample rate
|
||||||
|
// -L: enable looping flag in the last ADPCM block
|
||||||
|
string loopFlag = loop ? "-L" : "";
|
||||||
|
string args = $"-t spu -f {targetSampleRate} {loopFlag} \"{tempWav}\" \"{tempVag}\"";
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = PsxavencBinary,
|
||||||
|
Arguments = args,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = Process.Start(psi);
|
||||||
|
string stderr = process.StandardError.ReadToEnd();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[SplashEdit] psxavenc failed: {stderr}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(tempVag))
|
||||||
|
{
|
||||||
|
Debug.LogError("[SplashEdit] psxavenc produced no output file.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -t spu outputs raw SPU ADPCM blocks (no header) — use directly.
|
||||||
|
byte[] adpcm = File.ReadAllBytes(tempVag);
|
||||||
|
if (adpcm.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogError("[SplashEdit] psxavenc produced empty output.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return adpcm;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (File.Exists(tempWav)) File.Delete(tempWav); } catch { }
|
||||||
|
try { if (File.Exists(tempVag)) File.Delete(tempVag); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports a Unity AudioClip to a 16-bit mono WAV file.
|
||||||
|
/// </summary>
|
||||||
|
private static void ExportWav(AudioClip clip, string path)
|
||||||
|
{
|
||||||
|
float[] samples = new float[clip.samples * clip.channels];
|
||||||
|
clip.GetData(samples, 0);
|
||||||
|
|
||||||
|
// Downmix to mono if stereo
|
||||||
|
float[] mono;
|
||||||
|
if (clip.channels > 1)
|
||||||
|
{
|
||||||
|
mono = new float[clip.samples];
|
||||||
|
for (int i = 0; i < clip.samples; i++)
|
||||||
|
{
|
||||||
|
float sum = 0;
|
||||||
|
for (int ch = 0; ch < clip.channels; ch++)
|
||||||
|
sum += samples[i * clip.channels + ch];
|
||||||
|
mono[i] = sum / clip.channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mono = samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write WAV
|
||||||
|
using (var fs = new FileStream(path, FileMode.Create))
|
||||||
|
using (var writer = new BinaryWriter(fs))
|
||||||
|
{
|
||||||
|
int sampleCount = mono.Length;
|
||||||
|
int dataSize = sampleCount * 2; // 16-bit
|
||||||
|
int fileSize = 44 + dataSize;
|
||||||
|
|
||||||
|
// RIFF header
|
||||||
|
writer.Write(new char[] { 'R', 'I', 'F', 'F' });
|
||||||
|
writer.Write(fileSize - 8);
|
||||||
|
writer.Write(new char[] { 'W', 'A', 'V', 'E' });
|
||||||
|
|
||||||
|
// fmt chunk
|
||||||
|
writer.Write(new char[] { 'f', 'm', 't', ' ' });
|
||||||
|
writer.Write(16); // chunk size
|
||||||
|
writer.Write((short)1); // PCM
|
||||||
|
writer.Write((short)1); // mono
|
||||||
|
writer.Write(clip.frequency);
|
||||||
|
writer.Write(clip.frequency * 2); // byte rate
|
||||||
|
writer.Write((short)2); // block align
|
||||||
|
writer.Write((short)16); // bits per sample
|
||||||
|
|
||||||
|
// data chunk
|
||||||
|
writer.Write(new char[] { 'd', 'a', 't', 'a' });
|
||||||
|
writer.Write(dataSize);
|
||||||
|
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
{
|
||||||
|
short sample = (short)(Mathf.Clamp(mono[i], -1f, 1f) * 32767f);
|
||||||
|
writer.Write(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/PSXAudioConverter.cs.meta
Normal file
2
Editor/Core/PSXAudioConverter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 372b2ef07e125584ba43312b0662d7ac
|
||||||
418
Editor/Core/PSXConsoleWindow.cs
Normal file
418
Editor/Core/PSXConsoleWindow.cs
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A live console window that displays stdout/stderr from PCSX-Redux and PSX build output.
|
||||||
|
/// Opens automatically when a build starts or the emulator launches.
|
||||||
|
/// </summary>
|
||||||
|
public class PSXConsoleWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private const string WINDOW_TITLE = "PSX Console";
|
||||||
|
private const string MENU_PATH = "PlayStation 1/PSX Console";
|
||||||
|
private const int MAX_LINES = 2000;
|
||||||
|
private const int TRIM_AMOUNT = 500;
|
||||||
|
|
||||||
|
// ── Shared state (set by SplashControlPanel) ──
|
||||||
|
private static Process _process;
|
||||||
|
private static readonly List<LogLine> _lines = new List<LogLine>();
|
||||||
|
private static readonly object _lock = new object();
|
||||||
|
private static volatile bool _autoScroll = true;
|
||||||
|
private static volatile bool _reading;
|
||||||
|
|
||||||
|
// ── Instance state ──
|
||||||
|
private Vector2 _scrollPos;
|
||||||
|
private string _filterText = "";
|
||||||
|
private bool _showStdout = true;
|
||||||
|
private bool _showStderr = true;
|
||||||
|
private bool _wrapLines = true;
|
||||||
|
private GUIStyle _monoStyle;
|
||||||
|
private GUIStyle _monoStyleErr;
|
||||||
|
private GUIStyle _monoStyleSelected;
|
||||||
|
private int _lastLineCount;
|
||||||
|
|
||||||
|
// ── Selection state (for shift-click range and right-click copy) ──
|
||||||
|
private int _selectionAnchor = -1; // first clicked line index (into _lines)
|
||||||
|
private int _selectionEnd = -1; // last shift-clicked line index (into _lines)
|
||||||
|
|
||||||
|
private struct LogLine
|
||||||
|
{
|
||||||
|
public string text;
|
||||||
|
public bool isError;
|
||||||
|
public string timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Menu
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[MenuItem(MENU_PATH, false, 10)]
|
||||||
|
public static void ShowWindow()
|
||||||
|
{
|
||||||
|
var window = GetWindow<PSXConsoleWindow>();
|
||||||
|
window.titleContent = new GUIContent(WINDOW_TITLE, EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
|
||||||
|
window.minSize = new Vector2(400, 200);
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Public API — called by SplashControlPanel
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a line to the console from any source (serial host, emulator fallback, etc.).
|
||||||
|
/// Thread-safe. Works whether the window is open or not.
|
||||||
|
/// </summary>
|
||||||
|
public static void AddLine(string text, bool isError = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_lines.Add(new LogLine
|
||||||
|
{
|
||||||
|
text = text,
|
||||||
|
isError = isError,
|
||||||
|
timestamp = DateTime.Now.ToString("HH:mm:ss.fff")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_lines.Count > MAX_LINES)
|
||||||
|
{
|
||||||
|
_lines.RemoveRange(0, TRIM_AMOUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repaint is handled by OnEditorUpdate polling _lines.Count changes.
|
||||||
|
// Do NOT call EditorApplication.delayCall here - AddLine is called
|
||||||
|
// from background threads (serial host, process readers) and
|
||||||
|
// delayCall is not thread-safe. It kills the calling thread.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens the console window and begins capturing output from the given process.
|
||||||
|
/// The process must have RedirectStandardOutput and RedirectStandardError enabled.
|
||||||
|
/// </summary>
|
||||||
|
public static PSXConsoleWindow Attach(Process process)
|
||||||
|
{
|
||||||
|
// Stop reading from any previous process (but keep existing lines)
|
||||||
|
_reading = false;
|
||||||
|
|
||||||
|
_process = process;
|
||||||
|
|
||||||
|
var window = GetWindow<PSXConsoleWindow>();
|
||||||
|
window.titleContent = new GUIContent(WINDOW_TITLE, EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
|
||||||
|
window.minSize = new Vector2(400, 200);
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
// Start async readers
|
||||||
|
_reading = true;
|
||||||
|
StartReader(process.StandardOutput, false);
|
||||||
|
StartReader(process.StandardError, true);
|
||||||
|
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops reading and detaches from the current process.
|
||||||
|
/// </summary>
|
||||||
|
public static void Detach()
|
||||||
|
{
|
||||||
|
_reading = false;
|
||||||
|
_process = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Async readers
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void StartReader(System.IO.StreamReader reader, bool isError)
|
||||||
|
{
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_reading && !reader.EndOfStream)
|
||||||
|
{
|
||||||
|
string line = reader.ReadLine();
|
||||||
|
if (line == null) break;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_lines.Add(new LogLine
|
||||||
|
{
|
||||||
|
text = line,
|
||||||
|
isError = isError,
|
||||||
|
timestamp = DateTime.Now.ToString("HH:mm:ss.fff")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trim if too many lines
|
||||||
|
if (_lines.Count > MAX_LINES)
|
||||||
|
{
|
||||||
|
_lines.RemoveRange(0, TRIM_AMOUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Stream closed — normal when process exits
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = isError ? "PSXConsole-stderr" : "PSXConsole-stdout"
|
||||||
|
};
|
||||||
|
thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Window lifecycle
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
EditorApplication.update += OnEditorUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
EditorApplication.update -= OnEditorUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEditorUpdate()
|
||||||
|
{
|
||||||
|
// Repaint when new lines arrive
|
||||||
|
int count;
|
||||||
|
lock (_lock) { count = _lines.Count; }
|
||||||
|
if (count != _lastLineCount)
|
||||||
|
{
|
||||||
|
_lastLineCount = count;
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// GUI
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void EnsureStyles()
|
||||||
|
{
|
||||||
|
if (_monoStyle == null)
|
||||||
|
{
|
||||||
|
_monoStyle = new GUIStyle(EditorStyles.label)
|
||||||
|
{
|
||||||
|
font = Font.CreateDynamicFontFromOSFont("Consolas", 12),
|
||||||
|
fontSize = 11,
|
||||||
|
richText = false,
|
||||||
|
wordWrap = _wrapLines,
|
||||||
|
normal = { textColor = new Color(0.85f, 0.85f, 0.85f) },
|
||||||
|
padding = new RectOffset(4, 4, 1, 1),
|
||||||
|
margin = new RectOffset(0, 0, 0, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (_monoStyleErr == null)
|
||||||
|
{
|
||||||
|
_monoStyleErr = new GUIStyle(_monoStyle)
|
||||||
|
{
|
||||||
|
normal = { textColor = new Color(1f, 0.45f, 0.4f) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (_monoStyleSelected == null)
|
||||||
|
{
|
||||||
|
_monoStyleSelected = new GUIStyle(_monoStyle)
|
||||||
|
{
|
||||||
|
normal =
|
||||||
|
{
|
||||||
|
textColor = new Color(0.95f, 0.95f, 0.95f),
|
||||||
|
background = MakeSolidTexture(new Color(0.25f, 0.40f, 0.65f, 0.6f))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_monoStyle.wordWrap = _wrapLines;
|
||||||
|
_monoStyleErr.wordWrap = _wrapLines;
|
||||||
|
_monoStyleSelected.wordWrap = _wrapLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture2D MakeSolidTexture(Color color)
|
||||||
|
{
|
||||||
|
var tex = new Texture2D(1, 1);
|
||||||
|
tex.SetPixel(0, 0, color);
|
||||||
|
tex.Apply();
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
EnsureStyles();
|
||||||
|
DrawToolbar();
|
||||||
|
DrawConsoleOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawToolbar()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||||
|
|
||||||
|
// Process status
|
||||||
|
bool alive = _process != null && !_process.HasExited;
|
||||||
|
var statusColor = GUI.contentColor;
|
||||||
|
GUI.contentColor = alive ? Color.green : Color.gray;
|
||||||
|
GUILayout.Label(alive ? "● Live" : "● Stopped", EditorStyles.toolbarButton, GUILayout.Width(60));
|
||||||
|
GUI.contentColor = statusColor;
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
GUILayout.Label("Filter:", GUILayout.Width(40));
|
||||||
|
_filterText = EditorGUILayout.TextField(_filterText, EditorStyles.toolbarSearchField, GUILayout.Width(150));
|
||||||
|
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
|
// Toggles
|
||||||
|
_showStdout = GUILayout.Toggle(_showStdout, "stdout", EditorStyles.toolbarButton, GUILayout.Width(50));
|
||||||
|
_showStderr = GUILayout.Toggle(_showStderr, "stderr", EditorStyles.toolbarButton, GUILayout.Width(50));
|
||||||
|
_wrapLines = GUILayout.Toggle(_wrapLines, "Wrap", EditorStyles.toolbarButton, GUILayout.Width(40));
|
||||||
|
|
||||||
|
// Auto-scroll
|
||||||
|
_autoScroll = GUILayout.Toggle(_autoScroll, "Auto↓", EditorStyles.toolbarButton, GUILayout.Width(50));
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(45)))
|
||||||
|
{
|
||||||
|
lock (_lock) { _lines.Clear(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all
|
||||||
|
if (GUILayout.Button("Copy", EditorStyles.toolbarButton, GUILayout.Width(40)))
|
||||||
|
{
|
||||||
|
CopyToClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawConsoleOutput()
|
||||||
|
{
|
||||||
|
// Simple scroll view - no BeginArea/EndArea mixing that causes layout errors.
|
||||||
|
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
|
||||||
|
|
||||||
|
// Dark background behind the scroll content
|
||||||
|
Rect scrollBg = EditorGUILayout.BeginVertical();
|
||||||
|
EditorGUI.DrawRect(scrollBg, new Color(0.13f, 0.13f, 0.15f));
|
||||||
|
|
||||||
|
bool hasFilter = !string.IsNullOrEmpty(_filterText);
|
||||||
|
string filterLower = hasFilter ? _filterText.ToLowerInvariant() : null;
|
||||||
|
|
||||||
|
int selMin = Mathf.Min(_selectionAnchor, _selectionEnd);
|
||||||
|
int selMax = Mathf.Max(_selectionAnchor, _selectionEnd);
|
||||||
|
bool hasSelection = _selectionAnchor >= 0 && _selectionEnd >= 0;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_lines.Count == 0)
|
||||||
|
{
|
||||||
|
GUILayout.Label("Waiting for output...", EditorStyles.centeredGreyMiniLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _lines.Count; i++)
|
||||||
|
{
|
||||||
|
var line = _lines[i];
|
||||||
|
|
||||||
|
if (line.isError && !_showStderr) continue;
|
||||||
|
if (!line.isError && !_showStdout) continue;
|
||||||
|
if (hasFilter && line.text.ToLowerInvariant().IndexOf(filterLower, StringComparison.Ordinal) < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool selected = hasSelection && i >= selMin && i <= selMax;
|
||||||
|
GUIStyle style = selected ? _monoStyleSelected : (line.isError ? _monoStyleErr : _monoStyle);
|
||||||
|
|
||||||
|
string label = $"[{line.timestamp}] {line.text}";
|
||||||
|
GUILayout.Label(label, style);
|
||||||
|
|
||||||
|
// Handle click/right-click on last drawn rect
|
||||||
|
Rect lineRect = GUILayoutUtility.GetLastRect();
|
||||||
|
Event evt = Event.current;
|
||||||
|
if (evt.type == EventType.MouseDown && lineRect.Contains(evt.mousePosition))
|
||||||
|
{
|
||||||
|
if (evt.button == 0)
|
||||||
|
{
|
||||||
|
if (evt.shift && _selectionAnchor >= 0)
|
||||||
|
_selectionEnd = i;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectionAnchor = i;
|
||||||
|
_selectionEnd = i;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
if (_autoScroll)
|
||||||
|
_scrollPos.y = float.MaxValue;
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyRange(int fromIndex, int toIndex)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
int lo = Mathf.Min(fromIndex, toIndex);
|
||||||
|
int hi = Mathf.Max(fromIndex, toIndex);
|
||||||
|
for (int i = lo; i <= hi && i < _lines.Count; i++)
|
||||||
|
{
|
||||||
|
string prefix = _lines[i].isError ? "[ERR]" : "[OUT]";
|
||||||
|
sb.AppendLine($"[{_lines[i].timestamp}] {prefix} {_lines[i].text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUIUtility.systemCopyBuffer = sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToClipboard()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
foreach (var line in _lines)
|
||||||
|
{
|
||||||
|
string prefix = line.isError ? "[ERR]" : "[OUT]";
|
||||||
|
sb.AppendLine($"[{line.timestamp}] {prefix} {line.text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUIUtility.systemCopyBuffer = sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/PSXConsoleWindow.cs.meta
Normal file
2
Editor/Core/PSXConsoleWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c4e13fc5b859ac14099eb9f259ba11f0
|
||||||
777
Editor/Core/PSXEditorStyles.cs
Normal file
777
Editor/Core/PSXEditorStyles.cs
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unified styling system for PSX Splash editor windows.
|
||||||
|
/// Provides consistent colors, fonts, icons, and GUIStyles across the entire plugin.
|
||||||
|
/// </summary>
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public static class PSXEditorStyles
|
||||||
|
{
|
||||||
|
static PSXEditorStyles()
|
||||||
|
{
|
||||||
|
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnBeforeAssemblyReload()
|
||||||
|
{
|
||||||
|
foreach (var tex in _textureCache.Values)
|
||||||
|
{
|
||||||
|
if (tex != null)
|
||||||
|
Object.DestroyImmediate(tex);
|
||||||
|
}
|
||||||
|
_textureCache.Clear();
|
||||||
|
_styleCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Colors - PS1 Inspired Palette
|
||||||
|
|
||||||
|
// Primary colors
|
||||||
|
public static readonly Color PrimaryBlue = new Color(0.15f, 0.35f, 0.65f);
|
||||||
|
public static readonly Color PrimaryDark = new Color(0.12f, 0.12f, 0.14f);
|
||||||
|
public static readonly Color PrimaryLight = new Color(0.22f, 0.22f, 0.25f);
|
||||||
|
|
||||||
|
// Accent colors
|
||||||
|
public static readonly Color AccentGold = new Color(0.95f, 0.75f, 0.2f);
|
||||||
|
public static readonly Color AccentCyan = new Color(0.3f, 0.85f, 0.95f);
|
||||||
|
public static readonly Color AccentMagenta = new Color(0.85f, 0.3f, 0.65f);
|
||||||
|
public static readonly Color AccentGreen = new Color(0.35f, 0.85f, 0.45f);
|
||||||
|
|
||||||
|
// Semantic colors
|
||||||
|
public static readonly Color Success = new Color(0.35f, 0.8f, 0.4f);
|
||||||
|
public static readonly Color Warning = new Color(0.95f, 0.75f, 0.2f);
|
||||||
|
public static readonly Color Error = new Color(0.9f, 0.3f, 0.35f);
|
||||||
|
public static readonly Color Info = new Color(0.4f, 0.7f, 0.95f);
|
||||||
|
|
||||||
|
// Background colors
|
||||||
|
public static readonly Color BackgroundDark = new Color(0.15f, 0.15f, 0.17f);
|
||||||
|
public static readonly Color BackgroundMedium = new Color(0.2f, 0.2f, 0.22f);
|
||||||
|
public static readonly Color BackgroundLight = new Color(0.28f, 0.28f, 0.3f);
|
||||||
|
public static readonly Color BackgroundHighlight = new Color(0.25f, 0.35f, 0.5f);
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
public static readonly Color TextPrimary = new Color(0.9f, 0.9f, 0.92f);
|
||||||
|
public static readonly Color TextSecondary = new Color(0.65f, 0.65f, 0.7f);
|
||||||
|
public static readonly Color TextMuted = new Color(0.45f, 0.45f, 0.5f);
|
||||||
|
|
||||||
|
// VRAM specific colors
|
||||||
|
public static readonly Color VRAMFrameBuffer1 = new Color(1f, 0.3f, 0.3f, 0.4f);
|
||||||
|
public static readonly Color VRAMFrameBuffer2 = new Color(0.3f, 1f, 0.3f, 0.4f);
|
||||||
|
public static readonly Color VRAMProhibited = new Color(1f, 0f, 0f, 0.25f);
|
||||||
|
public static readonly Color VRAMTexture = new Color(0.3f, 0.6f, 1f, 0.5f);
|
||||||
|
public static readonly Color VRAMCLUT = new Color(1f, 0.6f, 0.3f, 0.5f);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Cached Styles
|
||||||
|
|
||||||
|
private static Dictionary<string, GUIStyle> _styleCache = new Dictionary<string, GUIStyle>();
|
||||||
|
private static Dictionary<string, Texture2D> _textureCache = new Dictionary<string, Texture2D>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Textures
|
||||||
|
|
||||||
|
public static Texture2D GetSolidTexture(Color color)
|
||||||
|
{
|
||||||
|
string key = $"solid_{color.r}_{color.g}_{color.b}_{color.a}";
|
||||||
|
if (!_textureCache.TryGetValue(key, out var tex) || tex == null)
|
||||||
|
{
|
||||||
|
tex = new Texture2D(1, 1);
|
||||||
|
tex.SetPixel(0, 0, color);
|
||||||
|
tex.Apply();
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
_textureCache[key] = tex;
|
||||||
|
}
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture2D CreateGradientTexture(int width, int height, Color top, Color bottom)
|
||||||
|
{
|
||||||
|
Texture2D tex = new Texture2D(width, height);
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
Color c = Color.Lerp(bottom, top, (float)y / height);
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
tex.SetPixel(x, y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tex.Apply();
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture2D CreateRoundedRect(int width, int height, int radius, Color fillColor, Color borderColor, int borderWidth = 1)
|
||||||
|
{
|
||||||
|
Texture2D tex = new Texture2D(width, height);
|
||||||
|
Color transparent = new Color(0, 0, 0, 0);
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
// Check if pixel is within rounded corners
|
||||||
|
bool inCorner = false;
|
||||||
|
float dist = 0;
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
if (x < radius && y > height - radius - 1)
|
||||||
|
{
|
||||||
|
dist = Vector2.Distance(new Vector2(x, y), new Vector2(radius, height - radius - 1));
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
// Top-right
|
||||||
|
else if (x > width - radius - 1 && y > height - radius - 1)
|
||||||
|
{
|
||||||
|
dist = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, height - radius - 1));
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
// Bottom-left
|
||||||
|
else if (x < radius && y < radius)
|
||||||
|
{
|
||||||
|
dist = Vector2.Distance(new Vector2(x, y), new Vector2(radius, radius));
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
// Bottom-right
|
||||||
|
else if (x > width - radius - 1 && y < radius)
|
||||||
|
{
|
||||||
|
dist = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, radius));
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCorner)
|
||||||
|
{
|
||||||
|
if (dist > radius)
|
||||||
|
tex.SetPixel(x, y, transparent);
|
||||||
|
else if (dist > radius - borderWidth)
|
||||||
|
tex.SetPixel(x, y, borderColor);
|
||||||
|
else
|
||||||
|
tex.SetPixel(x, y, fillColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Check border
|
||||||
|
if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth)
|
||||||
|
tex.SetPixel(x, y, borderColor);
|
||||||
|
else
|
||||||
|
tex.SetPixel(x, y, fillColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tex.Apply();
|
||||||
|
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region GUIStyles
|
||||||
|
|
||||||
|
private static GUIStyle _windowHeader;
|
||||||
|
public static GUIStyle WindowHeader
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_windowHeader == null)
|
||||||
|
{
|
||||||
|
_windowHeader = new GUIStyle(EditorStyles.boldLabel)
|
||||||
|
{
|
||||||
|
fontSize = 18,
|
||||||
|
alignment = TextAnchor.MiddleLeft,
|
||||||
|
padding = new RectOffset(10, 10, 8, 8),
|
||||||
|
margin = new RectOffset(0, 0, 0, 5)
|
||||||
|
};
|
||||||
|
_windowHeader.normal.textColor = TextPrimary;
|
||||||
|
}
|
||||||
|
return _windowHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _sectionHeader;
|
||||||
|
public static GUIStyle SectionHeader
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_sectionHeader == null)
|
||||||
|
{
|
||||||
|
_sectionHeader = new GUIStyle(EditorStyles.boldLabel)
|
||||||
|
{
|
||||||
|
fontSize = 14,
|
||||||
|
alignment = TextAnchor.MiddleLeft,
|
||||||
|
padding = new RectOffset(5, 5, 8, 8),
|
||||||
|
margin = new RectOffset(0, 0, 10, 5)
|
||||||
|
};
|
||||||
|
_sectionHeader.normal.textColor = TextPrimary;
|
||||||
|
}
|
||||||
|
return _sectionHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _cardStyle;
|
||||||
|
public static GUIStyle CardStyle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_cardStyle == null)
|
||||||
|
{
|
||||||
|
_cardStyle = new GUIStyle()
|
||||||
|
{
|
||||||
|
padding = new RectOffset(12, 12, 10, 10),
|
||||||
|
margin = new RectOffset(5, 5, 5, 5),
|
||||||
|
normal = { background = GetSolidTexture(BackgroundMedium) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _cardStyle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _cardHeaderStyle;
|
||||||
|
public static GUIStyle CardHeaderStyle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_cardHeaderStyle == null)
|
||||||
|
{
|
||||||
|
_cardHeaderStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||||
|
{
|
||||||
|
fontSize = 13,
|
||||||
|
padding = new RectOffset(0, 0, 0, 5),
|
||||||
|
margin = new RectOffset(0, 0, 0, 5)
|
||||||
|
};
|
||||||
|
_cardHeaderStyle.normal.textColor = TextPrimary;
|
||||||
|
}
|
||||||
|
return _cardHeaderStyle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _primaryButton;
|
||||||
|
public static GUIStyle PrimaryButton
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_primaryButton == null)
|
||||||
|
{
|
||||||
|
_primaryButton = new GUIStyle(GUI.skin.button)
|
||||||
|
{
|
||||||
|
fontSize = 12,
|
||||||
|
fontStyle = FontStyle.Bold,
|
||||||
|
padding = new RectOffset(15, 15, 8, 8),
|
||||||
|
margin = new RectOffset(5, 5, 5, 5),
|
||||||
|
alignment = TextAnchor.MiddleCenter
|
||||||
|
};
|
||||||
|
_primaryButton.normal.textColor = Color.white;
|
||||||
|
_primaryButton.normal.background = GetSolidTexture(PrimaryBlue);
|
||||||
|
_primaryButton.hover.background = GetSolidTexture(PrimaryBlue * 1.2f);
|
||||||
|
_primaryButton.active.background = GetSolidTexture(PrimaryBlue * 0.8f);
|
||||||
|
}
|
||||||
|
return _primaryButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _secondaryButton;
|
||||||
|
public static GUIStyle SecondaryButton
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_secondaryButton == null)
|
||||||
|
{
|
||||||
|
_secondaryButton = new GUIStyle(GUI.skin.button)
|
||||||
|
{
|
||||||
|
fontSize = 11,
|
||||||
|
padding = new RectOffset(12, 12, 6, 6),
|
||||||
|
margin = new RectOffset(3, 3, 3, 3),
|
||||||
|
alignment = TextAnchor.MiddleCenter
|
||||||
|
};
|
||||||
|
_secondaryButton.normal.textColor = TextPrimary;
|
||||||
|
_secondaryButton.normal.background = GetSolidTexture(BackgroundLight);
|
||||||
|
_secondaryButton.hover.background = GetSolidTexture(BackgroundLight * 1.3f);
|
||||||
|
_secondaryButton.active.background = GetSolidTexture(BackgroundLight * 0.7f);
|
||||||
|
}
|
||||||
|
return _secondaryButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _successButton;
|
||||||
|
public static GUIStyle SuccessButton
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_successButton == null)
|
||||||
|
{
|
||||||
|
_successButton = new GUIStyle(PrimaryButton);
|
||||||
|
_successButton.normal.background = GetSolidTexture(Success * 0.8f);
|
||||||
|
_successButton.hover.background = GetSolidTexture(Success);
|
||||||
|
_successButton.active.background = GetSolidTexture(Success * 0.6f);
|
||||||
|
}
|
||||||
|
return _successButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _dangerButton;
|
||||||
|
public static GUIStyle DangerButton
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_dangerButton == null)
|
||||||
|
{
|
||||||
|
_dangerButton = new GUIStyle(PrimaryButton);
|
||||||
|
_dangerButton.normal.background = GetSolidTexture(Error * 0.8f);
|
||||||
|
_dangerButton.hover.background = GetSolidTexture(Error);
|
||||||
|
_dangerButton.active.background = GetSolidTexture(Error * 0.6f);
|
||||||
|
}
|
||||||
|
return _dangerButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _statusBadge;
|
||||||
|
public static GUIStyle StatusBadge
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_statusBadge == null)
|
||||||
|
{
|
||||||
|
_statusBadge = new GUIStyle(EditorStyles.label)
|
||||||
|
{
|
||||||
|
fontSize = 10,
|
||||||
|
fontStyle = FontStyle.Bold,
|
||||||
|
alignment = TextAnchor.MiddleCenter,
|
||||||
|
padding = new RectOffset(8, 8, 3, 3),
|
||||||
|
margin = new RectOffset(3, 3, 3, 3)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _statusBadge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _toolbarStyle;
|
||||||
|
public static GUIStyle ToolbarStyle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_toolbarStyle == null)
|
||||||
|
{
|
||||||
|
_toolbarStyle = new GUIStyle()
|
||||||
|
{
|
||||||
|
padding = new RectOffset(8, 8, 6, 6),
|
||||||
|
margin = new RectOffset(0, 0, 0, 0),
|
||||||
|
normal = { background = GetSolidTexture(BackgroundDark) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _toolbarStyle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _infoBox;
|
||||||
|
public static GUIStyle InfoBox
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_infoBox == null)
|
||||||
|
{
|
||||||
|
_infoBox = new GUIStyle(EditorStyles.helpBox)
|
||||||
|
{
|
||||||
|
fontSize = 11,
|
||||||
|
padding = new RectOffset(10, 10, 8, 8),
|
||||||
|
margin = new RectOffset(5, 5, 5, 5),
|
||||||
|
richText = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _infoBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _centeredLabel;
|
||||||
|
public static GUIStyle CenteredLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_centeredLabel == null)
|
||||||
|
{
|
||||||
|
_centeredLabel = new GUIStyle(EditorStyles.label)
|
||||||
|
{
|
||||||
|
alignment = TextAnchor.MiddleCenter,
|
||||||
|
wordWrap = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _centeredLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _richLabel;
|
||||||
|
public static GUIStyle RichLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_richLabel == null)
|
||||||
|
{
|
||||||
|
_richLabel = new GUIStyle(EditorStyles.label)
|
||||||
|
{
|
||||||
|
richText = true,
|
||||||
|
wordWrap = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _richLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GUIStyle _foldoutHeader;
|
||||||
|
public static GUIStyle FoldoutHeader
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_foldoutHeader == null)
|
||||||
|
{
|
||||||
|
_foldoutHeader = new GUIStyle(EditorStyles.foldout)
|
||||||
|
{
|
||||||
|
fontSize = 12,
|
||||||
|
fontStyle = FontStyle.Bold,
|
||||||
|
padding = new RectOffset(15, 0, 3, 3)
|
||||||
|
};
|
||||||
|
_foldoutHeader.normal.textColor = TextPrimary;
|
||||||
|
}
|
||||||
|
return _foldoutHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Drawing Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a horizontal separator line
|
||||||
|
/// </summary>
|
||||||
|
public static void DrawSeparator(float topMargin = 5, float bottomMargin = 5)
|
||||||
|
{
|
||||||
|
GUILayout.Space(topMargin);
|
||||||
|
var rect = GUILayoutUtility.GetRect(1, 1, GUILayout.ExpandWidth(true));
|
||||||
|
EditorGUI.DrawRect(rect, TextMuted * 0.5f);
|
||||||
|
GUILayout.Space(bottomMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a status badge with color
|
||||||
|
/// </summary>
|
||||||
|
public static void DrawStatusBadge(string text, Color color, float width = 80)
|
||||||
|
{
|
||||||
|
var style = new GUIStyle(StatusBadge);
|
||||||
|
style.normal.background = GetSolidTexture(color);
|
||||||
|
style.normal.textColor = GetContrastColor(color);
|
||||||
|
GUILayout.Label(text, style, GUILayout.Width(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a progress bar
|
||||||
|
/// </summary>
|
||||||
|
public static void DrawProgressBar(float progress, string label, Color fillColor, float height = 20)
|
||||||
|
{
|
||||||
|
var rect = GUILayoutUtility.GetRect(100, height, GUILayout.ExpandWidth(true));
|
||||||
|
|
||||||
|
// Background
|
||||||
|
EditorGUI.DrawRect(rect, BackgroundDark);
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
var fillRect = new Rect(rect.x, rect.y, rect.width * Mathf.Clamp01(progress), rect.height);
|
||||||
|
EditorGUI.DrawRect(fillRect, fillColor);
|
||||||
|
|
||||||
|
// Border
|
||||||
|
DrawBorder(rect, TextMuted * 0.5f, 1);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
var labelStyle = new GUIStyle(EditorStyles.label)
|
||||||
|
{
|
||||||
|
alignment = TextAnchor.MiddleCenter,
|
||||||
|
normal = { textColor = TextPrimary }
|
||||||
|
};
|
||||||
|
GUI.Label(rect, $"{label} ({progress * 100:F0}%)", labelStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a border around a rect
|
||||||
|
/// </summary>
|
||||||
|
public static void DrawBorder(Rect rect, Color color, int thickness = 1)
|
||||||
|
{
|
||||||
|
// Top
|
||||||
|
EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, thickness), color);
|
||||||
|
// Bottom
|
||||||
|
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - thickness, rect.width, thickness), color);
|
||||||
|
// Left
|
||||||
|
EditorGUI.DrawRect(new Rect(rect.x, rect.y, thickness, rect.height), color);
|
||||||
|
// Right
|
||||||
|
EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.y, thickness, rect.height), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a contrasting text color for a background
|
||||||
|
/// </summary>
|
||||||
|
public static Color GetContrastColor(Color background)
|
||||||
|
{
|
||||||
|
float luminance = 0.299f * background.r + 0.587f * background.g + 0.114f * background.b;
|
||||||
|
return luminance > 0.5f ? Color.black : Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begin a styled card section
|
||||||
|
/// </summary>
|
||||||
|
public static void BeginCard()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginVertical(CardStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End a styled card section
|
||||||
|
/// </summary>
|
||||||
|
public static void EndCard()
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a card with header and content
|
||||||
|
/// </summary>
|
||||||
|
public static bool DrawFoldoutCard(string title, bool isExpanded, System.Action drawContent)
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginVertical(CardStyle);
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
isExpanded = EditorGUILayout.Foldout(isExpanded, title, true, FoldoutHeader);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
if (isExpanded)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
drawContent?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
return isExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a large icon button (for dashboard)
|
||||||
|
/// </summary>
|
||||||
|
public static bool DrawIconButton(string label, string icon, string description, float width = 150, float height = 100)
|
||||||
|
{
|
||||||
|
var rect = GUILayoutUtility.GetRect(width, height);
|
||||||
|
|
||||||
|
bool isHover = rect.Contains(Event.current.mousePosition);
|
||||||
|
var bgColor = isHover ? BackgroundHighlight : BackgroundMedium;
|
||||||
|
|
||||||
|
EditorGUI.DrawRect(rect, bgColor);
|
||||||
|
DrawBorder(rect, isHover ? AccentCyan : TextMuted * 0.3f, 1);
|
||||||
|
|
||||||
|
// Icon (using Unity's built-in icons or a placeholder)
|
||||||
|
var iconRect = new Rect(rect.x + rect.width / 2 - 16, rect.y + 15, 32, 32);
|
||||||
|
var iconContent = EditorGUIUtility.IconContent(icon);
|
||||||
|
if (iconContent != null && iconContent.image != null)
|
||||||
|
{
|
||||||
|
GUI.DrawTexture(iconRect, iconContent.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
var labelRect = new Rect(rect.x, rect.y + 52, rect.width, 20);
|
||||||
|
var labelStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||||
|
{
|
||||||
|
alignment = TextAnchor.MiddleCenter,
|
||||||
|
normal = { textColor = TextPrimary }
|
||||||
|
};
|
||||||
|
GUI.Label(labelRect, label, labelStyle);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
var descRect = new Rect(rect.x + 5, rect.y + 70, rect.width - 10, 25);
|
||||||
|
var descStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||||
|
{
|
||||||
|
alignment = TextAnchor.UpperCenter,
|
||||||
|
wordWrap = true,
|
||||||
|
normal = { textColor = TextSecondary }
|
||||||
|
};
|
||||||
|
GUI.Label(descRect, description, descStyle);
|
||||||
|
|
||||||
|
return GUI.Button(rect, GUIContent.none, GUIStyle.none);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw a horizontal button group
|
||||||
|
/// </summary>
|
||||||
|
public static int DrawButtonGroup(string[] labels, int selected, float height = 25)
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
|
||||||
|
for (int i = 0; i < labels.Length; i++)
|
||||||
|
{
|
||||||
|
bool isSelected = i == selected;
|
||||||
|
var style = new GUIStyle(GUI.skin.button)
|
||||||
|
{
|
||||||
|
fontStyle = isSelected ? FontStyle.Bold : FontStyle.Normal
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
style.normal.background = GetSolidTexture(PrimaryBlue);
|
||||||
|
style.normal.textColor = Color.white;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
style.normal.background = GetSolidTexture(BackgroundLight);
|
||||||
|
style.normal.textColor = TextSecondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button(labels[i], style, GUILayout.Height(height)))
|
||||||
|
{
|
||||||
|
selected = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Layout Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begin a toolbar row
|
||||||
|
/// </summary>
|
||||||
|
public static void BeginToolbar()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal(ToolbarStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End a toolbar row
|
||||||
|
/// </summary>
|
||||||
|
public static void EndToolbar()
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add flexible space
|
||||||
|
/// </summary>
|
||||||
|
public static void FlexibleSpace()
|
||||||
|
{
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begin a centered layout
|
||||||
|
/// </summary>
|
||||||
|
public static void BeginCentered()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.BeginVertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End a centered layout
|
||||||
|
/// </summary>
|
||||||
|
public static void EndCentered()
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Cleanup
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear cached styles and textures. Call when recompiling.
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearCache()
|
||||||
|
{
|
||||||
|
foreach (var tex in _textureCache.Values)
|
||||||
|
{
|
||||||
|
if (tex != null)
|
||||||
|
Object.DestroyImmediate(tex);
|
||||||
|
}
|
||||||
|
_textureCache.Clear();
|
||||||
|
|
||||||
|
_windowHeader = null;
|
||||||
|
_sectionHeader = null;
|
||||||
|
_cardStyle = null;
|
||||||
|
_cardHeaderStyle = null;
|
||||||
|
_primaryButton = null;
|
||||||
|
_secondaryButton = null;
|
||||||
|
_successButton = null;
|
||||||
|
_dangerButton = null;
|
||||||
|
_statusBadge = null;
|
||||||
|
_toolbarStyle = null;
|
||||||
|
_infoBox = null;
|
||||||
|
_centeredLabel = null;
|
||||||
|
_richLabel = null;
|
||||||
|
_foldoutHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Icons used throughout the PSX Splash editor
|
||||||
|
/// </summary>
|
||||||
|
public static class PSXIcons
|
||||||
|
{
|
||||||
|
// Unity built-in icons that work well for our purposes
|
||||||
|
public const string Scene = "d_SceneAsset Icon";
|
||||||
|
public const string Build = "d_BuildSettings.SelectedIcon";
|
||||||
|
public const string Settings = "d_Settings";
|
||||||
|
public const string Play = "d_PlayButton";
|
||||||
|
public const string Refresh = "d_Refresh";
|
||||||
|
public const string Warning = "d_console.warnicon";
|
||||||
|
public const string Error = "d_console.erroricon";
|
||||||
|
public const string Info = "d_console.infoicon";
|
||||||
|
public const string Success = "d_Progress";
|
||||||
|
public const string Texture = "d_Texture Icon";
|
||||||
|
public const string Mesh = "d_Mesh Icon";
|
||||||
|
public const string Script = "d_cs Script Icon";
|
||||||
|
public const string Folder = "d_Folder Icon";
|
||||||
|
public const string Download = "d_Download-Available";
|
||||||
|
public const string Upload = "d_UpArrow";
|
||||||
|
public const string Link = "d_Linked";
|
||||||
|
public const string Unlink = "d_Unlinked";
|
||||||
|
public const string Eye = "d_scenevis_visible_hover";
|
||||||
|
public const string EyeOff = "d_scenevis_hidden_hover";
|
||||||
|
public const string Add = "d_Toolbar Plus";
|
||||||
|
public const string Remove = "d_Toolbar Minus";
|
||||||
|
public const string Edit = "d_editicon.sml";
|
||||||
|
public const string Search = "d_Search Icon";
|
||||||
|
public const string Console = "d_UnityEditor.ConsoleWindow";
|
||||||
|
public const string Help = "d__Help";
|
||||||
|
public const string GameObject = "d_GameObject Icon";
|
||||||
|
public const string Camera = "d_Camera Icon";
|
||||||
|
public const string Light = "d_Light Icon";
|
||||||
|
public const string Prefab = "d_Prefab Icon";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a GUIContent with icon and tooltip
|
||||||
|
/// </summary>
|
||||||
|
public static GUIContent GetContent(string icon, string tooltip = "")
|
||||||
|
{
|
||||||
|
var content = EditorGUIUtility.IconContent(icon);
|
||||||
|
if (content == null) content = new GUIContent();
|
||||||
|
content.tooltip = tooltip;
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a GUIContent with icon, text and tooltip
|
||||||
|
/// </summary>
|
||||||
|
public static GUIContent GetContent(string icon, string text, string tooltip)
|
||||||
|
{
|
||||||
|
var content = EditorGUIUtility.IconContent(icon);
|
||||||
|
if (content == null) content = new GUIContent();
|
||||||
|
content.text = text;
|
||||||
|
content.tooltip = tooltip;
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/PSXEditorStyles.cs.meta
Normal file
2
Editor/Core/PSXEditorStyles.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8aefa79a412d32c4f8bc8249bb4cd118
|
||||||
178
Editor/Core/SplashBuildPaths.cs
Normal file
178
Editor/Core/SplashBuildPaths.cs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
using System.IO;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages all build-related paths for the SplashEdit pipeline.
|
||||||
|
/// All output goes outside Assets/ to avoid Unity import overhead.
|
||||||
|
/// </summary>
|
||||||
|
public static class SplashBuildPaths
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The build output directory at the Unity project root.
|
||||||
|
/// Contains exported splashpacks, manifest, compiled .ps-exe, ISO, build log.
|
||||||
|
/// </summary>
|
||||||
|
public static string BuildOutputDir =>
|
||||||
|
Path.Combine(ProjectRoot, "PSXBuild");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tools directory at the Unity project root.
|
||||||
|
/// Contains auto-downloaded tools like PCSX-Redux.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToolsDir =>
|
||||||
|
Path.Combine(ProjectRoot, ".tools");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PCSX-Redux install directory inside .tools/.
|
||||||
|
/// </summary>
|
||||||
|
public static string PCSXReduxDir =>
|
||||||
|
Path.Combine(ToolsDir, "pcsx-redux");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Platform-specific PCSX-Redux binary path.
|
||||||
|
/// </summary>
|
||||||
|
public static string PCSXReduxBinary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (Application.platform)
|
||||||
|
{
|
||||||
|
case RuntimePlatform.WindowsEditor:
|
||||||
|
return Path.Combine(PCSXReduxDir, "pcsx-redux.exe");
|
||||||
|
case RuntimePlatform.LinuxEditor:
|
||||||
|
return Path.Combine(ToolsDir, "PCSX-Redux-HEAD-x86_64.AppImage");
|
||||||
|
default:
|
||||||
|
return Path.Combine(PCSXReduxDir, "pcsx-redux");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Unity project root (parent of Assets/).
|
||||||
|
/// </summary>
|
||||||
|
public static string ProjectRoot =>
|
||||||
|
Directory.GetParent(Application.dataPath).FullName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the native psxsplash source.
|
||||||
|
/// First checks SplashSettings override, then looks for common locations.
|
||||||
|
/// </summary>
|
||||||
|
public static string NativeSourceDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// 1. Check the user-configured path from SplashSettings
|
||||||
|
string custom = SplashSettings.NativeProjectPath;
|
||||||
|
if (!string.IsNullOrEmpty(custom) && Directory.Exists(custom))
|
||||||
|
return custom;
|
||||||
|
|
||||||
|
// 2. Look inside the Unity project's Assets/ folder (git clone location)
|
||||||
|
string assetsClone = Path.Combine(UnityEngine.Application.dataPath, "psxsplash");
|
||||||
|
if (Directory.Exists(assetsClone) && File.Exists(Path.Combine(assetsClone, "Makefile")))
|
||||||
|
return assetsClone;
|
||||||
|
|
||||||
|
// 3. Look for Native/ inside the package
|
||||||
|
string packageNative = Path.GetFullPath(
|
||||||
|
Path.Combine("Packages", "net.psxsplash.splashedit", "Native"));
|
||||||
|
if (Directory.Exists(packageNative))
|
||||||
|
return packageNative;
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The compiled .ps-exe output from the native build.
|
||||||
|
/// </summary>
|
||||||
|
public static string CompiledExePath =>
|
||||||
|
Path.Combine(BuildOutputDir, "psxsplash.ps-exe");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scene manifest file path.
|
||||||
|
/// </summary>
|
||||||
|
public static string ManifestPath =>
|
||||||
|
Path.Combine(BuildOutputDir, "manifest.bin");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build log file path.
|
||||||
|
/// </summary>
|
||||||
|
public static string BuildLogPath =>
|
||||||
|
Path.Combine(BuildOutputDir, "build.log");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the splashpack output path for a scene by index.
|
||||||
|
/// Uses a deterministic naming scheme: scene_0.splashpack, scene_1.splashpack, etc.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetSceneSplashpackPath(int sceneIndex, string sceneName)
|
||||||
|
{
|
||||||
|
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.splashpack");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ISO output path for release builds.
|
||||||
|
/// </summary>
|
||||||
|
public static string ISOOutputPath =>
|
||||||
|
Path.Combine(BuildOutputDir, "psxsplash.bin");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CUE sheet path for release builds.
|
||||||
|
/// </summary>
|
||||||
|
public static string CUEOutputPath =>
|
||||||
|
Path.Combine(BuildOutputDir, "psxsplash.cue");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the build output and tools directories exist.
|
||||||
|
/// Also appends entries to the project .gitignore if not present.
|
||||||
|
/// </summary>
|
||||||
|
public static void EnsureDirectories()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(BuildOutputDir);
|
||||||
|
Directory.CreateDirectory(ToolsDir);
|
||||||
|
EnsureGitIgnore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if PCSX-Redux is installed in the tools directory.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPCSXReduxInstalled()
|
||||||
|
{
|
||||||
|
return File.Exists(PCSXReduxBinary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureGitIgnore()
|
||||||
|
{
|
||||||
|
string gitignorePath = Path.Combine(ProjectRoot, ".gitignore");
|
||||||
|
|
||||||
|
string[] entriesToAdd = new[] { "/PSXBuild/", "/.tools/" };
|
||||||
|
|
||||||
|
string existingContent = "";
|
||||||
|
if (File.Exists(gitignorePath))
|
||||||
|
{
|
||||||
|
existingContent = File.ReadAllText(gitignorePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool modified = false;
|
||||||
|
string toAppend = "";
|
||||||
|
|
||||||
|
foreach (string entry in entriesToAdd)
|
||||||
|
{
|
||||||
|
// Check if entry already exists (exact line match)
|
||||||
|
if (!existingContent.Contains(entry))
|
||||||
|
{
|
||||||
|
if (!modified)
|
||||||
|
{
|
||||||
|
toAppend += "\n# SplashEdit build output\n";
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
toAppend += entry + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified)
|
||||||
|
{
|
||||||
|
File.AppendAllText(gitignorePath, toAppend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/SplashBuildPaths.cs.meta
Normal file
2
Editor/Core/SplashBuildPaths.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3988772ca929eb14ea3bee6b643de4d0
|
||||||
1564
Editor/Core/SplashControlPanel.cs
Normal file
1564
Editor/Core/SplashControlPanel.cs
Normal file
File diff suppressed because it is too large
Load Diff
2
Editor/Core/SplashControlPanel.cs.meta
Normal file
2
Editor/Core/SplashControlPanel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5540e6cbefeb70d48a0c1e3843719784
|
||||||
159
Editor/Core/SplashSettings.cs
Normal file
159
Editor/Core/SplashSettings.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the pipeline target for builds.
|
||||||
|
/// </summary>
|
||||||
|
public enum BuildTarget
|
||||||
|
{
|
||||||
|
Emulator, // PCSX-Redux with PCdrv
|
||||||
|
RealHardware, // Send .ps-exe over serial via Unirom
|
||||||
|
ISO // Build a CD image
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the build configuration.
|
||||||
|
/// </summary>
|
||||||
|
public enum BuildMode
|
||||||
|
{
|
||||||
|
Debug,
|
||||||
|
Release
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Centralized EditorPrefs-backed settings for the SplashEdit pipeline.
|
||||||
|
/// All settings are project-scoped using a prefix derived from the project path.
|
||||||
|
/// </summary>
|
||||||
|
public static class SplashSettings
|
||||||
|
{
|
||||||
|
// Prefix all keys with project path hash to support multiple projects
|
||||||
|
internal static string Prefix => "SplashEdit_" + Application.dataPath.GetHashCode().ToString("X8") + "_";
|
||||||
|
|
||||||
|
// --- Build settings ---
|
||||||
|
public static BuildTarget Target
|
||||||
|
{
|
||||||
|
get => (BuildTarget)EditorPrefs.GetInt(Prefix + "Target", (int)BuildTarget.Emulator);
|
||||||
|
set => EditorPrefs.SetInt(Prefix + "Target", (int)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BuildMode Mode
|
||||||
|
{
|
||||||
|
get => (BuildMode)EditorPrefs.GetInt(Prefix + "Mode", (int)BuildMode.Release);
|
||||||
|
set => EditorPrefs.SetInt(Prefix + "Mode", (int)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toolchain paths ---
|
||||||
|
public static string NativeProjectPath
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetString(Prefix + "NativeProjectPath", "");
|
||||||
|
set => EditorPrefs.SetString(Prefix + "NativeProjectPath", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MIPSToolchainPath
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetString(Prefix + "MIPSToolchainPath", "");
|
||||||
|
set => EditorPrefs.SetString(Prefix + "MIPSToolchainPath", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PCSX-Redux ---
|
||||||
|
public static string PCSXReduxPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string custom = EditorPrefs.GetString(Prefix + "PCSXReduxPath", "");
|
||||||
|
if (!string.IsNullOrEmpty(custom))
|
||||||
|
return custom;
|
||||||
|
// Fall back to auto-downloaded location
|
||||||
|
if (SplashBuildPaths.IsPCSXReduxInstalled())
|
||||||
|
return SplashBuildPaths.PCSXReduxBinary;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
set => EditorPrefs.SetString(Prefix + "PCSXReduxPath", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string PCSXReduxPCdrvBase
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetString(Prefix + "PCSXReduxPCdrvBase", SplashBuildPaths.BuildOutputDir);
|
||||||
|
set => EditorPrefs.SetString(Prefix + "PCSXReduxPCdrvBase", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Serial / Real Hardware ---
|
||||||
|
public static string SerialPort
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetString(Prefix + "SerialPort", "COM3");
|
||||||
|
set => EditorPrefs.SetString(Prefix + "SerialPort", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SerialBaudRate
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetInt(Prefix + "SerialBaudRate", 115200);
|
||||||
|
set => EditorPrefs.SetInt(Prefix + "SerialBaudRate", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VRAM Layout ---
|
||||||
|
public static int ResolutionWidth
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetInt(Prefix + "ResWidth", 320);
|
||||||
|
set => EditorPrefs.SetInt(Prefix + "ResWidth", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ResolutionHeight
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetInt(Prefix + "ResHeight", 240);
|
||||||
|
set => EditorPrefs.SetInt(Prefix + "ResHeight", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool DualBuffering
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetBool(Prefix + "DualBuffering", true);
|
||||||
|
set => EditorPrefs.SetBool(Prefix + "DualBuffering", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool VerticalLayout
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetBool(Prefix + "VerticalLayout", true);
|
||||||
|
set => EditorPrefs.SetBool(Prefix + "VerticalLayout", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Export settings ---
|
||||||
|
public static float DefaultGTEScaling
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetFloat(Prefix + "GTEScaling", 100f);
|
||||||
|
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool AutoValidateOnExport
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetBool(Prefix + "AutoValidate", true);
|
||||||
|
set => EditorPrefs.SetBool(Prefix + "AutoValidate", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Play Mode Intercept ---
|
||||||
|
public static bool InterceptPlayMode
|
||||||
|
{
|
||||||
|
get => EditorPrefs.GetBool(Prefix + "InterceptPlayMode", false);
|
||||||
|
set => EditorPrefs.SetBool(Prefix + "InterceptPlayMode", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets all settings to defaults by deleting all prefixed keys.
|
||||||
|
/// </summary>
|
||||||
|
public static void ResetAll()
|
||||||
|
{
|
||||||
|
string[] keys = new[]
|
||||||
|
{
|
||||||
|
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
|
||||||
|
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
|
||||||
|
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
|
||||||
|
"GTEScaling", "AutoValidate", "InterceptPlayMode"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (string key in keys)
|
||||||
|
{
|
||||||
|
EditorPrefs.DeleteKey(Prefix + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/SplashSettings.cs.meta
Normal file
2
Editor/Core/SplashSettings.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4765dbe728569d84699a22347e7c14ff
|
||||||
549
Editor/Core/UniromUploader.cs
Normal file
549
Editor/Core/UniromUploader.cs
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a .ps-exe to a PS1 running Unirom 8 via serial.
|
||||||
|
/// Implements the NOTPSXSerial / Unirom protocol:
|
||||||
|
/// Challenge/Response handshake → header → metadata → chunked data with checksums.
|
||||||
|
/// Reference: https://github.com/JonathanDotCel/NOTPSXSerial
|
||||||
|
/// </summary>
|
||||||
|
public static class UniromUploader
|
||||||
|
{
|
||||||
|
// Protocol constants
|
||||||
|
private const string CHALLENGE_SEND_EXE = "SEXE";
|
||||||
|
private const string RESPONSE_OK = "OKAY";
|
||||||
|
private const int CHUNK_SIZE = 2048;
|
||||||
|
private const int HEADER_SIZE = 0x800; // 2048
|
||||||
|
private const int SERIAL_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
// Protocol version — negotiated during handshake
|
||||||
|
private static int _protocolVersion = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a .ps-exe file to the PS1 via serial.
|
||||||
|
/// The PS1 must be at the Unirom shell prompt.
|
||||||
|
/// </summary>
|
||||||
|
public static bool UploadExe(string portName, int baudRate, string exePath, Action<string> log)
|
||||||
|
{
|
||||||
|
var port = DoUpload(portName, baudRate, exePath, log, installDebugHooks: false);
|
||||||
|
if (port == null) return false;
|
||||||
|
try { port.Close(); } catch { }
|
||||||
|
port.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a .ps-exe with Unirom debug hooks installed, using SBIN+JUMP
|
||||||
|
/// instead of SEXE to avoid BIOS Exec() clobbering the debug handler.
|
||||||
|
///
|
||||||
|
/// Flow: DEBG (install kernel-resident debug hooks) → SBIN (raw binary to address)
|
||||||
|
/// → JUMP (start execution at entry point). This bypasses BIOS Exec() entirely,
|
||||||
|
/// so the exception vector table patched by DEBG survives into the running program.
|
||||||
|
///
|
||||||
|
/// Returns the open SerialPort for the caller to use for PCDrv monitoring.
|
||||||
|
/// The caller takes ownership of the returned port.
|
||||||
|
/// </summary>
|
||||||
|
public static SerialPort UploadExeForPCdrv(string portName, int baudRate, string exePath, Action<string> log)
|
||||||
|
{
|
||||||
|
return DoUploadSBIN(portName, baudRate, exePath, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Core SEXE upload implementation. Opens port, optionally sends DEBG, does SEXE upload.
|
||||||
|
/// Used by UploadExe() for simple uploads without PCDrv.
|
||||||
|
/// Returns the open SerialPort (caller must close/dispose when done).
|
||||||
|
/// Returns null on failure.
|
||||||
|
/// </summary>
|
||||||
|
private static SerialPort DoUpload(string portName, int baudRate, string exePath, Action<string> log, bool installDebugHooks)
|
||||||
|
{
|
||||||
|
if (!File.Exists(exePath))
|
||||||
|
{
|
||||||
|
log?.Invoke($"File not found: {exePath}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] exeData = File.ReadAllBytes(exePath);
|
||||||
|
log?.Invoke($"Uploading {Path.GetFileName(exePath)} ({exeData.Length} bytes)");
|
||||||
|
|
||||||
|
// Pad to 2048-byte sector boundary (required by Unirom)
|
||||||
|
int mod = exeData.Length % CHUNK_SIZE;
|
||||||
|
if (mod != 0)
|
||||||
|
{
|
||||||
|
int paddingRequired = CHUNK_SIZE - mod;
|
||||||
|
byte[] padded = new byte[exeData.Length + paddingRequired];
|
||||||
|
Buffer.BlockCopy(exeData, 0, padded, 0, exeData.Length);
|
||||||
|
exeData = padded;
|
||||||
|
log?.Invoke($"Padded to {exeData.Length} bytes (2048-byte boundary)");
|
||||||
|
}
|
||||||
|
|
||||||
|
_protocolVersion = 1;
|
||||||
|
SerialPort port = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
port = new SerialPort(portName, baudRate)
|
||||||
|
{
|
||||||
|
ReadTimeout = SERIAL_TIMEOUT_MS,
|
||||||
|
WriteTimeout = SERIAL_TIMEOUT_MS,
|
||||||
|
StopBits = StopBits.Two,
|
||||||
|
Parity = Parity.None,
|
||||||
|
DataBits = 8,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
DtrEnable = true,
|
||||||
|
RtsEnable = true
|
||||||
|
};
|
||||||
|
port.Open();
|
||||||
|
|
||||||
|
// Drain any leftover bytes in the buffer
|
||||||
|
while (port.BytesToRead > 0)
|
||||||
|
port.ReadByte();
|
||||||
|
|
||||||
|
// ── Step 0 (PCDrv only): Install debug hooks while Unirom is still in command mode ──
|
||||||
|
if (installDebugHooks)
|
||||||
|
{
|
||||||
|
log?.Invoke("Installing debug hooks (DEBG)...");
|
||||||
|
if (!ChallengeResponse(port, "DEBG", "OKAY", log))
|
||||||
|
{
|
||||||
|
log?.Invoke("WARNING: DEBG failed. Is Unirom at the shell? PCDrv may not work.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log?.Invoke("Debug hooks installed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(100);
|
||||||
|
while (port.BytesToRead > 0)
|
||||||
|
port.ReadByte();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: Challenge/Response handshake ──
|
||||||
|
log?.Invoke("Sending SEXE challenge...");
|
||||||
|
if (!ChallengeResponse(port, CHALLENGE_SEND_EXE, RESPONSE_OK, log))
|
||||||
|
{
|
||||||
|
log?.Invoke("No response from Unirom. Is the PS1 at the Unirom shell?");
|
||||||
|
port.Close(); port.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
log?.Invoke($"Unirom responded (protocol V{_protocolVersion}). Starting transfer...");
|
||||||
|
|
||||||
|
// ── Step 2: Calculate checksum (skip first 0x800 header sector) ──
|
||||||
|
uint checksum = CalculateChecksum(exeData, skipFirstSector: true);
|
||||||
|
|
||||||
|
// ── Step 3: Send the 2048-byte header sector ──
|
||||||
|
port.Write(exeData, 0, HEADER_SIZE);
|
||||||
|
|
||||||
|
// ── Step 4: Send metadata ──
|
||||||
|
port.Write(exeData, 0x10, 4); // Jump/PC address
|
||||||
|
port.Write(exeData, 0x18, 4); // Base/write address
|
||||||
|
port.Write(BitConverter.GetBytes(exeData.Length - HEADER_SIZE), 0, 4); // Data length
|
||||||
|
port.Write(BitConverter.GetBytes(checksum), 0, 4); // Checksum
|
||||||
|
|
||||||
|
// ── Step 5: Send data chunks (skip first sector) ──
|
||||||
|
if (!WriteChunked(port, exeData, skipFirstSector: true, log))
|
||||||
|
{
|
||||||
|
log?.Invoke("Data transfer failed.");
|
||||||
|
port.Close(); port.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke("Upload complete. Exe executing on PS1.");
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log?.Invoke($"Upload failed: {ex.Message}");
|
||||||
|
if (port != null && port.IsOpen)
|
||||||
|
{
|
||||||
|
try { port.Close(); } catch { }
|
||||||
|
}
|
||||||
|
port?.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a .ps-exe using DEBG + SBIN + JUMP to preserve debug hooks.
|
||||||
|
///
|
||||||
|
/// Unlike SEXE which calls BIOS Exec() (reinitializing the exception vector table
|
||||||
|
/// and destroying DEBG's kernel-resident debug handler), SBIN writes raw bytes
|
||||||
|
/// directly to the target address and JUMP starts execution without touching
|
||||||
|
/// the BIOS. This preserves the break-instruction handler that PCDrv depends on.
|
||||||
|
///
|
||||||
|
/// Protocol:
|
||||||
|
/// 1. DEBG → OKAY: Install kernel-resident SIO debug stub
|
||||||
|
/// 2. SBIN → OKAY: addr(4 LE) + len(4 LE) + checksum(4 LE) + raw program data
|
||||||
|
/// 3. JUMP → OKAY: addr(4 LE) — jump to entry point
|
||||||
|
/// </summary>
|
||||||
|
private static SerialPort DoUploadSBIN(string portName, int baudRate, string exePath, Action<string> log)
|
||||||
|
{
|
||||||
|
if (!File.Exists(exePath))
|
||||||
|
{
|
||||||
|
log?.Invoke($"File not found: {exePath}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] exeData = File.ReadAllBytes(exePath);
|
||||||
|
log?.Invoke($"Uploading {Path.GetFileName(exePath)} ({exeData.Length} bytes) via SBIN+JUMP");
|
||||||
|
|
||||||
|
// Validate this is a PS-X EXE
|
||||||
|
if (exeData.Length < HEADER_SIZE + 4)
|
||||||
|
{
|
||||||
|
log?.Invoke("File too small to be a valid PS-X EXE.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
string magic = Encoding.ASCII.GetString(exeData, 0, 8);
|
||||||
|
if (!magic.StartsWith("PS-X EXE"))
|
||||||
|
{
|
||||||
|
log?.Invoke($"Not a PS-X EXE (magic: '{magic}')");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse header
|
||||||
|
uint entryPoint = BitConverter.ToUInt32(exeData, 0x10); // PC / jump address
|
||||||
|
uint destAddr = BitConverter.ToUInt32(exeData, 0x18); // Copy destination
|
||||||
|
uint textSize = BitConverter.ToUInt32(exeData, 0x1C); // Text section size
|
||||||
|
|
||||||
|
log?.Invoke($"PS-X EXE: entry=0x{entryPoint:X8}, dest=0x{destAddr:X8}, textSz=0x{textSize:X}");
|
||||||
|
|
||||||
|
// Extract program data (everything after the 2048-byte header)
|
||||||
|
int progDataLen = exeData.Length - HEADER_SIZE;
|
||||||
|
byte[] progData = new byte[progDataLen];
|
||||||
|
Buffer.BlockCopy(exeData, HEADER_SIZE, progData, 0, progDataLen);
|
||||||
|
|
||||||
|
// Pad program data to 2048-byte boundary (required by Unirom chunked transfer)
|
||||||
|
int mod = progData.Length % CHUNK_SIZE;
|
||||||
|
if (mod != 0)
|
||||||
|
{
|
||||||
|
int paddingRequired = CHUNK_SIZE - mod;
|
||||||
|
byte[] padded = new byte[progData.Length + paddingRequired];
|
||||||
|
Buffer.BlockCopy(progData, 0, padded, 0, progData.Length);
|
||||||
|
progData = padded;
|
||||||
|
log?.Invoke($"Program data padded to {progData.Length} bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
_protocolVersion = 1;
|
||||||
|
SerialPort port = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
port = new SerialPort(portName, baudRate)
|
||||||
|
{
|
||||||
|
ReadTimeout = SERIAL_TIMEOUT_MS,
|
||||||
|
WriteTimeout = SERIAL_TIMEOUT_MS,
|
||||||
|
StopBits = StopBits.Two,
|
||||||
|
Parity = Parity.None,
|
||||||
|
DataBits = 8,
|
||||||
|
Handshake = Handshake.None,
|
||||||
|
DtrEnable = true,
|
||||||
|
RtsEnable = true
|
||||||
|
};
|
||||||
|
port.Open();
|
||||||
|
|
||||||
|
// Drain any leftover bytes
|
||||||
|
while (port.BytesToRead > 0)
|
||||||
|
port.ReadByte();
|
||||||
|
|
||||||
|
// ── Step 1: DEBG — Install kernel-resident debug hooks ──
|
||||||
|
log?.Invoke("Installing debug hooks (DEBG)...");
|
||||||
|
if (!ChallengeResponse(port, "DEBG", "OKAY", log))
|
||||||
|
{
|
||||||
|
log?.Invoke("DEBG failed. Is Unirom at the shell?");
|
||||||
|
port.Close(); port.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
log?.Invoke("Debug hooks installed.");
|
||||||
|
|
||||||
|
// Drain + settle — Unirom may send extra bytes after DEBG
|
||||||
|
Thread.Sleep(100);
|
||||||
|
while (port.BytesToRead > 0)
|
||||||
|
port.ReadByte();
|
||||||
|
|
||||||
|
// ── Step 2: SBIN — Upload raw program data to target address ──
|
||||||
|
log?.Invoke($"Sending SBIN to 0x{destAddr:X8} ({progData.Length} bytes)...");
|
||||||
|
if (!ChallengeResponse(port, "SBIN", "OKAY", log))
|
||||||
|
{
|
||||||
|
log?.Invoke("SBIN failed. Unirom may not support this command.");
|
||||||
|
port.Close(); port.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SBIN metadata: address(4) + length(4) + checksum(4)
|
||||||
|
uint checksum = CalculateChecksum(progData, skipFirstSector: false);
|
||||||
|
port.Write(BitConverter.GetBytes(destAddr), 0, 4);
|
||||||
|
port.Write(BitConverter.GetBytes(progData.Length), 0, 4);
|
||||||
|
port.Write(BitConverter.GetBytes(checksum), 0, 4);
|
||||||
|
|
||||||
|
log?.Invoke($"SBIN metadata sent (checksum=0x{checksum:X8}). Sending data...");
|
||||||
|
|
||||||
|
// Send program data chunks
|
||||||
|
if (!WriteChunked(port, progData, skipFirstSector: false, log))
|
||||||
|
{
|
||||||
|
log?.Invoke("SBIN data transfer failed.");
|
||||||
|
port.Close(); port.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
log?.Invoke("SBIN upload complete.");
|
||||||
|
|
||||||
|
// Drain any residual
|
||||||
|
Thread.Sleep(100);
|
||||||
|
while (port.BytesToRead > 0)
|
||||||
|
port.ReadByte();
|
||||||
|
|
||||||
|
// ── Step 3: JUMP — Start execution at entry point ──
|
||||||
|
log?.Invoke($"Sending JUMP to 0x{entryPoint:X8}...");
|
||||||
|
if (!ChallengeResponse(port, "JUMP", "OKAY", log))
|
||||||
|
{
|
||||||
|
log?.Invoke("JUMP failed.");
|
||||||
|
port.Close(); port.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// JUMP payload: just the address (4 bytes LE)
|
||||||
|
port.Write(BitConverter.GetBytes(entryPoint), 0, 4);
|
||||||
|
|
||||||
|
log?.Invoke("JUMP sent. Exe now running (debug hooks preserved).");
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log?.Invoke($"Upload failed: {ex.Message}");
|
||||||
|
if (port != null && port.IsOpen)
|
||||||
|
{
|
||||||
|
try { port.Close(); } catch { }
|
||||||
|
}
|
||||||
|
port?.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Challenge / Response with protocol negotiation
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static bool ChallengeResponse(SerialPort port, string challenge, string expectedResponse, Action<string> log)
|
||||||
|
{
|
||||||
|
// Send the challenge
|
||||||
|
byte[] challengeBytes = Encoding.ASCII.GetBytes(challenge);
|
||||||
|
port.Write(challengeBytes, 0, challengeBytes.Length);
|
||||||
|
Thread.Sleep(50);
|
||||||
|
|
||||||
|
// Wait for the response with protocol negotiation
|
||||||
|
return WaitResponse(port, expectedResponse, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WaitResponse(SerialPort port, string expected, Action<string> log, int timeoutMs = 10000)
|
||||||
|
{
|
||||||
|
string buffer = "";
|
||||||
|
DateTime deadline = DateTime.Now.AddMilliseconds(timeoutMs);
|
||||||
|
|
||||||
|
while (DateTime.Now < deadline)
|
||||||
|
{
|
||||||
|
if (port.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
buffer += (char)port.ReadByte();
|
||||||
|
|
||||||
|
// Keep buffer at 4 chars max (rolling window)
|
||||||
|
if (buffer.Length > 4)
|
||||||
|
buffer = buffer.Substring(buffer.Length - 4);
|
||||||
|
|
||||||
|
// Protocol V3 upgrade (DJB2 checksums)
|
||||||
|
// Always respond — Unirom re-offers V2/V3 for each command,
|
||||||
|
// and our protocolVersion may already be >1 from a prior DEBG exchange.
|
||||||
|
if (buffer == "OKV3")
|
||||||
|
{
|
||||||
|
log?.Invoke("Upgraded to protocol V3");
|
||||||
|
byte[] upv3 = Encoding.ASCII.GetBytes("UPV3");
|
||||||
|
port.Write(upv3, 0, upv3.Length);
|
||||||
|
_protocolVersion = 3;
|
||||||
|
buffer = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol V2 upgrade (per-chunk checksums)
|
||||||
|
if (buffer == "OKV2")
|
||||||
|
{
|
||||||
|
log?.Invoke("Upgraded to protocol V2");
|
||||||
|
byte[] upv2 = Encoding.ASCII.GetBytes("UPV2");
|
||||||
|
port.Write(upv2, 0, upv2.Length);
|
||||||
|
if (_protocolVersion < 2) _protocolVersion = 2;
|
||||||
|
buffer = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsupported in debug mode
|
||||||
|
if (buffer == "UNSP")
|
||||||
|
{
|
||||||
|
log?.Invoke("Command not supported while Unirom is in debug mode.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Got the expected response
|
||||||
|
if (buffer == expected)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Chunked data transfer with per-chunk checksum verification
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static bool WriteChunked(SerialPort port, byte[] data, bool skipFirstSector, Action<string> log)
|
||||||
|
{
|
||||||
|
int start = skipFirstSector ? CHUNK_SIZE : 0;
|
||||||
|
int totalDataBytes = data.Length - start;
|
||||||
|
int numChunks = (totalDataBytes + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||||
|
int chunkIndex = 0;
|
||||||
|
|
||||||
|
for (int offset = start; offset < data.Length; )
|
||||||
|
{
|
||||||
|
// Determine chunk size (last chunk may be smaller)
|
||||||
|
int thisChunk = Math.Min(CHUNK_SIZE, data.Length - offset);
|
||||||
|
|
||||||
|
// Calculate per-chunk checksum (simple byte sum for V2, also works for V1)
|
||||||
|
ulong chunkChecksum = 0;
|
||||||
|
for (int j = 0; j < thisChunk; j++)
|
||||||
|
chunkChecksum += data[offset + j];
|
||||||
|
|
||||||
|
// Send the chunk
|
||||||
|
port.Write(data, offset, thisChunk);
|
||||||
|
|
||||||
|
// Wait for bytes to drain
|
||||||
|
while (port.BytesToWrite > 0)
|
||||||
|
Thread.Sleep(0);
|
||||||
|
|
||||||
|
chunkIndex++;
|
||||||
|
|
||||||
|
// Progress report every 10 chunks or on last chunk
|
||||||
|
if (chunkIndex % 10 == 0 || offset + thisChunk >= data.Length)
|
||||||
|
{
|
||||||
|
int sent = offset + thisChunk - start;
|
||||||
|
int pct = totalDataBytes > 0 ? sent * 100 / totalDataBytes : 100;
|
||||||
|
log?.Invoke($"Upload: {pct}% ({sent}/{totalDataBytes})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol V2/V3: per-chunk checksum verification
|
||||||
|
if (_protocolVersion >= 2)
|
||||||
|
{
|
||||||
|
if (!HandleChunkAck(port, chunkChecksum, data, offset, thisChunk, log, out bool retry))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (retry)
|
||||||
|
continue; // Don't advance offset — resend this chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += thisChunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the per-chunk CHEK/MORE/ERR! exchange for protocol V2+.
|
||||||
|
/// </summary>
|
||||||
|
private static bool HandleChunkAck(SerialPort port, ulong chunkChecksum, byte[] data, int offset, int chunkSize, Action<string> log, out bool retry)
|
||||||
|
{
|
||||||
|
retry = false;
|
||||||
|
|
||||||
|
// Wait for "CHEK" request from Unirom
|
||||||
|
string cmdBuffer = "";
|
||||||
|
DateTime deadline = DateTime.Now.AddMilliseconds(SERIAL_TIMEOUT_MS);
|
||||||
|
|
||||||
|
while (DateTime.Now < deadline)
|
||||||
|
{
|
||||||
|
if (port.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
cmdBuffer += (char)port.ReadByte();
|
||||||
|
if (cmdBuffer.Length > 4)
|
||||||
|
cmdBuffer = cmdBuffer.Substring(cmdBuffer.Length - 4);
|
||||||
|
|
||||||
|
if (cmdBuffer == "CHEK")
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmdBuffer != "CHEK")
|
||||||
|
{
|
||||||
|
log?.Invoke("Timeout waiting for CHEK from Unirom");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the chunk checksum (4 bytes, little-endian)
|
||||||
|
port.Write(BitConverter.GetBytes((uint)chunkChecksum), 0, 4);
|
||||||
|
Thread.Sleep(1);
|
||||||
|
|
||||||
|
// Wait for MORE (ok) or ERR! (resend)
|
||||||
|
cmdBuffer = "";
|
||||||
|
deadline = DateTime.Now.AddMilliseconds(SERIAL_TIMEOUT_MS);
|
||||||
|
|
||||||
|
while (DateTime.Now < deadline)
|
||||||
|
{
|
||||||
|
if (port.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
cmdBuffer += (char)port.ReadByte();
|
||||||
|
if (cmdBuffer.Length > 4)
|
||||||
|
cmdBuffer = cmdBuffer.Substring(cmdBuffer.Length - 4);
|
||||||
|
|
||||||
|
if (cmdBuffer == "MORE")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (cmdBuffer == "ERR!")
|
||||||
|
{
|
||||||
|
log?.Invoke("Checksum error — retrying chunk...");
|
||||||
|
retry = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke("Timeout waiting for MORE/ERR! from Unirom");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Checksum calculation
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static uint CalculateChecksum(byte[] data, bool skipFirstSector)
|
||||||
|
{
|
||||||
|
int start = skipFirstSector ? HEADER_SIZE : 0;
|
||||||
|
|
||||||
|
if (_protocolVersion == 3)
|
||||||
|
{
|
||||||
|
// DJB2 hash
|
||||||
|
uint hash = 5381;
|
||||||
|
for (int i = start; i < data.Length; i++)
|
||||||
|
hash = ((hash << 5) + hash) ^ data[i];
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Simple byte sum
|
||||||
|
uint sum = 0;
|
||||||
|
for (int i = start; i < data.Length; i++)
|
||||||
|
sum += data[i];
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Core/UniromUploader.cs.meta
Normal file
2
Editor/Core/UniromUploader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e39963a5097ad6a48952a0a9d04d1563
|
||||||
39
Editor/DependencyCheckInitializer.cs
Normal file
39
Editor/DependencyCheckInitializer.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Automatically opens the SplashEdit Control Panel on the first editor
|
||||||
|
/// session if the MIPS toolchain has not been installed yet.
|
||||||
|
/// </summary>
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public static class DependencyCheckInitializer
|
||||||
|
{
|
||||||
|
private const string SessionKey = "SplashEditOpenedThisSession";
|
||||||
|
|
||||||
|
static DependencyCheckInitializer()
|
||||||
|
{
|
||||||
|
EditorApplication.update += OpenControlPanelOnStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenControlPanelOnStart()
|
||||||
|
{
|
||||||
|
EditorApplication.update -= OpenControlPanelOnStart;
|
||||||
|
|
||||||
|
if (SessionState.GetBool(SessionKey, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SessionState.SetBool(SessionKey, true);
|
||||||
|
|
||||||
|
// Only auto-open the Control Panel when the toolchain is missing
|
||||||
|
bool toolchainReady = ToolchainChecker.IsToolAvailable("mips") ||
|
||||||
|
ToolchainChecker.IsToolAvailable("mipsel-none-elf-gcc") ||
|
||||||
|
ToolchainChecker.IsToolAvailable("mipsel-linux-gnu-gcc");
|
||||||
|
|
||||||
|
if (!toolchainReady)
|
||||||
|
{
|
||||||
|
SplashControlPanel.ShowWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/DependencyCheckInitializer.cs.meta
Normal file
2
Editor/DependencyCheckInitializer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c7043b9e1acbfbe40b9bd9be80e764e5
|
||||||
8
Editor/Inspectors.meta
Normal file
8
Editor/Inspectors.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0279126b700b37d4485c1f4f1ae44e54
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
136
Editor/Inspectors/PSXComponentEditors.cs
Normal file
136
Editor/Inspectors/PSXComponentEditors.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXInteractable component.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXInteractable))]
|
||||||
|
public class PSXInteractableEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private bool _interactionFoldout = true;
|
||||||
|
private bool _advancedFoldout = false;
|
||||||
|
|
||||||
|
private SerializedProperty _interactionRadius;
|
||||||
|
private SerializedProperty _interactButton;
|
||||||
|
private SerializedProperty _isRepeatable;
|
||||||
|
private SerializedProperty _cooldownFrames;
|
||||||
|
private SerializedProperty _showPrompt;
|
||||||
|
private SerializedProperty _requireLineOfSight;
|
||||||
|
private SerializedProperty _interactionOffset;
|
||||||
|
|
||||||
|
private static readonly string[] ButtonNames =
|
||||||
|
{
|
||||||
|
"Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left",
|
||||||
|
"L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_interactionRadius = serializedObject.FindProperty("interactionRadius");
|
||||||
|
_interactButton = serializedObject.FindProperty("interactButton");
|
||||||
|
_isRepeatable = serializedObject.FindProperty("isRepeatable");
|
||||||
|
_cooldownFrames = serializedObject.FindProperty("cooldownFrames");
|
||||||
|
_showPrompt = serializedObject.FindProperty("showPrompt");
|
||||||
|
_requireLineOfSight = serializedObject.FindProperty("requireLineOfSight");
|
||||||
|
_interactionOffset = serializedObject.FindProperty("interactionOffset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
DrawHeader();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
_interactionFoldout = DrawFoldoutSection("Interaction Settings", _interactionFoldout, () =>
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(_interactionRadius);
|
||||||
|
|
||||||
|
// Button selector with visual
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.PrefixLabel("Interact Button");
|
||||||
|
_interactButton.intValue = EditorGUILayout.Popup(_interactButton.intValue, ButtonNames);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(_isRepeatable);
|
||||||
|
|
||||||
|
if (_isRepeatable.boolValue)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
EditorGUILayout.PropertyField(_cooldownFrames, new GUIContent("Cooldown (frames)"));
|
||||||
|
|
||||||
|
// Show cooldown in seconds
|
||||||
|
float seconds = _cooldownFrames.intValue / 60f;
|
||||||
|
EditorGUILayout.LabelField($"≈ {seconds:F2} seconds at 60fps", EditorStyles.miniLabel);
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(_showPrompt);
|
||||||
|
});
|
||||||
|
|
||||||
|
_advancedFoldout = DrawFoldoutSection("Advanced", _advancedFoldout, () =>
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(_requireLineOfSight);
|
||||||
|
EditorGUILayout.PropertyField(_interactionOffset);
|
||||||
|
});
|
||||||
|
|
||||||
|
DrawLuaEventsInfo(new[] { "onInteract" });
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/Inspectors/PSXComponentEditors.cs.meta
Normal file
2
Editor/Inspectors/PSXComponentEditors.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7bd9caaf5a0cb90409cf0acdf17d8d89
|
||||||
43
Editor/LuaFileAssetEditor.cs
Normal file
43
Editor/LuaFileAssetEditor.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for <see cref="LuaFile"/> assets that displays the
|
||||||
|
/// embedded Lua source code in a read-only text area with an option to
|
||||||
|
/// open the source file in an external editor.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(LuaFile))]
|
||||||
|
public class LuaScriptAssetEditor : Editor
|
||||||
|
{
|
||||||
|
private Vector2 _scrollPosition;
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
LuaFile luaScriptAsset = (LuaFile)target;
|
||||||
|
|
||||||
|
// Open in external editor button
|
||||||
|
string assetPath = AssetDatabase.GetAssetPath(target);
|
||||||
|
if (!string.IsNullOrEmpty(assetPath))
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Open in External Editor"))
|
||||||
|
{
|
||||||
|
// Opens the .lua source file in the OS-configured editor
|
||||||
|
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(assetPath, 1);
|
||||||
|
}
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read-only source view
|
||||||
|
EditorGUILayout.LabelField("Lua Source", EditorStyles.boldLabel);
|
||||||
|
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition,
|
||||||
|
GUILayout.MaxHeight(400));
|
||||||
|
EditorGUI.BeginDisabledGroup(true);
|
||||||
|
EditorGUILayout.TextArea(luaScriptAsset.LuaScript, GUILayout.ExpandHeight(true));
|
||||||
|
EditorGUI.EndDisabledGroup();
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/LuaFileAssetEditor.cs.meta
Normal file
2
Editor/LuaFileAssetEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 66e212c64ebd0a34f9c23febe3e8545d
|
||||||
25
Editor/LuaImporter.cs
Normal file
25
Editor/LuaImporter.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.AssetImporters;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
[ScriptedImporter(2, "lua")]
|
||||||
|
class LuaImporter : ScriptedImporter
|
||||||
|
{
|
||||||
|
public override void OnImportAsset(AssetImportContext ctx)
|
||||||
|
{
|
||||||
|
var asset = ScriptableObject.CreateInstance<LuaFile>();
|
||||||
|
var luaCode = File.ReadAllText(ctx.assetPath);
|
||||||
|
asset.Init(luaCode);
|
||||||
|
asset.name = Path.GetFileName(ctx.assetPath);
|
||||||
|
var text = new TextAsset(asset.LuaScript);
|
||||||
|
|
||||||
|
ctx.AddObjectToAsset("Text", text);
|
||||||
|
ctx.AddObjectToAsset("Script", asset);
|
||||||
|
ctx.SetMainObject(asset); // LuaFile is the main object, not TextAsset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/LuaImporter.cs.meta
Normal file
2
Editor/LuaImporter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74e983e6cf3376944af7b469023d6e4d
|
||||||
80
Editor/PSXMenuItems.cs
Normal file
80
Editor/PSXMenuItems.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal menu items — everything goes through the unified Control Panel.
|
||||||
|
/// Only keeps: Control Panel shortcut + GameObject creation helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static class PSXMenuItems
|
||||||
|
{
|
||||||
|
private const string MENU_ROOT = "PlayStation 1/";
|
||||||
|
|
||||||
|
// ───── Main Entry Point ─────
|
||||||
|
|
||||||
|
[MenuItem(MENU_ROOT + "SplashEdit Control Panel %#p", false, 0)]
|
||||||
|
public static void OpenControlPanel()
|
||||||
|
{
|
||||||
|
SplashControlPanel.ShowWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───── GameObject Menu ─────
|
||||||
|
|
||||||
|
[MenuItem("GameObject/PlayStation 1/Scene Exporter", false, 10)]
|
||||||
|
public static void CreateSceneExporter(MenuCommand menuCommand)
|
||||||
|
{
|
||||||
|
var existing = Object.FindObjectOfType<PSXSceneExporter>();
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog(
|
||||||
|
"Scene Exporter Exists",
|
||||||
|
"A PSXSceneExporter already exists in this scene.\n\n" +
|
||||||
|
"Only one exporter is needed per scene.",
|
||||||
|
"OK");
|
||||||
|
Selection.activeGameObject = existing.gameObject;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var go = new GameObject("PSXSceneExporter");
|
||||||
|
go.AddComponent<PSXSceneExporter>();
|
||||||
|
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
|
||||||
|
Undo.RegisterCreatedObjectUndo(go, "Create PSX Scene Exporter");
|
||||||
|
Selection.activeGameObject = go;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("GameObject/PlayStation 1/Exportable Object", false, 12)]
|
||||||
|
public static void CreateExportableObject(MenuCommand menuCommand)
|
||||||
|
{
|
||||||
|
var go = new GameObject("PSXObject");
|
||||||
|
go.AddComponent<PSXObjectExporter>();
|
||||||
|
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
|
||||||
|
Undo.RegisterCreatedObjectUndo(go, "Create PSX Object");
|
||||||
|
Selection.activeGameObject = go;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───── Context Menu ─────
|
||||||
|
|
||||||
|
[MenuItem("CONTEXT/MeshFilter/Add PSX Object Exporter")]
|
||||||
|
public static void AddPSXObjectExporterFromMesh(MenuCommand command)
|
||||||
|
{
|
||||||
|
var meshFilter = command.context as MeshFilter;
|
||||||
|
if (meshFilter != null && meshFilter.GetComponent<PSXObjectExporter>() == null)
|
||||||
|
{
|
||||||
|
Undo.AddComponent<PSXObjectExporter>(meshFilter.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("CONTEXT/MeshRenderer/Add PSX Object Exporter")]
|
||||||
|
public static void AddPSXObjectExporterFromRenderer(MenuCommand command)
|
||||||
|
{
|
||||||
|
var renderer = command.context as MeshRenderer;
|
||||||
|
if (renderer != null && renderer.GetComponent<PSXObjectExporter>() == null)
|
||||||
|
{
|
||||||
|
Undo.AddComponent<PSXObjectExporter>(renderer.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/PSXMenuItems.cs.meta
Normal file
2
Editor/PSXMenuItems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 174ee99c9e9aafd4ea9002fc3548f53d
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using SplashEdit.RuntimeCode;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace SplashEdit.EditorCode
|
|
||||||
{
|
|
||||||
[CustomEditor(typeof(PSXNavMesh))]
|
|
||||||
public class PSXNavMeshEditor : Editor
|
|
||||||
{
|
|
||||||
public override void OnInspectorGUI()
|
|
||||||
{
|
|
||||||
DrawDefaultInspector();
|
|
||||||
|
|
||||||
PSXNavMesh comp = (PSXNavMesh)target;
|
|
||||||
if (GUILayout.Button("Create preview"))
|
|
||||||
{
|
|
||||||
PSXSceneExporter exporter = FindObjectsByType<PSXSceneExporter>(FindObjectsSortMode.None).FirstOrDefault();
|
|
||||||
if(exporter != null)
|
|
||||||
{
|
|
||||||
comp.CreateNavmesh(exporter.GTEScaling);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogError("No PSXSceneExporter found in the scene. We can't pull the GTE scaling from the exporter.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 9d3bd83aac4c3ce9ab1698a6a2bc735d
|
|
||||||
401
Editor/PSXNavRegionEditor.cs
Normal file
401
Editor/PSXNavRegionEditor.cs
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Editor window for PS1 navigation mesh generation.
|
||||||
|
/// Uses DotRecast (C# Recast) to voxelize scene geometry and build
|
||||||
|
/// convex navigation regions for the PS1 runtime.
|
||||||
|
/// All nav settings live on the PSXPlayer component so the editor
|
||||||
|
/// preview and the scene export always use the same values.
|
||||||
|
/// </summary>
|
||||||
|
public class PSXNavRegionEditor : EditorWindow
|
||||||
|
{
|
||||||
|
private PSXNavRegionBuilder _builder;
|
||||||
|
private bool _previewRegions = true;
|
||||||
|
private bool _previewPortals = true;
|
||||||
|
private bool _previewLabels = true;
|
||||||
|
private int _selectedRegion = -1;
|
||||||
|
private bool _showAdvanced = false;
|
||||||
|
|
||||||
|
[MenuItem("PSX/Nav Region Builder")]
|
||||||
|
public static void ShowWindow()
|
||||||
|
{
|
||||||
|
GetWindow<PSXNavRegionEditor>("Nav Region Builder");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
SceneView.duringSceneGui += OnSceneGUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
SceneView.duringSceneGui -= OnSceneGUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
GUILayout.Label("PSX Nav Region Builder", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
var players = FindObjectsByType<PSXPlayer>(FindObjectsSortMode.None);
|
||||||
|
|
||||||
|
if (players.Length == 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"No PSXPlayer in scene. Add a PSXPlayer component to configure navigation settings.",
|
||||||
|
MessageType.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var player = players[0];
|
||||||
|
var so = new SerializedObject(player);
|
||||||
|
so.Update();
|
||||||
|
|
||||||
|
// Info
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"Uses DotRecast (Recast voxelization) to build PS1 nav regions.\n" +
|
||||||
|
"Settings are on the PSXPlayer component so editor preview\n" +
|
||||||
|
"and scene export always match.\n" +
|
||||||
|
"1. Configure settings below (saved on PSXPlayer)\n" +
|
||||||
|
"2. Click 'Build Nav Regions' to preview\n" +
|
||||||
|
"3. Results export automatically with the scene",
|
||||||
|
MessageType.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Agent settings (from PSXPlayer serialized fields)
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
GUILayout.Label("Agent Settings (PSXPlayer)", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("playerHeight"),
|
||||||
|
new GUIContent("Agent Height", "Camera eye height above feet"));
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("playerRadius"),
|
||||||
|
new GUIContent("Agent Radius", "Collision radius for wall sliding"));
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("maxStepHeight"),
|
||||||
|
new GUIContent("Max Step Height", "Maximum height the agent can step up"));
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("walkableSlopeAngle"),
|
||||||
|
new GUIContent("Max Slope", "Maximum walkable slope angle in degrees"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Advanced settings
|
||||||
|
_showAdvanced = EditorGUILayout.Foldout(_showAdvanced, "Advanced Settings");
|
||||||
|
if (_showAdvanced)
|
||||||
|
{
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("navCellSize"),
|
||||||
|
new GUIContent("Cell Size", "Voxel size in XZ plane. Smaller = more accurate but slower."));
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("navCellHeight"),
|
||||||
|
new GUIContent("Cell Height", "Voxel height. Smaller = more accurate vertical resolution."));
|
||||||
|
|
||||||
|
EditorGUILayout.Space(3);
|
||||||
|
float cs = player.NavCellSize;
|
||||||
|
float ch = player.NavCellHeight;
|
||||||
|
int walkH = (int)System.Math.Ceiling(player.PlayerHeight / ch);
|
||||||
|
int walkR = (int)System.Math.Ceiling(player.PlayerRadius / cs);
|
||||||
|
int walkC = (int)System.Math.Floor(player.MaxStepHeight / ch);
|
||||||
|
EditorGUILayout.LabelField("Voxel walkable height", $"{walkH} cells");
|
||||||
|
EditorGUILayout.LabelField("Voxel walkable radius", $"{walkR} cells");
|
||||||
|
EditorGUILayout.LabelField("Voxel walkable climb", $"{walkC} cells ({walkC * ch:F3} units)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
so.ApplyModifiedProperties();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Build button
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
GUILayout.Label("Generation", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.4f);
|
||||||
|
if (GUILayout.Button("Build Nav Regions", GUILayout.Height(35)))
|
||||||
|
{
|
||||||
|
BuildNavRegions(player);
|
||||||
|
}
|
||||||
|
GUI.backgroundColor = Color.white;
|
||||||
|
|
||||||
|
if (_builder != null && _builder.RegionCount > 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(3);
|
||||||
|
if (GUILayout.Button("Clear Regions"))
|
||||||
|
{
|
||||||
|
_builder = null;
|
||||||
|
_selectedRegion = -1;
|
||||||
|
SceneView.RepaintAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Visualization
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
GUILayout.Label("Visualization", EditorStyles.boldLabel);
|
||||||
|
_previewRegions = EditorGUILayout.Toggle("Show Regions", _previewRegions);
|
||||||
|
_previewPortals = EditorGUILayout.Toggle("Show Portals", _previewPortals);
|
||||||
|
_previewLabels = EditorGUILayout.Toggle("Show Labels", _previewLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
if (_builder != null && _builder.RegionCount > 0)
|
||||||
|
{
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
GUILayout.Label("Statistics", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.LabelField("Regions", _builder.RegionCount.ToString());
|
||||||
|
EditorGUILayout.LabelField("Portals", _builder.PortalCount.ToString());
|
||||||
|
|
||||||
|
var rooms = new HashSet<byte>();
|
||||||
|
for (int i = 0; i < _builder.RegionCount; i++)
|
||||||
|
rooms.Add(_builder.Regions[i].roomIndex);
|
||||||
|
EditorGUILayout.LabelField("Rooms", rooms.Count.ToString());
|
||||||
|
|
||||||
|
int exportSize = _builder.GetBinarySize();
|
||||||
|
EditorGUILayout.LabelField("Export Size",
|
||||||
|
$"{exportSize:N0} bytes ({exportSize / 1024f:F1} KB)");
|
||||||
|
|
||||||
|
int flat = 0, ramp = 0, stairs = 0;
|
||||||
|
for (int i = 0; i < _builder.RegionCount; i++)
|
||||||
|
{
|
||||||
|
switch (_builder.Regions[i].surfaceType)
|
||||||
|
{
|
||||||
|
case NavSurfaceType.Flat: flat++; break;
|
||||||
|
case NavSurfaceType.Ramp: ramp++; break;
|
||||||
|
case NavSurfaceType.Stairs: stairs++; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUILayout.LabelField("Types",
|
||||||
|
$"{flat} flat, {ramp} ramp, {stairs} stairs");
|
||||||
|
|
||||||
|
if (_selectedRegion >= 0 && _selectedRegion < _builder.RegionCount)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(3);
|
||||||
|
GUILayout.Label($"Selected Region #{_selectedRegion}",
|
||||||
|
EditorStyles.miniLabel);
|
||||||
|
var region = _builder.Regions[_selectedRegion];
|
||||||
|
EditorGUILayout.LabelField(" Vertices",
|
||||||
|
region.vertsXZ.Count.ToString());
|
||||||
|
EditorGUILayout.LabelField(" Portals",
|
||||||
|
region.portalCount.ToString());
|
||||||
|
EditorGUILayout.LabelField(" Surface",
|
||||||
|
region.surfaceType.ToString());
|
||||||
|
EditorGUILayout.LabelField(" Room",
|
||||||
|
region.roomIndex.ToString());
|
||||||
|
EditorGUILayout.LabelField(" Floor Y",
|
||||||
|
$"{region.planeD:F2} (A={region.planeA:F3}, B={region.planeB:F3})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateRegions();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"No nav regions built. Click 'Build Nav Regions' to generate.",
|
||||||
|
MessageType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Build
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
private void BuildNavRegions(PSXPlayer player)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayProgressBar("Nav Region Builder", "Building nav regions with DotRecast...", 0.3f);
|
||||||
|
|
||||||
|
_builder = new PSXNavRegionBuilder();
|
||||||
|
_builder.AgentHeight = player.PlayerHeight;
|
||||||
|
_builder.AgentRadius = player.PlayerRadius;
|
||||||
|
_builder.MaxStepHeight = player.MaxStepHeight;
|
||||||
|
_builder.WalkableSlopeAngle = player.WalkableSlopeAngle;
|
||||||
|
_builder.CellSize = player.NavCellSize;
|
||||||
|
_builder.CellHeight = player.NavCellHeight;
|
||||||
|
|
||||||
|
Vector3 playerSpawn = player.transform.position;
|
||||||
|
player.FindNavmesh();
|
||||||
|
playerSpawn = player.CamPoint;
|
||||||
|
|
||||||
|
PSXObjectExporter[] exporters =
|
||||||
|
FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
|
||||||
|
|
||||||
|
_builder.Build(exporters, playerSpawn);
|
||||||
|
|
||||||
|
_selectedRegion = -1;
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
SceneView.RepaintAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Validation
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
private void ValidateRegions()
|
||||||
|
{
|
||||||
|
if (_builder == null) return;
|
||||||
|
|
||||||
|
List<string> warnings = new List<string>();
|
||||||
|
|
||||||
|
for (int i = 0; i < _builder.RegionCount; i++)
|
||||||
|
{
|
||||||
|
var region = _builder.Regions[i];
|
||||||
|
if (region.vertsXZ.Count < 3)
|
||||||
|
warnings.Add($"Region {i}: degenerate ({region.vertsXZ.Count} verts)");
|
||||||
|
if (region.portalCount == 0 && _builder.RegionCount > 1)
|
||||||
|
warnings.Add($"Region {i}: isolated (no portals)");
|
||||||
|
if (region.vertsXZ.Count > 8)
|
||||||
|
warnings.Add($"Region {i}: too many verts ({region.vertsXZ.Count} > 8)");
|
||||||
|
}
|
||||||
|
|
||||||
|
int exportSize = _builder.GetBinarySize();
|
||||||
|
if (exportSize > 8192)
|
||||||
|
warnings.Add($"Export size {exportSize} bytes is large for PS1 (> 8KB)");
|
||||||
|
|
||||||
|
if (warnings.Count > 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||||
|
{
|
||||||
|
GUILayout.Label("Warnings", EditorStyles.boldLabel);
|
||||||
|
foreach (string w in warnings)
|
||||||
|
EditorGUILayout.LabelField(w, EditorStyles.miniLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Scene view drawing
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
private static readonly Color[] RoomColors = new Color[]
|
||||||
|
{
|
||||||
|
new Color(0.2f, 0.8f, 0.2f),
|
||||||
|
new Color(0.2f, 0.6f, 0.9f),
|
||||||
|
new Color(0.9f, 0.7f, 0.1f),
|
||||||
|
new Color(0.8f, 0.2f, 0.8f),
|
||||||
|
new Color(0.1f, 0.9f, 0.9f),
|
||||||
|
new Color(0.9f, 0.5f, 0.2f),
|
||||||
|
new Color(0.5f, 0.9f, 0.3f),
|
||||||
|
new Color(0.9f, 0.3f, 0.5f),
|
||||||
|
new Color(0.4f, 0.4f, 0.9f),
|
||||||
|
new Color(0.7f, 0.9f, 0.7f),
|
||||||
|
new Color(0.9f, 0.9f, 0.4f),
|
||||||
|
new Color(0.6f, 0.3f, 0.6f),
|
||||||
|
new Color(0.3f, 0.7f, 0.7f),
|
||||||
|
new Color(0.8f, 0.6f, 0.4f),
|
||||||
|
new Color(0.4f, 0.8f, 0.6f),
|
||||||
|
new Color(0.7f, 0.4f, 0.4f),
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnSceneGUI(SceneView sceneView)
|
||||||
|
{
|
||||||
|
if (_builder == null || _builder.RegionCount == 0) return;
|
||||||
|
|
||||||
|
var regions = _builder.Regions;
|
||||||
|
|
||||||
|
if (_previewRegions)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < regions.Count; i++)
|
||||||
|
{
|
||||||
|
var region = regions[i];
|
||||||
|
bool selected = (i == _selectedRegion);
|
||||||
|
|
||||||
|
Color baseColor = RoomColors[region.roomIndex % RoomColors.Length];
|
||||||
|
float fillAlpha = selected ? 0.4f : 0.15f;
|
||||||
|
|
||||||
|
if (region.vertsXZ.Count >= 3)
|
||||||
|
{
|
||||||
|
Vector3[] worldVerts = new Vector3[region.vertsXZ.Count];
|
||||||
|
for (int v = 0; v < region.vertsXZ.Count; v++)
|
||||||
|
{
|
||||||
|
float y = region.planeA * region.vertsXZ[v].x +
|
||||||
|
region.planeB * region.vertsXZ[v].y +
|
||||||
|
region.planeD;
|
||||||
|
worldVerts[v] = new Vector3(
|
||||||
|
region.vertsXZ[v].x, y + 0.05f, region.vertsXZ[v].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handles.color = selected
|
||||||
|
? Color.white
|
||||||
|
: new Color(baseColor.r, baseColor.g, baseColor.b, 0.8f);
|
||||||
|
for (int v = 0; v < worldVerts.Length; v++)
|
||||||
|
Handles.DrawLine(worldVerts[v],
|
||||||
|
worldVerts[(v + 1) % worldVerts.Length]);
|
||||||
|
|
||||||
|
Handles.color = new Color(baseColor.r, baseColor.g, baseColor.b,
|
||||||
|
fillAlpha);
|
||||||
|
for (int v = 1; v < worldVerts.Length - 1; v++)
|
||||||
|
Handles.DrawAAConvexPolygon(
|
||||||
|
worldVerts[0], worldVerts[v], worldVerts[v + 1]);
|
||||||
|
|
||||||
|
if (_previewLabels)
|
||||||
|
{
|
||||||
|
Vector3 center = Vector3.zero;
|
||||||
|
foreach (var wv in worldVerts) center += wv;
|
||||||
|
center /= worldVerts.Length;
|
||||||
|
|
||||||
|
string label = $"R{i}";
|
||||||
|
if (region.roomIndex != 0xFF)
|
||||||
|
label += $"\nRm{region.roomIndex}";
|
||||||
|
Handles.Label(center, label, EditorStyles.whiteBoldLabel);
|
||||||
|
|
||||||
|
if (Handles.Button(center, Quaternion.identity,
|
||||||
|
0.2f, 0.3f, Handles.SphereHandleCap))
|
||||||
|
{
|
||||||
|
_selectedRegion = i;
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_previewPortals && _builder.Portals != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < regions.Count; i++)
|
||||||
|
{
|
||||||
|
var region = regions[i];
|
||||||
|
int pStart = region.portalStart;
|
||||||
|
int pCount = region.portalCount;
|
||||||
|
|
||||||
|
for (int p = pStart;
|
||||||
|
p < pStart + pCount && p < _builder.Portals.Count; p++)
|
||||||
|
{
|
||||||
|
var portal = _builder.Portals[p];
|
||||||
|
|
||||||
|
float yA = region.planeA * portal.a.x +
|
||||||
|
region.planeB * portal.a.y + region.planeD;
|
||||||
|
float yB = region.planeA * portal.b.x +
|
||||||
|
region.planeB * portal.b.y + region.planeD;
|
||||||
|
|
||||||
|
Vector3 worldA = new Vector3(portal.a.x, yA + 0.08f, portal.a.y);
|
||||||
|
Vector3 worldB = new Vector3(portal.b.x, yB + 0.08f, portal.b.y);
|
||||||
|
|
||||||
|
if (Mathf.Abs(portal.heightDelta) <= 0.35f)
|
||||||
|
Handles.color = new Color(1f, 1f, 1f, 0.9f);
|
||||||
|
else
|
||||||
|
Handles.color = new Color(1f, 0.9f, 0.2f, 0.9f);
|
||||||
|
|
||||||
|
Handles.DrawLine(worldA, worldB, 3f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/PSXNavRegionEditor.cs.meta
Normal file
2
Editor/PSXNavRegionEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e6ea40b4c8e02314c9388c86b2920403
|
||||||
514
Editor/PSXObjectExporterEditor.cs
Normal file
514
Editor/PSXObjectExporterEditor.cs
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom inspector for PSXObjectExporter with enhanced UX.
|
||||||
|
/// Shows mesh info, texture preview, collision visualization, and validation.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PSXObjectExporter))]
|
||||||
|
[CanEditMultipleObjects]
|
||||||
|
public class PSXObjectExporterEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
// Serialized properties
|
||||||
|
private SerializedProperty isActiveProp;
|
||||||
|
private SerializedProperty bitDepthProp;
|
||||||
|
private SerializedProperty luaFileProp;
|
||||||
|
private SerializedProperty objectFlagsProp;
|
||||||
|
private SerializedProperty collisionTypeProp;
|
||||||
|
private SerializedProperty exportCollisionMeshProp;
|
||||||
|
private SerializedProperty customCollisionMeshProp;
|
||||||
|
private SerializedProperty collisionLayerProp;
|
||||||
|
private SerializedProperty previewNormalsProp;
|
||||||
|
private SerializedProperty normalPreviewLengthProp;
|
||||||
|
private SerializedProperty showCollisionBoundsProp;
|
||||||
|
private SerializedProperty textureProp;
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
private bool showMeshInfo = true;
|
||||||
|
private bool showTextureInfo = true;
|
||||||
|
private bool showExportSettings = true;
|
||||||
|
private bool showCollisionSettings = true;
|
||||||
|
private bool showGizmoSettings = false;
|
||||||
|
private bool showValidation = true;
|
||||||
|
|
||||||
|
// Cached data
|
||||||
|
private MeshFilter meshFilter;
|
||||||
|
private MeshRenderer meshRenderer;
|
||||||
|
private int triangleCount;
|
||||||
|
private int vertexCount;
|
||||||
|
private Bounds meshBounds;
|
||||||
|
private List<string> validationErrors = new List<string>();
|
||||||
|
private List<string> validationWarnings = new List<string>();
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
private GUIStyle headerStyle;
|
||||||
|
private GUIStyle errorStyle;
|
||||||
|
private GUIStyle warningStyle;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
private bool _validationDirty = true;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
// Get serialized properties
|
||||||
|
isActiveProp = serializedObject.FindProperty("isActive");
|
||||||
|
bitDepthProp = serializedObject.FindProperty("bitDepth");
|
||||||
|
luaFileProp = serializedObject.FindProperty("luaFile");
|
||||||
|
objectFlagsProp = serializedObject.FindProperty("objectFlags");
|
||||||
|
collisionTypeProp = serializedObject.FindProperty("collisionType");
|
||||||
|
exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh");
|
||||||
|
customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh");
|
||||||
|
collisionLayerProp = serializedObject.FindProperty("collisionLayer");
|
||||||
|
previewNormalsProp = serializedObject.FindProperty("previewNormals");
|
||||||
|
normalPreviewLengthProp = serializedObject.FindProperty("normalPreviewLength");
|
||||||
|
showCollisionBoundsProp = serializedObject.FindProperty("showCollisionBounds");
|
||||||
|
textureProp = serializedObject.FindProperty("texture");
|
||||||
|
|
||||||
|
// Cache mesh info
|
||||||
|
CacheMeshInfo();
|
||||||
|
|
||||||
|
// Defer validation to first inspector draw
|
||||||
|
_validationDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheMeshInfo()
|
||||||
|
{
|
||||||
|
var exporter = target as PSXObjectExporter;
|
||||||
|
if (exporter == null) return;
|
||||||
|
|
||||||
|
meshFilter = exporter.GetComponent<MeshFilter>();
|
||||||
|
meshRenderer = exporter.GetComponent<MeshRenderer>();
|
||||||
|
|
||||||
|
if (meshFilter != null && meshFilter.sharedMesh != null)
|
||||||
|
{
|
||||||
|
var mesh = meshFilter.sharedMesh;
|
||||||
|
triangleCount = mesh.triangles.Length / 3;
|
||||||
|
vertexCount = mesh.vertexCount;
|
||||||
|
meshBounds = mesh.bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunValidation()
|
||||||
|
{
|
||||||
|
validationErrors.Clear();
|
||||||
|
validationWarnings.Clear();
|
||||||
|
|
||||||
|
var exporter = target as PSXObjectExporter;
|
||||||
|
if (exporter == null) return;
|
||||||
|
|
||||||
|
// Check mesh
|
||||||
|
if (meshFilter == null || meshFilter.sharedMesh == null)
|
||||||
|
{
|
||||||
|
validationErrors.Add("No mesh assigned to MeshFilter");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (triangleCount > 100)
|
||||||
|
{
|
||||||
|
validationWarnings.Add($"High triangle count ({triangleCount}). PS1 recommended: <100 per object");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check vertex bounds
|
||||||
|
var mesh = meshFilter.sharedMesh;
|
||||||
|
var verts = mesh.vertices;
|
||||||
|
bool hasOutOfBounds = false;
|
||||||
|
|
||||||
|
foreach (var v in verts)
|
||||||
|
{
|
||||||
|
var world = exporter.transform.TransformPoint(v);
|
||||||
|
float scaled = Mathf.Max(Mathf.Abs(world.x), Mathf.Abs(world.y), Mathf.Abs(world.z)) * 4096f;
|
||||||
|
if (scaled > 32767f)
|
||||||
|
{
|
||||||
|
hasOutOfBounds = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOutOfBounds)
|
||||||
|
{
|
||||||
|
validationErrors.Add("Vertices exceed PS1 coordinate limits (±8 units from origin)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check renderer
|
||||||
|
if (meshRenderer == null)
|
||||||
|
{
|
||||||
|
validationWarnings.Add("No MeshRenderer - object will not be visible");
|
||||||
|
}
|
||||||
|
else if (meshRenderer.sharedMaterial == null)
|
||||||
|
{
|
||||||
|
validationWarnings.Add("No material assigned - will use default colors");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
// Run deferred validation
|
||||||
|
if (_validationDirty)
|
||||||
|
{
|
||||||
|
RunValidation();
|
||||||
|
_validationDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitStyles();
|
||||||
|
|
||||||
|
// Active toggle at top
|
||||||
|
EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object"));
|
||||||
|
|
||||||
|
if (!isActiveProp.boolValue)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("This object will be skipped during export.", MessageType.Info);
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Mesh Info Section
|
||||||
|
DrawMeshInfoSection();
|
||||||
|
|
||||||
|
// Texture Section
|
||||||
|
DrawTextureSection();
|
||||||
|
|
||||||
|
// Export Settings Section
|
||||||
|
DrawExportSettingsSection();
|
||||||
|
|
||||||
|
// Collision Settings Section
|
||||||
|
DrawCollisionSettingsSection();
|
||||||
|
|
||||||
|
// Gizmo Settings Section
|
||||||
|
DrawGizmoSettingsSection();
|
||||||
|
|
||||||
|
// Validation Section
|
||||||
|
DrawValidationSection();
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
DrawActionButtons();
|
||||||
|
|
||||||
|
if (serializedObject.ApplyModifiedProperties())
|
||||||
|
{
|
||||||
|
_validationDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitStyles()
|
||||||
|
{
|
||||||
|
if (headerStyle == null)
|
||||||
|
{
|
||||||
|
headerStyle = new GUIStyle(EditorStyles.foldoutHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorStyle == null)
|
||||||
|
{
|
||||||
|
errorStyle = new GUIStyle(EditorStyles.label);
|
||||||
|
errorStyle.normal.textColor = Color.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warningStyle == null)
|
||||||
|
{
|
||||||
|
warningStyle = new GUIStyle(EditorStyles.label);
|
||||||
|
warningStyle.normal.textColor = new Color(1f, 0.7f, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMeshInfoSection()
|
||||||
|
{
|
||||||
|
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information");
|
||||||
|
if (showMeshInfo)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
if (meshFilter != null && meshFilter.sharedMesh != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField("Mesh", meshFilter.sharedMesh.name);
|
||||||
|
EditorGUILayout.LabelField("Triangles", triangleCount.ToString());
|
||||||
|
EditorGUILayout.LabelField("Vertices", vertexCount.ToString());
|
||||||
|
EditorGUILayout.LabelField("Bounds Size", meshBounds.size.ToString("F2"));
|
||||||
|
|
||||||
|
// Triangle budget bar
|
||||||
|
float budgetPercent = triangleCount / 100f;
|
||||||
|
Rect rect = EditorGUILayout.GetControlRect(false, 20);
|
||||||
|
EditorGUI.ProgressBar(rect, Mathf.Clamp01(budgetPercent), $"Triangle Budget: {triangleCount}/100");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("No mesh assigned", MessageType.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTextureSection()
|
||||||
|
{
|
||||||
|
showTextureInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showTextureInfo, "Texture Settings");
|
||||||
|
if (showTextureInfo)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(textureProp, new GUIContent("Override Texture"));
|
||||||
|
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
|
||||||
|
|
||||||
|
// Show texture preview if assigned
|
||||||
|
var tex = textureProp.objectReferenceValue as Texture2D;
|
||||||
|
if (tex != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
using (new EditorGUILayout.HorizontalScope())
|
||||||
|
{
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
Rect previewRect = GUILayoutUtility.GetRect(64, 64, GUILayout.Width(64));
|
||||||
|
EditorGUI.DrawPreviewTexture(previewRect, tex);
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField($"Size: {tex.width}x{tex.height}");
|
||||||
|
|
||||||
|
// VRAM estimate
|
||||||
|
int bpp = bitDepthProp.enumValueIndex == 0 ? 4 : (bitDepthProp.enumValueIndex == 1 ? 8 : 16);
|
||||||
|
int vramBytes = (tex.width * tex.height * bpp) / 8;
|
||||||
|
EditorGUILayout.LabelField($"Est. VRAM: {vramBytes} bytes ({bpp}bpp)");
|
||||||
|
}
|
||||||
|
else if (meshRenderer != null && meshRenderer.sharedMaterial != null)
|
||||||
|
{
|
||||||
|
var matTex = meshRenderer.sharedMaterial.mainTexture;
|
||||||
|
if (matTex != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox($"Using material texture: {matTex.name}", MessageType.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawExportSettingsSection()
|
||||||
|
{
|
||||||
|
showExportSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showExportSettings, "Export Settings");
|
||||||
|
if (showExportSettings)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(objectFlagsProp, new GUIContent("Object Flags"));
|
||||||
|
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
|
||||||
|
|
||||||
|
// Quick Lua file buttons
|
||||||
|
if (luaFileProp.objectReferenceValue != null)
|
||||||
|
{
|
||||||
|
using (new EditorGUILayout.HorizontalScope())
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Edit Lua", GUILayout.Width(80)))
|
||||||
|
{
|
||||||
|
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("Clear", GUILayout.Width(60)))
|
||||||
|
{
|
||||||
|
luaFileProp.objectReferenceValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Create New Lua Script"))
|
||||||
|
{
|
||||||
|
CreateNewLuaScript();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCollisionSettingsSection()
|
||||||
|
{
|
||||||
|
showCollisionSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showCollisionSettings, "Collision Settings");
|
||||||
|
if (showCollisionSettings)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Collision Type"));
|
||||||
|
|
||||||
|
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
|
||||||
|
if (collType != PSXCollisionType.None)
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
|
||||||
|
EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Collision Mesh"));
|
||||||
|
EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Collision Layer"));
|
||||||
|
|
||||||
|
// Collision info
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
string collisionInfo = collType switch
|
||||||
|
{
|
||||||
|
PSXCollisionType.Solid => "Solid: Blocks movement, fires onCollision",
|
||||||
|
PSXCollisionType.Trigger => "Trigger: Fires onTriggerEnter/Exit, doesn't block",
|
||||||
|
PSXCollisionType.Platform => "Platform: Solid from above only",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
EditorGUILayout.HelpBox(collisionInfo, MessageType.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGizmoSettingsSection()
|
||||||
|
{
|
||||||
|
showGizmoSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showGizmoSettings, "Gizmo Settings");
|
||||||
|
if (showGizmoSettings)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(previewNormalsProp, new GUIContent("Preview Normals"));
|
||||||
|
if (previewNormalsProp.boolValue)
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(normalPreviewLengthProp, new GUIContent("Normal Length"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(showCollisionBoundsProp, new GUIContent("Show Collision Bounds"));
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawValidationSection()
|
||||||
|
{
|
||||||
|
if (validationErrors.Count == 0 && validationWarnings.Count == 0)
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActionButtons()
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
using (new EditorGUILayout.HorizontalScope())
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Select Scene Exporter"))
|
||||||
|
{
|
||||||
|
var exporter = FindObjectOfType<PSXSceneExporter>();
|
||||||
|
if (exporter != null)
|
||||||
|
{
|
||||||
|
Selection.activeGameObject = exporter.gameObject;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("Open Scene Validator"))
|
||||||
|
{
|
||||||
|
PSXSceneValidatorWindow.ShowWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateNewLuaScript()
|
||||||
|
{
|
||||||
|
var exporter = target as PSXObjectExporter;
|
||||||
|
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
|
||||||
|
string path = EditorUtility.SaveFilePanelInProject(
|
||||||
|
"Create Lua Script",
|
||||||
|
defaultName + ".lua",
|
||||||
|
"lua",
|
||||||
|
"Create a new Lua script for this object");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
string template = $@"-- Lua script for {exporter.gameObject.name}
|
||||||
|
--
|
||||||
|
-- Available globals: Entity, Vec3, Input, Timer, Camera, Audio,
|
||||||
|
-- Debug, Math, Scene, Persist
|
||||||
|
--
|
||||||
|
-- Available events:
|
||||||
|
-- onCreate(self) — called once when the object is registered
|
||||||
|
-- onUpdate(self, dt) — called every frame (dt = delta frames, usually 1)
|
||||||
|
-- onEnable(self) — called when the object becomes active
|
||||||
|
-- onDisable(self) — called when the object becomes inactive
|
||||||
|
-- onCollision(self, other) — called on collision with another object
|
||||||
|
-- onTriggerEnter(self, other)
|
||||||
|
-- onTriggerStay(self, other)
|
||||||
|
-- onTriggerExit(self, other)
|
||||||
|
-- onInteract(self) — called when the player interacts
|
||||||
|
-- onButtonPress(self, btn) — called on button press (btn = Input.CROSS etc.)
|
||||||
|
-- onButtonRelease(self, btn)
|
||||||
|
-- onDestroy(self) — called before the object is destroyed
|
||||||
|
--
|
||||||
|
-- Properties: self.position (Vec3), self.rotationY (pi-units), self.active (bool)
|
||||||
|
|
||||||
|
function onCreate(self)
|
||||||
|
-- Called once when this object is registered in the scene
|
||||||
|
end
|
||||||
|
|
||||||
|
function onUpdate(self, dt)
|
||||||
|
-- Called every frame. dt = number of elapsed frames (usually 1).
|
||||||
|
end
|
||||||
|
|
||||||
|
function onInteract(self)
|
||||||
|
-- Called when the player interacts with this object
|
||||||
|
end
|
||||||
|
";
|
||||||
|
System.IO.File.WriteAllText(path, template);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
|
||||||
|
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
|
||||||
|
if (luaFile != null)
|
||||||
|
{
|
||||||
|
luaFileProp.objectReferenceValue = luaFile;
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("CONTEXT/PSXObjectExporter/Copy Settings to Selected")]
|
||||||
|
private static void CopySettingsToSelected(MenuCommand command)
|
||||||
|
{
|
||||||
|
var source = command.context as PSXObjectExporter;
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
foreach (var go in Selection.gameObjects)
|
||||||
|
{
|
||||||
|
var target = go.GetComponent<PSXObjectExporter>();
|
||||||
|
if (target != null && target != source)
|
||||||
|
{
|
||||||
|
Undo.RecordObject(target, "Copy PSX Settings");
|
||||||
|
// Copy via serialized object
|
||||||
|
EditorUtility.CopySerialized(source, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/PSXObjectExporterEditor.cs.meta
Normal file
2
Editor/PSXObjectExporterEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d45032f12fc4b614783ad30927846e6c
|
||||||
@@ -4,19 +4,140 @@ using SplashEdit.RuntimeCode;
|
|||||||
|
|
||||||
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 : Editor
|
public class PSXSceneExporterEditor : UnityEditor.Editor
|
||||||
{
|
{
|
||||||
public override void OnInspectorGUI()
|
// Saved RenderSettings state so we can restore it on deselect.
|
||||||
{
|
private bool _savedFog;
|
||||||
DrawDefaultInspector();
|
private Color _savedFogColor;
|
||||||
|
private FogMode _savedFogMode;
|
||||||
|
private float _savedFogStart;
|
||||||
|
private float _savedFogEnd;
|
||||||
|
|
||||||
PSXSceneExporter comp = (PSXSceneExporter)target;
|
private bool _previewActive = false;
|
||||||
if (GUILayout.Button("Export"))
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
SaveAndApplyFogPreview();
|
||||||
|
// Re-apply whenever the scene is repainted (handles inspector value changes).
|
||||||
|
EditorApplication.update += OnEditorUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
comp.Export();
|
// Fog disabled on the component - turn off the preview.
|
||||||
|
RenderSettings.fog = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float gteScale = exporter.GTEScaling;
|
||||||
|
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
|
||||||
|
|
||||||
|
// fogFarSZ in GTE SZ units (20.12 fp); convert to Unity world-space.
|
||||||
|
// SZ = (unityDist / GTEScaling) * 4096, so unityDist = SZ * GTEScaling / 4096
|
||||||
|
float fogFarSZ = 8000f / density;
|
||||||
|
float fogNearSZ = fogFarSZ / 3f;
|
||||||
|
|
||||||
|
float fogFarUnity = fogFarSZ * gteScale / 4096f;
|
||||||
|
float fogNearUnity = fogNearSZ * gteScale / 4096f;
|
||||||
|
|
||||||
|
RenderSettings.fog = true;
|
||||||
|
RenderSettings.fogColor = exporter.FogColor;
|
||||||
|
RenderSettings.fogMode = FogMode.Linear;
|
||||||
|
RenderSettings.fogStartDistance = fogNearUnity;
|
||||||
|
RenderSettings.fogEndDistance = fogFarUnity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreFog()
|
||||||
|
{
|
||||||
|
if (!_previewActive) return;
|
||||||
|
_previewActive = false;
|
||||||
|
|
||||||
|
RenderSettings.fog = _savedFog;
|
||||||
|
RenderSettings.fogColor = _savedFogColor;
|
||||||
|
RenderSettings.fogMode = _savedFogMode;
|
||||||
|
RenderSettings.fogStartDistance = _savedFogStart;
|
||||||
|
RenderSettings.fogEndDistance = _savedFogEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
DrawDefaultInspector();
|
||||||
|
|
||||||
|
// Show computed fog distances when fog is enabled, so the user
|
||||||
|
// can see exactly what range the preview represents.
|
||||||
|
var exporter = (PSXSceneExporter)target;
|
||||||
|
if (exporter.FogEnabled)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
GUILayout.Label("Fog Preview (active in Scene view)", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
float gteScale = exporter.GTEScaling;
|
||||||
|
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
|
||||||
|
float fogFarUnity = (8000f / density) * gteScale / 4096f;
|
||||||
|
float fogNearUnity = fogFarUnity / 3f;
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("Near distance", $"{fogNearUnity:F1} Unity units");
|
||||||
|
EditorGUILayout.LabelField("Far distance", $"{fogFarUnity:F1} Unity units");
|
||||||
|
EditorGUILayout.LabelField("(PS1 SZ range)", $"{8000f / (density * 3f):F0} - {8000f / density:F0} GTE units");
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
// Keep preview applied as values may have changed.
|
||||||
|
ApplyFogPreview();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Make sure preview is off when fog is disabled.
|
||||||
|
RenderSettings.fog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: becf2eb607e7a60baaf3bebe4683d66f
|
guid: 738efb5c0ed755b45991d2067957b997
|
||||||
496
Editor/PSXSceneValidatorWindow.cs
Normal file
496
Editor/PSXSceneValidatorWindow.cs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/PSXSceneValidatorWindow.cs.meta
Normal file
2
Editor/PSXSceneValidatorWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0a26bf89301a2554ca287b9e28e44906
|
||||||
203
Editor/PSXSplashInstaller.cs
Normal file
203
Editor/PSXSplashInstaller.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
|
||||||
|
public static class PSXSplashInstaller
|
||||||
|
{
|
||||||
|
public static readonly string RepoUrl = "https://github.com/psxsplash/psxsplash.git";
|
||||||
|
public static readonly string InstallPath = "Assets/psxsplash";
|
||||||
|
public static readonly string FullInstallPath;
|
||||||
|
|
||||||
|
static PSXSplashInstaller()
|
||||||
|
{
|
||||||
|
FullInstallPath = Path.Combine(Application.dataPath, "psxsplash");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsInstalled()
|
||||||
|
{
|
||||||
|
return Directory.Exists(FullInstallPath) &&
|
||||||
|
Directory.EnumerateFileSystemEntries(FullInstallPath).Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> Install()
|
||||||
|
{
|
||||||
|
if (IsInstalled()) return true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create the parent directory if it doesn't exist
|
||||||
|
Directory.CreateDirectory(Application.dataPath);
|
||||||
|
|
||||||
|
// Clone the repository
|
||||||
|
var result = await RunGitCommandAsync($"clone --recursive {RepoUrl} \"{FullInstallPath}\"", Application.dataPath);
|
||||||
|
return !result.Contains("error");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Failed to install PSXSplash: {e.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Dictionary<string, string>> GetBranchesWithLatestCommitsAsync()
|
||||||
|
{
|
||||||
|
if (!IsInstalled()) return new Dictionary<string, string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Fetch all branches and tags
|
||||||
|
await RunGitCommandAsync("fetch --all", FullInstallPath);
|
||||||
|
|
||||||
|
// Get all remote branches
|
||||||
|
var branchesOutput = await RunGitCommandAsync("branch -r", FullInstallPath);
|
||||||
|
var branches = branchesOutput.Split('\n')
|
||||||
|
.Where(b => !string.IsNullOrEmpty(b.Trim()))
|
||||||
|
.Select(b => b.Trim().Replace("origin/", ""))
|
||||||
|
.Where(b => !b.Contains("HEAD"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var branchesWithCommits = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
// Get the latest commit for each branch
|
||||||
|
foreach (var branch in branches)
|
||||||
|
{
|
||||||
|
var commitOutput = await RunGitCommandAsync($"log origin/{branch} -1 --pretty=format:%h", FullInstallPath);
|
||||||
|
if (!string.IsNullOrEmpty(commitOutput))
|
||||||
|
{
|
||||||
|
branchesWithCommits[branch] = commitOutput.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return branchesWithCommits;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Failed to get branches: {e.Message}");
|
||||||
|
return new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<List<string>> GetReleasesAsync()
|
||||||
|
{
|
||||||
|
if (!IsInstalled()) return new List<string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RunGitCommandAsync("fetch --tags", FullInstallPath);
|
||||||
|
var output = await RunGitCommandAsync("tag -l", FullInstallPath);
|
||||||
|
|
||||||
|
return output.Split('\n')
|
||||||
|
.Where(t => !string.IsNullOrEmpty(t.Trim()))
|
||||||
|
.Select(t => t.Trim())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Failed to get releases: {e.Message}");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> CheckoutVersionAsync(string version)
|
||||||
|
{
|
||||||
|
if (!IsInstalled()) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If it's a branch name, checkout the branch
|
||||||
|
// If it's a commit hash, checkout the commit
|
||||||
|
var result = await RunGitCommandAsync($"checkout {version}", FullInstallPath);
|
||||||
|
var result2 = await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath);
|
||||||
|
|
||||||
|
return !result.Contains("error") && !result2.Contains("error");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Failed to checkout version: {e.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> FetchLatestAsync()
|
||||||
|
{
|
||||||
|
if (!IsInstalled()) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await RunGitCommandAsync("fetch --all", FullInstallPath);
|
||||||
|
return !result.Contains("error");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Failed to fetch latest: {e.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> RunGitCommandAsync(string arguments, string workingDirectory)
|
||||||
|
{
|
||||||
|
var processInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "git",
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var process = new Process())
|
||||||
|
{
|
||||||
|
process.StartInfo = processInfo;
|
||||||
|
var outputBuilder = new System.Text.StringBuilder();
|
||||||
|
var errorBuilder = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
process.OutputDataReceived += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(e.Data))
|
||||||
|
outputBuilder.AppendLine(e.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.ErrorDataReceived += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(e.Data))
|
||||||
|
errorBuilder.AppendLine(e.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
// Wait for exit with timeout
|
||||||
|
var timeout = TimeSpan.FromSeconds(30);
|
||||||
|
if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
|
||||||
|
{
|
||||||
|
process.WaitForExit(); // Ensure all output is processed
|
||||||
|
|
||||||
|
string output = outputBuilder.ToString();
|
||||||
|
string error = errorBuilder.ToString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(error))
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Git error: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
throw new TimeoutException("Git command timed out");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/PSXSplashInstaller.cs.meta
Normal file
2
Editor/PSXSplashInstaller.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 72d1da27a16f0794cb1ad49c00799e74
|
||||||
@@ -17,7 +17,7 @@ namespace SplashEdit.EditorCode
|
|||||||
private PSXBPP bpp = PSXBPP.TEX_4BIT;
|
private PSXBPP bpp = PSXBPP.TEX_4BIT;
|
||||||
private readonly int previewSize = 256;
|
private readonly int previewSize = 256;
|
||||||
|
|
||||||
[MenuItem("Window/Quantized Preview")]
|
[MenuItem("PlayStation 1/Quantized Preview")]
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
// Creates and displays the window
|
// Creates and displays the window
|
||||||
|
|||||||
84
Editor/ToolchainChecker.cs
Normal file
84
Editor/ToolchainChecker.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.IO;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility that detects whether required build tools (MIPS cross-compiler,
|
||||||
|
/// GNU Make, GDB, etc.) are available on the host system by probing the
|
||||||
|
/// PATH via <c>where</c> (Windows) or <c>which</c> (Unix).
|
||||||
|
/// </summary>
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
public static class ToolchainChecker
|
||||||
|
{
|
||||||
|
private static readonly string[] mipsToolSuffixes = new[]
|
||||||
|
{
|
||||||
|
"addr2line", "ar", "as", "cpp", "elfedit", "g++", "gcc", "gcc-ar", "gcc-nm",
|
||||||
|
"gcc-ranlib", "gcov", "ld", "nm", "objcopy", "objdump", "ranlib", "readelf",
|
||||||
|
"size", "strings", "strip"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the full tool names to be checked, based on platform.
|
||||||
|
/// </summary>
|
||||||
|
public static string[] GetRequiredTools()
|
||||||
|
{
|
||||||
|
string prefix = Application.platform == RuntimePlatform.WindowsEditor
|
||||||
|
? "mipsel-none-elf-"
|
||||||
|
: "mipsel-linux-gnu-";
|
||||||
|
|
||||||
|
return mipsToolSuffixes.Select(s => prefix + s).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for availability of any tool (either full name like "make" or "mipsel-*").
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsToolAvailable(string toolName)
|
||||||
|
{
|
||||||
|
string command = Application.platform == RuntimePlatform.WindowsEditor ? "where" : "which";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = command,
|
||||||
|
Arguments = toolName,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(output))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Additional fallback for MIPS tools on Windows in local MIPS path
|
||||||
|
if (Application.platform == RuntimePlatform.WindowsEditor &&
|
||||||
|
toolName.StartsWith("mipsel-none-elf-"))
|
||||||
|
{
|
||||||
|
string localMipsBin = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"mips", "mips", "bin");
|
||||||
|
|
||||||
|
string fullPath = Path.Combine(localMipsBin, toolName + ".exe");
|
||||||
|
return File.Exists(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/ToolchainChecker.cs.meta
Normal file
2
Editor/ToolchainChecker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 142296fdef504c64bb08110e6f28e581
|
||||||
137
Editor/ToolchainInstaller.cs
Normal file
137
Editor/ToolchainInstaller.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SplashEdit.EditorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Installs the MIPS cross-compiler toolchain and GNU Make.
|
||||||
|
/// Supports Windows and Linux only.
|
||||||
|
/// </summary>
|
||||||
|
public static class ToolchainInstaller
|
||||||
|
{
|
||||||
|
private static bool _installing;
|
||||||
|
|
||||||
|
public static string MipsVersion = "14.2.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs an external process and waits for it to exit.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task RunCommandAsync(string fileName, string arguments, string workingDirectory = "")
|
||||||
|
{
|
||||||
|
if (fileName.Equals("mips", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
fileName = "powershell";
|
||||||
|
string roamingPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
string scriptPath = Path.Combine(roamingPath, "mips", "mips.ps1");
|
||||||
|
arguments = $"-ExecutionPolicy Bypass -File \"{scriptPath}\" {arguments}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<int>();
|
||||||
|
|
||||||
|
Process process = new Process();
|
||||||
|
process.StartInfo.FileName = fileName;
|
||||||
|
process.StartInfo.Arguments = arguments;
|
||||||
|
process.StartInfo.CreateNoWindow = false;
|
||||||
|
process.StartInfo.UseShellExecute = true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(workingDirectory))
|
||||||
|
process.StartInfo.WorkingDirectory = workingDirectory;
|
||||||
|
|
||||||
|
process.EnableRaisingEvents = true;
|
||||||
|
process.Exited += (sender, args) =>
|
||||||
|
{
|
||||||
|
tcs.SetResult(process.ExitCode);
|
||||||
|
process.Dispose();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
int exitCode = await tcs.Task;
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new Exception($"Process '{fileName}' exited with code {exitCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs the MIPS GCC cross-compiler for the current platform.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> InstallToolchain()
|
||||||
|
{
|
||||||
|
if (_installing) return false;
|
||||||
|
_installing = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||||
|
{
|
||||||
|
if (!ToolchainChecker.IsToolAvailable("mips"))
|
||||||
|
{
|
||||||
|
await RunCommandAsync("powershell",
|
||||||
|
"-c \"& { iwr -UseBasicParsing https://raw.githubusercontent.com/grumpycoders/pcsx-redux/main/mips.ps1 | iex }\"");
|
||||||
|
EditorUtility.DisplayDialog("Reboot Required",
|
||||||
|
"Installing the MIPS toolchain requires a reboot. Please reboot and try again.",
|
||||||
|
"OK");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await RunCommandAsync("mips", $"install {MipsVersion}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||||
|
{
|
||||||
|
if (ToolchainChecker.IsToolAvailable("apt"))
|
||||||
|
await RunCommandAsync("pkexec", "apt install g++-mipsel-linux-gnu -y");
|
||||||
|
else if (ToolchainChecker.IsToolAvailable("trizen"))
|
||||||
|
await RunCommandAsync("trizen", "-S cross-mipsel-linux-gnu-binutils cross-mipsel-linux-gnu-gcc");
|
||||||
|
else
|
||||||
|
throw new Exception("Unsupported Linux distribution. Install mipsel-linux-gnu-gcc manually.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Only Windows and Linux are supported.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("Error",
|
||||||
|
$"Toolchain installation failed: {ex.Message}", "OK");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_installing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs GNU Make. On Windows it is bundled with the MIPS toolchain.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task InstallMake()
|
||||||
|
{
|
||||||
|
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||||
|
{
|
||||||
|
bool proceed = EditorUtility.DisplayDialog(
|
||||||
|
"Install GNU Make",
|
||||||
|
"On Windows, GNU Make is included with the MIPS toolchain installer. Install the full toolchain?",
|
||||||
|
"Yes", "No");
|
||||||
|
if (proceed) await InstallToolchain();
|
||||||
|
}
|
||||||
|
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||||
|
{
|
||||||
|
if (ToolchainChecker.IsToolAvailable("apt"))
|
||||||
|
await RunCommandAsync("pkexec", "apt install build-essential -y");
|
||||||
|
else
|
||||||
|
throw new Exception("Unsupported Linux distribution. Install 'make' manually.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Only Windows and Linux are supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/ToolchainInstaller.cs.meta
Normal file
2
Editor/ToolchainInstaller.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c5aa88b01a3eef145806c8e9e59f4e9d
|
||||||
@@ -37,7 +37,7 @@ namespace SplashEdit.EditorCode
|
|||||||
};
|
};
|
||||||
private static string[] resolutionsStrings => resolutions.Select(c => $"{c.x}x{c.y}").ToArray();
|
private static string[] resolutionsStrings => resolutions.Select(c => $"{c.x}x{c.y}").ToArray();
|
||||||
|
|
||||||
[MenuItem("Window/VRAM Editor")]
|
[MenuItem("PlayStation 1/VRAM Editor")]
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
|
VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
|
||||||
@@ -299,11 +299,5 @@ namespace SplashEdit.EditorCode
|
|||||||
|
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores current configuration to the PSX data asset.
|
|
||||||
/// This is now triggered manually via the "Save Settings" button.
|
|
||||||
/// </summary>
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "net.psxplash.splashedit.Editor",
|
"name": "net.psxsplash.splashedit.Editor",
|
||||||
"rootNamespace": "",
|
"rootNamespace": "",
|
||||||
"references": [
|
"references": [
|
||||||
"net.psxsplash.splashedit.Runtime"
|
"net.psxsplash.splashedit.Runtime",
|
||||||
|
"Unity.AI.Navigation"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
"allowUnsafeCode": false,
|
"allowUnsafeCode": false,
|
||||||
"overrideReferences": false,
|
"overrideReferences": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7e3500b5974da9723bdd0d457348ea2d
|
guid: 8bf64a45e6e447140a68258cd60d0ec1
|
||||||
AssemblyDefinitionImporter:
|
AssemblyDefinitionImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: ab7e1dbd79d3e1101b7d44cdf06a2991
|
guid: f1210e43ecf5c354486bc01af97ba9eb
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: d695ef52da250cdcea6c30ab1122c56e
|
guid: 55ec05596eb659341b8fdb46cd21ab63
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 0
|
enableMipMap: 1
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 1
|
wrapU: 0
|
||||||
wrapV: 1
|
wrapV: 0
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 0
|
nPOTScale: 1
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 1
|
alphaIsTransparency: 0
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 2
|
textureType: 0
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,7 +94,20 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: WebGL
|
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 4d7bd095e76e6f3df976224b15405059
|
guid: 67d31a8682e9646419734c5412403992
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 0
|
enableMipMap: 1
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 1
|
wrapU: 0
|
||||||
wrapV: 1
|
wrapV: 0
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 0
|
nPOTScale: 1
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 1
|
alphaIsTransparency: 0
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 2
|
textureType: 0
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,7 +94,20 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: WebGL
|
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: e11677149a517ca5186e32dfda3ec088
|
guid: 5a5f4bcf472dcfc44b794a898530a6f0
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 0
|
enableMipMap: 1
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 1
|
wrapU: 0
|
||||||
wrapV: 1
|
wrapV: 0
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 0
|
nPOTScale: 1
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 1
|
alphaIsTransparency: 0
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 2
|
textureType: 0
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,7 +94,20 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: WebGL
|
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 0be7a2d4700082dbc83b9274837c70bc
|
guid: 44fe191eb81c12e4698ab9e87406b878
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 0
|
enableMipMap: 1
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 1
|
wrapU: 0
|
||||||
wrapV: 1
|
wrapV: 0
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 0
|
nPOTScale: 1
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 1
|
alphaIsTransparency: 0
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 2
|
textureType: 0
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,7 +94,20 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: WebGL
|
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: c1679c9d58898f14494d614dfe5f76a6
|
guid: 07933442bdb4ee14f83fd0b0d0144b8a
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|||||||
8
Plugins.meta
Normal file
8
Plugins.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Plugins/DotRecast.meta
Normal file
8
Plugins/DotRecast.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Plugins/DotRecast/DotRecast.Core.dll
LFS
Normal file
BIN
Plugins/DotRecast/DotRecast.Core.dll
LFS
Normal file
Binary file not shown.
27
Plugins/DotRecast/DotRecast.Core.dll.meta
Normal file
27
Plugins/DotRecast/DotRecast.Core.dll.meta
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8
|
||||||
|
PluginImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
iconMap: {}
|
||||||
|
executionOrder: {}
|
||||||
|
defineConstraints: []
|
||||||
|
isPreloaded: 0
|
||||||
|
isOverridable: 0
|
||||||
|
isExplicitlyReferenced: 0
|
||||||
|
validateReferences: 1
|
||||||
|
platformData:
|
||||||
|
- first:
|
||||||
|
Any:
|
||||||
|
second:
|
||||||
|
enabled: 1
|
||||||
|
settings: {}
|
||||||
|
- first:
|
||||||
|
Editor: Editor
|
||||||
|
second:
|
||||||
|
enabled: 1
|
||||||
|
settings:
|
||||||
|
DefaultValueInitialized: true
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Plugins/DotRecast/DotRecast.Recast.dll
LFS
Normal file
BIN
Plugins/DotRecast/DotRecast.Recast.dll
LFS
Normal file
Binary file not shown.
27
Plugins/DotRecast/DotRecast.Recast.dll.meta
Normal file
27
Plugins/DotRecast/DotRecast.Recast.dll.meta
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
|
||||||
|
PluginImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
iconMap: {}
|
||||||
|
executionOrder: {}
|
||||||
|
defineConstraints: []
|
||||||
|
isPreloaded: 0
|
||||||
|
isOverridable: 0
|
||||||
|
isExplicitlyReferenced: 0
|
||||||
|
validateReferences: 1
|
||||||
|
platformData:
|
||||||
|
- first:
|
||||||
|
Any:
|
||||||
|
second:
|
||||||
|
enabled: 1
|
||||||
|
settings: {}
|
||||||
|
- first:
|
||||||
|
Editor: Editor
|
||||||
|
second:
|
||||||
|
enabled: 1
|
||||||
|
settings:
|
||||||
|
DefaultValueInitialized: true
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
10
README.md
10
README.md
@@ -52,13 +52,11 @@ There are currently four custom Unity components for exporting your scenes to th
|
|||||||
|
|
||||||
**PSX Player**: Attach this to any GameObject that you want to act as the player. For now, it acts as a FPS / Free Camera template. The analogue does work for it if you have the option specified in your choice emulator.
|
**PSX Player**: Attach this to any GameObject that you want to act as the player. For now, it acts as a FPS / Free Camera template. The analogue does work for it if you have the option specified in your choice emulator.
|
||||||
|
|
||||||
**PSX Nav Mesh**: Attach this to any GameObject that you want to apply collisions to. These collisions are rudimentary and currently only work in a top-down view. There must be at least one existing GameObject that uses the **PSX Player** component for this to work properly.
|
|
||||||
|
|
||||||
## Before Exporting:
|
## Before Exporting:
|
||||||
|
|
||||||
Check the following:
|
Check the following:
|
||||||
|
|
||||||
**Player and Nav Meshes**: In case it wasn't clear already, a **PSX Player** component and **PSX Nav Mesh** component must coexist in the same scene for the player to walk on the collisions properly - as both rely on eachother to work.
|
**Player and Navigation**: A **PSX Player** component must exist in the scene. Navigation is handled by the Nav Region system, which is built automatically from objects with ``Generate Navigation`` enabled in their **PSX Object Exporter** settings.
|
||||||
|
|
||||||
**Scaling**: Ensure that your Geometry Transformation Engine (or GTE) scaling is set to a reasonable value, which can be found in your GameObject holding the component. Ideally, each object in your scene should be within the GTE bounds (or red wireframe bounding box). Bandwidth does mention that there is a scaling / overflow bug with Nav Meshes being worked on and the way to circumvent this is by scaling up the GTE value to be higher.
|
**Scaling**: Ensure that your Geometry Transformation Engine (or GTE) scaling is set to a reasonable value, which can be found in your GameObject holding the component. Ideally, each object in your scene should be within the GTE bounds (or red wireframe bounding box). Bandwidth does mention that there is a scaling / overflow bug with Nav Meshes being worked on and the way to circumvent this is by scaling up the GTE value to be higher.
|
||||||
|
|
||||||
@@ -76,11 +74,11 @@ Convert and preview your textures in a PSX-compatible format.
|
|||||||
**Light Baking**:\
|
**Light Baking**:\
|
||||||
Prebakes any lights in the scene into the vertex colors of the nearest triangles in the exported mesh data.
|
Prebakes any lights in the scene into the vertex colors of the nearest triangles in the exported mesh data.
|
||||||
|
|
||||||
**Nav Mesh Generation**:\
|
**Nav Region Generation**:\
|
||||||
Generates a new Nav Mesh for FPS movement in your scene.
|
Automatically builds convex navigation regions from walkable surfaces for efficient player movement and floor resolution.
|
||||||
|
|
||||||
**FPS / Free Camera Template**:\
|
**FPS / Free Camera Template**:\
|
||||||
Allows you to walk around in your scene or view it from different perspectives. It's really just the way **PSX Player** and **PSX Nav Mesh** components are setup right now.
|
Allows you to walk around in your scene or view it from different perspectives using the **PSX Player** component.
|
||||||
|
|
||||||
## Additional Features:
|
## Additional Features:
|
||||||
**VRAM Editor**:
|
**VRAM Editor**:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 4df40ce535b32f3a4b30ce0803fa699a
|
guid: 589dcafb532c388449644bfeb4cf5178
|
||||||
TextScriptImporter:
|
TextScriptImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 49f302f0a6255c06c8e340024aa43f45
|
guid: 383aa9702d932194ea91e4f3488b41e2
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|||||||
393
Runtime/BVH.cs
Normal file
393
Runtime/BVH.cs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bounding Volume Hierarchy for PS1 frustum culling.
|
||||||
|
/// Unlike BSP, BVH doesn't split triangles - it groups them by spatial locality.
|
||||||
|
/// This is better for PS1 because:
|
||||||
|
/// 1. No additional triangles created (memory constrained)
|
||||||
|
/// 2. Simple AABB tests are fast on 33MHz CPU
|
||||||
|
/// 3. Natural hierarchical culling
|
||||||
|
/// </summary>
|
||||||
|
public class BVH : IPSXBinaryWritable
|
||||||
|
{
|
||||||
|
// Configuration
|
||||||
|
private const int MAX_TRIANGLES_PER_LEAF = 64; // PS1 can handle batches of this size
|
||||||
|
private const int MAX_DEPTH = 16; // Prevent pathological cases
|
||||||
|
private const int MIN_TRIANGLES_TO_SPLIT = 8; // Don't split tiny groups
|
||||||
|
|
||||||
|
private List<PSXObjectExporter> _objects;
|
||||||
|
private BVHNode _root;
|
||||||
|
private List<BVHNode> _allNodes; // Flat list for export
|
||||||
|
private List<TriangleRef> _allTriangleRefs; // Triangle references for export
|
||||||
|
|
||||||
|
public int NodeCount => _allNodes?.Count ?? 0;
|
||||||
|
public int TriangleRefCount => _allTriangleRefs?.Count ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to a triangle - doesn't copy data, just points to it
|
||||||
|
/// </summary>
|
||||||
|
public struct TriangleRef
|
||||||
|
{
|
||||||
|
public ushort objectIndex; // Which GameObject
|
||||||
|
public ushort triangleIndex; // Which triangle in that object's mesh
|
||||||
|
|
||||||
|
public TriangleRef(int objIdx, int triIdx)
|
||||||
|
{
|
||||||
|
objectIndex = (ushort)objIdx;
|
||||||
|
triangleIndex = (ushort)triIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BVH Node - 32 bytes when exported
|
||||||
|
/// </summary>
|
||||||
|
public class BVHNode
|
||||||
|
{
|
||||||
|
public Bounds bounds;
|
||||||
|
public BVHNode left;
|
||||||
|
public BVHNode right;
|
||||||
|
public List<TriangleRef> triangles; // Only for leaf nodes
|
||||||
|
public int depth;
|
||||||
|
|
||||||
|
// Export indices (filled during serialization)
|
||||||
|
public int nodeIndex = -1;
|
||||||
|
public int leftIndex = -1; // -1 = no child (leaf check)
|
||||||
|
public int rightIndex = -1;
|
||||||
|
public int firstTriangleIndex = -1;
|
||||||
|
public int triangleCount = 0;
|
||||||
|
|
||||||
|
public bool IsLeaf => left == null && right == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triangle with bounds for building
|
||||||
|
/// </summary>
|
||||||
|
private struct TriangleWithBounds
|
||||||
|
{
|
||||||
|
public TriangleRef reference;
|
||||||
|
public Bounds bounds;
|
||||||
|
public Vector3 centroid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BVH(List<PSXObjectExporter> objects)
|
||||||
|
{
|
||||||
|
_objects = objects;
|
||||||
|
_allNodes = new List<BVHNode>();
|
||||||
|
_allTriangleRefs = new List<TriangleRef>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Build()
|
||||||
|
{
|
||||||
|
_allNodes.Clear();
|
||||||
|
_allTriangleRefs.Clear();
|
||||||
|
|
||||||
|
// Extract all triangles with their bounds
|
||||||
|
List<TriangleWithBounds> triangles = ExtractTriangles();
|
||||||
|
|
||||||
|
if (triangles.Count == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("BVH: No triangles to process");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the tree
|
||||||
|
_root = BuildNode(triangles, 0);
|
||||||
|
|
||||||
|
// Flatten for export
|
||||||
|
FlattenTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TriangleWithBounds> ExtractTriangles()
|
||||||
|
{
|
||||||
|
var result = new List<TriangleWithBounds>();
|
||||||
|
|
||||||
|
for (int objIdx = 0; objIdx < _objects.Count; objIdx++)
|
||||||
|
{
|
||||||
|
var exporter = _objects[objIdx];
|
||||||
|
if (!exporter.IsActive) continue;
|
||||||
|
|
||||||
|
MeshFilter mf = exporter.GetComponent<MeshFilter>();
|
||||||
|
if (mf == null || mf.sharedMesh == null) continue;
|
||||||
|
|
||||||
|
Mesh mesh = mf.sharedMesh;
|
||||||
|
Vector3[] vertices = mesh.vertices;
|
||||||
|
int[] indices = mesh.triangles;
|
||||||
|
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
|
||||||
|
|
||||||
|
for (int i = 0; i < indices.Length; i += 3)
|
||||||
|
{
|
||||||
|
Vector3 v0 = worldMatrix.MultiplyPoint3x4(vertices[indices[i]]);
|
||||||
|
Vector3 v1 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 1]]);
|
||||||
|
Vector3 v2 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 2]]);
|
||||||
|
|
||||||
|
// Calculate bounds
|
||||||
|
Bounds triBounds = new Bounds(v0, Vector3.zero);
|
||||||
|
triBounds.Encapsulate(v1);
|
||||||
|
triBounds.Encapsulate(v2);
|
||||||
|
|
||||||
|
result.Add(new TriangleWithBounds
|
||||||
|
{
|
||||||
|
reference = new TriangleRef(objIdx, i / 3),
|
||||||
|
bounds = triBounds,
|
||||||
|
centroid = (v0 + v1 + v2) / 3f
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BVHNode BuildNode(List<TriangleWithBounds> triangles, int depth)
|
||||||
|
{
|
||||||
|
if (triangles.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var node = new BVHNode { depth = depth };
|
||||||
|
|
||||||
|
// Calculate bounds encompassing all triangles
|
||||||
|
node.bounds = triangles[0].bounds;
|
||||||
|
foreach (var tri in triangles)
|
||||||
|
{
|
||||||
|
node.bounds.Encapsulate(tri.bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create leaf if conditions met
|
||||||
|
if (triangles.Count <= MAX_TRIANGLES_PER_LEAF ||
|
||||||
|
depth >= MAX_DEPTH ||
|
||||||
|
triangles.Count < MIN_TRIANGLES_TO_SPLIT)
|
||||||
|
{
|
||||||
|
node.triangles = triangles.Select(t => t.reference).ToList();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find best split axis (longest extent)
|
||||||
|
Vector3 extent = node.bounds.size;
|
||||||
|
int axis = 0;
|
||||||
|
if (extent.y > extent.x && extent.y > extent.z) axis = 1;
|
||||||
|
else if (extent.z > extent.x && extent.z > extent.y) axis = 2;
|
||||||
|
|
||||||
|
// Sort by centroid along chosen axis
|
||||||
|
triangles.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
float va = axis == 0 ? a.centroid.x : (axis == 1 ? a.centroid.y : a.centroid.z);
|
||||||
|
float vb = axis == 0 ? b.centroid.x : (axis == 1 ? b.centroid.y : b.centroid.z);
|
||||||
|
return va.CompareTo(vb);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find split plane position at median centroid
|
||||||
|
int mid = triangles.Count / 2;
|
||||||
|
if (mid == 0) mid = 1;
|
||||||
|
if (mid >= triangles.Count) mid = triangles.Count - 1;
|
||||||
|
|
||||||
|
float splitPos = axis == 0 ? triangles[mid].centroid.x :
|
||||||
|
(axis == 1 ? triangles[mid].centroid.y : triangles[mid].centroid.z);
|
||||||
|
|
||||||
|
// Partition triangles - allow overlap for triangles spanning the split plane
|
||||||
|
var leftTris = new List<TriangleWithBounds>();
|
||||||
|
var rightTris = new List<TriangleWithBounds>();
|
||||||
|
|
||||||
|
foreach (var tri in triangles)
|
||||||
|
{
|
||||||
|
float triMin = axis == 0 ? tri.bounds.min.x : (axis == 1 ? tri.bounds.min.y : tri.bounds.min.z);
|
||||||
|
float triMax = axis == 0 ? tri.bounds.max.x : (axis == 1 ? tri.bounds.max.y : tri.bounds.max.z);
|
||||||
|
|
||||||
|
// Triangle spans split plane - add to BOTH children (spatial split)
|
||||||
|
// This fixes large triangles at screen edges being culled incorrectly
|
||||||
|
if (triMin < splitPos && triMax > splitPos)
|
||||||
|
{
|
||||||
|
leftTris.Add(tri);
|
||||||
|
rightTris.Add(tri);
|
||||||
|
}
|
||||||
|
// Triangle entirely on left side
|
||||||
|
else if (triMax <= splitPos)
|
||||||
|
{
|
||||||
|
leftTris.Add(tri);
|
||||||
|
}
|
||||||
|
// Triangle entirely on right side
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rightTris.Add(tri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if split is beneficial (prevents infinite recursion on coincident triangles)
|
||||||
|
if (leftTris.Count == 0 || rightTris.Count == 0 ||
|
||||||
|
(leftTris.Count == triangles.Count && rightTris.Count == triangles.Count))
|
||||||
|
{
|
||||||
|
node.triangles = triangles.Select(t => t.reference).ToList();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.left = BuildNode(leftTris, depth + 1);
|
||||||
|
node.right = BuildNode(rightTris, depth + 1);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flatten tree to arrays for export
|
||||||
|
/// </summary>
|
||||||
|
private void FlattenTree()
|
||||||
|
{
|
||||||
|
_allNodes.Clear();
|
||||||
|
_allTriangleRefs.Clear();
|
||||||
|
|
||||||
|
if (_root == null) return;
|
||||||
|
|
||||||
|
// BFS to assign indices
|
||||||
|
var queue = new Queue<BVHNode>();
|
||||||
|
queue.Enqueue(_root);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var node = queue.Dequeue();
|
||||||
|
node.nodeIndex = _allNodes.Count;
|
||||||
|
_allNodes.Add(node);
|
||||||
|
|
||||||
|
if (node.left != null) queue.Enqueue(node.left);
|
||||||
|
if (node.right != null) queue.Enqueue(node.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: fill in child indices and triangle data
|
||||||
|
foreach (var node in _allNodes)
|
||||||
|
{
|
||||||
|
if (node.left != null)
|
||||||
|
node.leftIndex = node.left.nodeIndex;
|
||||||
|
if (node.right != null)
|
||||||
|
node.rightIndex = node.right.nodeIndex;
|
||||||
|
|
||||||
|
if (node.IsLeaf && node.triangles != null && node.triangles.Count > 0)
|
||||||
|
{
|
||||||
|
// Sort tri-refs by objectIndex within each leaf so the C++ renderer
|
||||||
|
// can batch consecutive refs and avoid redundant GTE matrix reloads.
|
||||||
|
node.triangles.Sort((a, b) => a.objectIndex.CompareTo(b.objectIndex));
|
||||||
|
node.firstTriangleIndex = _allTriangleRefs.Count;
|
||||||
|
node.triangleCount = node.triangles.Count;
|
||||||
|
_allTriangleRefs.AddRange(node.triangles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export BVH to binary writer
|
||||||
|
/// Format:
|
||||||
|
/// - uint16 nodeCount
|
||||||
|
/// - uint16 triangleRefCount
|
||||||
|
/// - BVHNode[nodeCount] (32 bytes each)
|
||||||
|
/// - TriangleRef[triangleRefCount] (4 bytes each)
|
||||||
|
/// </summary>
|
||||||
|
public void WriteToBinary(BinaryWriter writer, float gteScaling)
|
||||||
|
{
|
||||||
|
// Note: counts are already in the file header (bvhNodeCount, bvhTriangleRefCount)
|
||||||
|
// Don't write them again here - C++ reads BVH data directly after colliders
|
||||||
|
|
||||||
|
// Write nodes (32 bytes each)
|
||||||
|
foreach (var node in _allNodes)
|
||||||
|
{
|
||||||
|
// AABB bounds (24 bytes)
|
||||||
|
Vector3 min = node.bounds.min;
|
||||||
|
Vector3 max = node.bounds.max;
|
||||||
|
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(min.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-max.y / gteScaling)); // Y flipped
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(min.z / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(max.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-min.y / gteScaling)); // Y flipped
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(max.z / gteScaling));
|
||||||
|
|
||||||
|
// Child indices (4 bytes) - 0xFFFF means no child
|
||||||
|
writer.Write((ushort)(node.leftIndex >= 0 ? node.leftIndex : 0xFFFF));
|
||||||
|
writer.Write((ushort)(node.rightIndex >= 0 ? node.rightIndex : 0xFFFF));
|
||||||
|
|
||||||
|
// Triangle data (4 bytes)
|
||||||
|
writer.Write((ushort)(node.firstTriangleIndex >= 0 ? node.firstTriangleIndex : 0));
|
||||||
|
writer.Write((ushort)node.triangleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write triangle references (4 bytes each)
|
||||||
|
foreach (var triRef in _allTriangleRefs)
|
||||||
|
{
|
||||||
|
writer.Write(triRef.objectIndex);
|
||||||
|
writer.Write(triRef.triangleIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get total bytes that will be written
|
||||||
|
/// </summary>
|
||||||
|
public int GetBinarySize()
|
||||||
|
{
|
||||||
|
// Just nodes + triangle refs, counts are in file header
|
||||||
|
return (_allNodes.Count * 32) + (_allTriangleRefs.Count * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw gizmos for debugging
|
||||||
|
/// </summary>
|
||||||
|
public void DrawGizmos(int maxDepth = 999)
|
||||||
|
{
|
||||||
|
if (_root == null) return;
|
||||||
|
DrawNodeGizmos(_root, maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNodeGizmos(BVHNode node, int maxDepth)
|
||||||
|
{
|
||||||
|
if (node == null || node.depth > maxDepth) return;
|
||||||
|
|
||||||
|
// Color by depth
|
||||||
|
Color c = Color.HSVToRGB((node.depth * 0.12f) % 1f, 0.7f, 0.9f);
|
||||||
|
c.a = node.IsLeaf ? 0.3f : 0.1f;
|
||||||
|
Gizmos.color = c;
|
||||||
|
|
||||||
|
// Draw bounds
|
||||||
|
Gizmos.DrawWireCube(node.bounds.center, node.bounds.size);
|
||||||
|
|
||||||
|
if (node.IsLeaf)
|
||||||
|
{
|
||||||
|
// Draw leaf as semi-transparent
|
||||||
|
Gizmos.color = new Color(c.r, c.g, c.b, 0.1f);
|
||||||
|
Gizmos.DrawCube(node.bounds.center, node.bounds.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse
|
||||||
|
DrawNodeGizmos(node.left, maxDepth);
|
||||||
|
DrawNodeGizmos(node.right, maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get statistics for debugging
|
||||||
|
/// </summary>
|
||||||
|
public string GetStatistics()
|
||||||
|
{
|
||||||
|
if (_root == null) return "BVH not built";
|
||||||
|
|
||||||
|
int leafCount = 0;
|
||||||
|
int maxDepth = 0;
|
||||||
|
int totalTris = 0;
|
||||||
|
|
||||||
|
void CountNodes(BVHNode node)
|
||||||
|
{
|
||||||
|
if (node == null) return;
|
||||||
|
if (node.depth > maxDepth) maxDepth = node.depth;
|
||||||
|
if (node.IsLeaf)
|
||||||
|
{
|
||||||
|
leafCount++;
|
||||||
|
totalTris += node.triangleCount;
|
||||||
|
}
|
||||||
|
CountNodes(node.left);
|
||||||
|
CountNodes(node.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
CountNodes(_root);
|
||||||
|
|
||||||
|
return $"Nodes: {_allNodes.Count}, Leaves: {leafCount}, Max Depth: {maxDepth}, Triangle Refs: {totalTris}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/BVH.cs.meta
Normal file
2
Runtime/BVH.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 735c7edec8b9f5d4facdf22f48d99ee0
|
||||||
8
Runtime/Core.meta
Normal file
8
Runtime/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 90864d7c8ee7ae6409c8a0c0a2ea9075
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
18
Runtime/IPSXBinaryWritable.cs
Normal file
18
Runtime/IPSXBinaryWritable.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by scene-level data builders that serialize their output
|
||||||
|
/// into the splashpack binary stream.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPSXBinaryWritable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Write binary data to the splashpack stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">The binary writer positioned at the correct offset.</param>
|
||||||
|
/// <param name="gteScaling">GTE coordinate scaling factor.</param>
|
||||||
|
void WriteToBinary(BinaryWriter writer, float gteScaling);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/IPSXBinaryWritable.cs.meta
Normal file
2
Runtime/IPSXBinaryWritable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aa53647f0fc3ed24292dd3fd9a0b294e
|
||||||
20
Runtime/IPSXExportable.cs
Normal file
20
Runtime/IPSXExportable.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by MonoBehaviours that participate in the PSX scene export pipeline.
|
||||||
|
/// Each exportable object converts its Unity representation into PSX-ready data.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPSXExportable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Convert Unity textures into PSX texture data (palette-quantized, packed).
|
||||||
|
/// </summary>
|
||||||
|
void CreatePSXTextures2D();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert the Unity mesh into a PSX-ready triangle list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="gteScaling">GTE coordinate scaling factor.</param>
|
||||||
|
void CreatePSXMesh(float gteScaling);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/IPSXExportable.cs.meta
Normal file
2
Runtime/IPSXExportable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0598c601ee3672b40828f0d31bbec29b
|
||||||
@@ -70,7 +70,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
List<Vector3> centroids = Enumerable.Range(0, k).Select(i => colors[i * colors.Count / k]).ToList();
|
List<Vector3> centroids = Enumerable.Range(0, k).Select(i => colors[i * colors.Count / k]).ToList();
|
||||||
|
|
||||||
List<List<Vector3>> clusters;
|
List<List<Vector3>> clusters;
|
||||||
for (int i = 0; i < 10; i++) // Fixed iterations for performance.... i hate this...
|
for (int i = 0; i < 10; i++) // Fixed iteration count
|
||||||
{
|
{
|
||||||
clusters = Enumerable.Range(0, k).Select(_ => new List<Vector3>()).ToList();
|
clusters = Enumerable.Range(0, k).Select(_ => new List<Vector3>()).ToList();
|
||||||
foreach (Vector3 color in colors)
|
foreach (Vector3 color in colors)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 1291c85b333132b8392486949420d31a
|
guid: c760e5745d5c72746aec8ac9583c456f
|
||||||
16
Runtime/LuaFile.cs
Normal file
16
Runtime/LuaFile.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
public class LuaFile : ScriptableObject
|
||||||
|
{
|
||||||
|
[SerializeField] private string luaScript;
|
||||||
|
public string LuaScript => luaScript;
|
||||||
|
|
||||||
|
public void Init(string luaCode)
|
||||||
|
{
|
||||||
|
luaScript = luaCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2
Runtime/LuaFile.cs.meta
Normal file
2
Runtime/LuaFile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 00e64fcbfc4e23e4dbe284131fa4d89b
|
||||||
43
Runtime/PSXAudioSource.cs
Normal file
43
Runtime/PSXAudioSource.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-converted audio clip data ready for splashpack serialization.
|
||||||
|
/// Populated by the Editor (PSXSceneExporter) so Runtime code never
|
||||||
|
/// touches PSXAudioConverter.
|
||||||
|
/// </summary>
|
||||||
|
public struct AudioClipExport
|
||||||
|
{
|
||||||
|
public byte[] adpcmData;
|
||||||
|
public int sampleRate;
|
||||||
|
public bool loop;
|
||||||
|
public string clipName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attach to a GameObject to include an audio clip in the PS1 build.
|
||||||
|
/// At export time, the AudioClip is converted to SPU ADPCM and packed
|
||||||
|
/// into the splashpack binary. Use Audio.Play(clipIndex) from Lua.
|
||||||
|
/// </summary>
|
||||||
|
[AddComponentMenu("PSX/Audio Source")]
|
||||||
|
public class PSXAudioSource : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Name used to identify this clip in Lua (Audio.Play(\"name\"))." )]
|
||||||
|
public string ClipName = "";
|
||||||
|
|
||||||
|
[Tooltip("Unity AudioClip to convert to PS1 SPU ADPCM format.")]
|
||||||
|
public AudioClip Clip;
|
||||||
|
|
||||||
|
[Tooltip("Target sample rate for the PS1 (lower = smaller, max 44100).")]
|
||||||
|
[Range(8000, 44100)]
|
||||||
|
public int SampleRate = 22050;
|
||||||
|
|
||||||
|
[Tooltip("Whether this clip should loop when played.")]
|
||||||
|
public bool Loop = false;
|
||||||
|
|
||||||
|
[Tooltip("Default playback volume (0-127).")]
|
||||||
|
[Range(0, 127)]
|
||||||
|
public int DefaultVolume = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXAudioSource.cs.meta
Normal file
2
Runtime/PSXAudioSource.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3c4c3feb30e8c264baddc3a5e774473b
|
||||||
357
Runtime/PSXCollisionExporter.cs
Normal file
357
Runtime/PSXCollisionExporter.cs
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Surface flags for collision triangles — must match C++ SurfaceFlag enum
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum PSXSurfaceFlag : byte
|
||||||
|
{
|
||||||
|
Solid = 0x01,
|
||||||
|
Slope = 0x02,
|
||||||
|
Stairs = 0x04,
|
||||||
|
Trigger = 0x08,
|
||||||
|
NoWalk = 0x10,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports scene collision geometry as a flat world-space triangle soup
|
||||||
|
/// with per-triangle surface flags and world-space AABBs.
|
||||||
|
///
|
||||||
|
/// Binary layout (matches C++ structs):
|
||||||
|
/// CollisionDataHeader (20 bytes)
|
||||||
|
/// CollisionMeshHeader[meshCount] (32 bytes each)
|
||||||
|
/// CollisionTri[triangleCount] (52 bytes each)
|
||||||
|
/// CollisionChunk[chunkW*chunkH] (4 bytes each, exterior only)
|
||||||
|
/// </summary>
|
||||||
|
public class PSXCollisionExporter : IPSXBinaryWritable
|
||||||
|
{
|
||||||
|
// Configurable
|
||||||
|
public float WalkableSlopeAngle = 46.0f; // Degrees; steeper = wall
|
||||||
|
|
||||||
|
// Build results
|
||||||
|
private List<CollisionMesh> _meshes = new List<CollisionMesh>();
|
||||||
|
private List<CollisionTriExport> _allTriangles = new List<CollisionTriExport>();
|
||||||
|
private CollisionChunkExport[,] _chunks;
|
||||||
|
private Vector3 _chunkOrigin;
|
||||||
|
private float _chunkSize;
|
||||||
|
private int _chunkGridW, _chunkGridH;
|
||||||
|
|
||||||
|
public int MeshCount => _meshes.Count;
|
||||||
|
public int TriangleCount => _allTriangles.Count;
|
||||||
|
|
||||||
|
// Internal types
|
||||||
|
private class CollisionMesh
|
||||||
|
{
|
||||||
|
public Bounds worldAABB;
|
||||||
|
public int firstTriangle;
|
||||||
|
public int triangleCount;
|
||||||
|
public byte roomIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CollisionTriExport
|
||||||
|
{
|
||||||
|
public Vector3 v0, e1, e2, normal;
|
||||||
|
public byte flags;
|
||||||
|
public byte roomIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CollisionChunkExport
|
||||||
|
{
|
||||||
|
public int firstMeshIndex;
|
||||||
|
public int meshCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build collision data from scene exporters.
|
||||||
|
/// When autoIncludeSolid is true, objects with CollisionType=None are
|
||||||
|
/// automatically treated as Solid. This ensures all scene geometry
|
||||||
|
/// blocks the player without requiring manual flagging.
|
||||||
|
/// </summary>
|
||||||
|
public void Build(PSXObjectExporter[] exporters, float gteScaling,
|
||||||
|
bool autoIncludeSolid = true)
|
||||||
|
{
|
||||||
|
_meshes.Clear();
|
||||||
|
_allTriangles.Clear();
|
||||||
|
|
||||||
|
float cosWalkable = Mathf.Cos(WalkableSlopeAngle * Mathf.Deg2Rad);
|
||||||
|
int autoIncluded = 0;
|
||||||
|
|
||||||
|
foreach (var exporter in exporters)
|
||||||
|
{
|
||||||
|
PSXCollisionType effectiveType = exporter.CollisionType;
|
||||||
|
|
||||||
|
if (effectiveType == PSXCollisionType.None)
|
||||||
|
{
|
||||||
|
if (autoIncludeSolid)
|
||||||
|
{
|
||||||
|
// Auto-include as Solid so all geometry blocks the player
|
||||||
|
effectiveType = PSXCollisionType.Solid;
|
||||||
|
autoIncluded++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the collision mesh (custom or render mesh)
|
||||||
|
MeshFilter mf = exporter.GetComponent<MeshFilter>();
|
||||||
|
Mesh collisionMesh = exporter.CustomCollisionMesh != null
|
||||||
|
? exporter.CustomCollisionMesh
|
||||||
|
: mf?.sharedMesh;
|
||||||
|
|
||||||
|
if (collisionMesh == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
|
||||||
|
Vector3[] vertices = collisionMesh.vertices;
|
||||||
|
int[] indices = collisionMesh.triangles;
|
||||||
|
|
||||||
|
int firstTri = _allTriangles.Count;
|
||||||
|
Bounds meshBoundsWorld = new Bounds();
|
||||||
|
bool boundsInit = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < indices.Length; i += 3)
|
||||||
|
{
|
||||||
|
Vector3 v0 = worldMatrix.MultiplyPoint3x4(vertices[indices[i]]);
|
||||||
|
Vector3 v1 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 1]]);
|
||||||
|
Vector3 v2 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 2]]);
|
||||||
|
|
||||||
|
Vector3 edge1 = v1 - v0;
|
||||||
|
Vector3 edge2 = v2 - v0;
|
||||||
|
Vector3 normal = Vector3.Cross(edge1, edge2).normalized;
|
||||||
|
|
||||||
|
// Determine surface flags
|
||||||
|
byte flags = 0;
|
||||||
|
|
||||||
|
if (effectiveType == PSXCollisionType.Trigger)
|
||||||
|
{
|
||||||
|
flags = (byte)PSXSurfaceFlag.Trigger;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Floor-like: normal.y > cosWalkable
|
||||||
|
// Note: Unity Y is up; PS1 Y is down. We export in Unity space
|
||||||
|
// and convert to PS1 space during WriteToBinary.
|
||||||
|
float dotUp = normal.y;
|
||||||
|
|
||||||
|
if (dotUp > cosWalkable)
|
||||||
|
{
|
||||||
|
flags = (byte)PSXSurfaceFlag.Solid;
|
||||||
|
|
||||||
|
// Check if stairs (tagged on exporter or steep-ish)
|
||||||
|
if (exporter.ObjectFlags.HasFlag(PSXObjectFlags.Static) &&
|
||||||
|
dotUp < 0.95f && dotUp > cosWalkable)
|
||||||
|
{
|
||||||
|
flags |= (byte)PSXSurfaceFlag.Stairs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dotUp > 0.0f)
|
||||||
|
{
|
||||||
|
// Slope too steep to walk on
|
||||||
|
flags = (byte)(PSXSurfaceFlag.Solid | PSXSurfaceFlag.Slope);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Wall or ceiling
|
||||||
|
flags = (byte)PSXSurfaceFlag.Solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_allTriangles.Add(new CollisionTriExport
|
||||||
|
{
|
||||||
|
v0 = v0,
|
||||||
|
e1 = edge1,
|
||||||
|
e2 = edge2,
|
||||||
|
normal = normal,
|
||||||
|
flags = flags,
|
||||||
|
roomIndex = 0xFF,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update world bounds
|
||||||
|
if (!boundsInit)
|
||||||
|
{
|
||||||
|
meshBoundsWorld = new Bounds(v0, Vector3.zero);
|
||||||
|
boundsInit = true;
|
||||||
|
}
|
||||||
|
meshBoundsWorld.Encapsulate(v0);
|
||||||
|
meshBoundsWorld.Encapsulate(v1);
|
||||||
|
meshBoundsWorld.Encapsulate(v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
int triCount = _allTriangles.Count - firstTri;
|
||||||
|
if (triCount > 0)
|
||||||
|
{
|
||||||
|
_meshes.Add(new CollisionMesh
|
||||||
|
{
|
||||||
|
worldAABB = meshBoundsWorld,
|
||||||
|
firstTriangle = firstTri,
|
||||||
|
triangleCount = triCount,
|
||||||
|
roomIndex = 0xFF,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build spatial grid
|
||||||
|
if (_meshes.Count > 0)
|
||||||
|
{
|
||||||
|
BuildSpatialGrid(gteScaling);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_chunkGridW = 0;
|
||||||
|
_chunkGridH = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSpatialGrid(float gteScaling)
|
||||||
|
{
|
||||||
|
// Compute world bounds of all collision
|
||||||
|
Bounds allBounds = _meshes[0].worldAABB;
|
||||||
|
foreach (var mesh in _meshes)
|
||||||
|
allBounds.Encapsulate(mesh.worldAABB);
|
||||||
|
|
||||||
|
// Grid cell size: ~4 GTE units in world space
|
||||||
|
_chunkSize = 4.0f * gteScaling;
|
||||||
|
_chunkOrigin = new Vector3(allBounds.min.x, 0, allBounds.min.z);
|
||||||
|
|
||||||
|
_chunkGridW = Mathf.CeilToInt((allBounds.max.x - allBounds.min.x) / _chunkSize);
|
||||||
|
_chunkGridH = Mathf.CeilToInt((allBounds.max.z - allBounds.min.z) / _chunkSize);
|
||||||
|
|
||||||
|
// Clamp to reasonable limits
|
||||||
|
_chunkGridW = Mathf.Clamp(_chunkGridW, 1, 64);
|
||||||
|
_chunkGridH = Mathf.Clamp(_chunkGridH, 1, 64);
|
||||||
|
|
||||||
|
// For each chunk, find which meshes overlap it
|
||||||
|
// We store mesh indices sorted per chunk
|
||||||
|
var chunkMeshLists = new List<int>[_chunkGridW, _chunkGridH];
|
||||||
|
for (int z = 0; z < _chunkGridH; z++)
|
||||||
|
for (int x = 0; x < _chunkGridW; x++)
|
||||||
|
chunkMeshLists[x, z] = new List<int>();
|
||||||
|
|
||||||
|
for (int mi = 0; mi < _meshes.Count; mi++)
|
||||||
|
{
|
||||||
|
var mesh = _meshes[mi];
|
||||||
|
int minCX = Mathf.FloorToInt((mesh.worldAABB.min.x - _chunkOrigin.x) / _chunkSize);
|
||||||
|
int maxCX = Mathf.FloorToInt((mesh.worldAABB.max.x - _chunkOrigin.x) / _chunkSize);
|
||||||
|
int minCZ = Mathf.FloorToInt((mesh.worldAABB.min.z - _chunkOrigin.z) / _chunkSize);
|
||||||
|
int maxCZ = Mathf.FloorToInt((mesh.worldAABB.max.z - _chunkOrigin.z) / _chunkSize);
|
||||||
|
|
||||||
|
minCX = Mathf.Clamp(minCX, 0, _chunkGridW - 1);
|
||||||
|
maxCX = Mathf.Clamp(maxCX, 0, _chunkGridW - 1);
|
||||||
|
minCZ = Mathf.Clamp(minCZ, 0, _chunkGridH - 1);
|
||||||
|
maxCZ = Mathf.Clamp(maxCZ, 0, _chunkGridH - 1);
|
||||||
|
|
||||||
|
for (int cz = minCZ; cz <= maxCZ; cz++)
|
||||||
|
for (int cx = minCX; cx <= maxCX; cx++)
|
||||||
|
chunkMeshLists[cx, cz].Add(mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten into contiguous array (mesh indices already in order)
|
||||||
|
// We'll write chunks as (firstMeshIndex, meshCount) referencing the mesh header array
|
||||||
|
_chunks = new CollisionChunkExport[_chunkGridW, _chunkGridH];
|
||||||
|
for (int z = 0; z < _chunkGridH; z++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < _chunkGridW; x++)
|
||||||
|
{
|
||||||
|
var list = chunkMeshLists[x, z];
|
||||||
|
_chunks[x, z] = new CollisionChunkExport
|
||||||
|
{
|
||||||
|
firstMeshIndex = list.Count > 0 ? list[0] : 0,
|
||||||
|
meshCount = list.Count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write collision data to binary.
|
||||||
|
/// All coordinates converted to PS1 20.12 fixed-point with Y flip.
|
||||||
|
/// </summary>
|
||||||
|
public void WriteToBinary(BinaryWriter writer, float gteScaling)
|
||||||
|
{
|
||||||
|
// Header (20 bytes)
|
||||||
|
writer.Write((ushort)_meshes.Count);
|
||||||
|
writer.Write((ushort)_allTriangles.Count);
|
||||||
|
writer.Write((ushort)_chunkGridW);
|
||||||
|
writer.Write((ushort)_chunkGridH);
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(_chunkOrigin.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(_chunkOrigin.z / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(_chunkSize / gteScaling));
|
||||||
|
|
||||||
|
// Mesh headers (32 bytes each)
|
||||||
|
foreach (var mesh in _meshes)
|
||||||
|
{
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(mesh.worldAABB.min.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-mesh.worldAABB.max.y / gteScaling)); // Y flip
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(mesh.worldAABB.min.z / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(mesh.worldAABB.max.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-mesh.worldAABB.min.y / gteScaling)); // Y flip
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(mesh.worldAABB.max.z / gteScaling));
|
||||||
|
writer.Write((ushort)mesh.firstTriangle);
|
||||||
|
writer.Write((ushort)mesh.triangleCount);
|
||||||
|
writer.Write(mesh.roomIndex);
|
||||||
|
writer.Write((byte)0);
|
||||||
|
writer.Write((byte)0);
|
||||||
|
writer.Write((byte)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triangles (52 bytes each)
|
||||||
|
foreach (var tri in _allTriangles)
|
||||||
|
{
|
||||||
|
// v0
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.v0.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-tri.v0.y / gteScaling)); // Y flip
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.v0.z / gteScaling));
|
||||||
|
// edge1
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.e1.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-tri.e1.y / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.e1.z / gteScaling));
|
||||||
|
// edge2
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.e2.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-tri.e2.y / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.e2.z / gteScaling));
|
||||||
|
// normal (in PS1 space: Y negated)
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.normal.x));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-tri.normal.y));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(tri.normal.z));
|
||||||
|
// flags
|
||||||
|
writer.Write(tri.flags);
|
||||||
|
writer.Write(tri.roomIndex);
|
||||||
|
writer.Write((ushort)0); // pad
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spatial grid chunks (4 bytes each, exterior only)
|
||||||
|
if (_chunkGridW > 0 && _chunkGridH > 0)
|
||||||
|
{
|
||||||
|
for (int z = 0; z < _chunkGridH; z++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < _chunkGridW; x++)
|
||||||
|
{
|
||||||
|
writer.Write((ushort)_chunks[x, z].firstMeshIndex);
|
||||||
|
writer.Write((ushort)_chunks[x, z].meshCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get total bytes that will be written.
|
||||||
|
/// </summary>
|
||||||
|
public int GetBinarySize()
|
||||||
|
{
|
||||||
|
int size = 20; // header
|
||||||
|
size += _meshes.Count * 32;
|
||||||
|
size += _allTriangles.Count * 52;
|
||||||
|
if (_chunkGridW > 0 && _chunkGridH > 0)
|
||||||
|
size += _chunkGridW * _chunkGridH * 4;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXCollisionExporter.cs.meta
Normal file
2
Runtime/PSXCollisionExporter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 701b39be55b3bbb46b1c2a4ddaa34132
|
||||||
@@ -3,9 +3,12 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace SplashEdit.RuntimeCode
|
namespace SplashEdit.RuntimeCode
|
||||||
{
|
{
|
||||||
[CreateAssetMenu(fileName = "PSXData", menuName = "Scriptable Objects/PSXData")]
|
|
||||||
|
[CreateAssetMenu(fileName = "PSXData", menuName = "PSXSplash/PS1 Project Data")]
|
||||||
public class PSXData : ScriptableObject
|
public class PSXData : ScriptableObject
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Texture packing settings
|
||||||
public Vector2 OutputResolution = new Vector2(320, 240);
|
public Vector2 OutputResolution = new Vector2(320, 240);
|
||||||
public bool DualBuffering = true;
|
public bool DualBuffering = true;
|
||||||
public bool VerticalBuffering = true;
|
public bool VerticalBuffering = true;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: cbd8c66199e036896848ce1569567dd6
|
guid: b6e1524fb8b4b754e965d03e634658e6
|
||||||
61
Runtime/PSXInteractable.cs
Normal file
61
Runtime/PSXInteractable.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Makes an object interactable by the player.
|
||||||
|
/// When the player is within range and presses the interact button,
|
||||||
|
/// the onInteract Lua event fires.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(PSXObjectExporter))]
|
||||||
|
public class PSXInteractable : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Interaction Settings")]
|
||||||
|
[Tooltip("Distance within which the player can interact with this object")]
|
||||||
|
[SerializeField] private float interactionRadius = 2.0f;
|
||||||
|
|
||||||
|
[Tooltip("Button that triggers interaction (0-15, matches PS1 button mapping)")]
|
||||||
|
[SerializeField] private int interactButton = 5; // Default to Cross button
|
||||||
|
|
||||||
|
[Tooltip("Can this object be interacted with multiple times?")]
|
||||||
|
[SerializeField] private bool isRepeatable = true;
|
||||||
|
|
||||||
|
[Tooltip("Cooldown between interactions (in frames, 60 = 1 second at NTSC)")]
|
||||||
|
[SerializeField] private ushort cooldownFrames = 30;
|
||||||
|
|
||||||
|
[Tooltip("Show interaction prompt when in range (requires UI system)")]
|
||||||
|
[SerializeField] private bool showPrompt = true;
|
||||||
|
|
||||||
|
[Header("Advanced")]
|
||||||
|
[Tooltip("Require line-of-sight to player for interaction")]
|
||||||
|
[SerializeField] private bool requireLineOfSight = false;
|
||||||
|
|
||||||
|
[Tooltip("Custom interaction point offset from object center")]
|
||||||
|
[SerializeField] private Vector3 interactionOffset = Vector3.zero;
|
||||||
|
|
||||||
|
// Public accessors for export
|
||||||
|
public float InteractionRadius => interactionRadius;
|
||||||
|
public int InteractButton => interactButton;
|
||||||
|
public bool IsRepeatable => isRepeatable;
|
||||||
|
public ushort CooldownFrames => cooldownFrames;
|
||||||
|
public bool ShowPrompt => showPrompt;
|
||||||
|
public bool RequireLineOfSight => requireLineOfSight;
|
||||||
|
public Vector3 InteractionOffset => interactionOffset;
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
// Draw interaction radius
|
||||||
|
Gizmos.color = new Color(1f, 1f, 0f, 0.3f); // Yellow, semi-transparent
|
||||||
|
Vector3 center = transform.position + interactionOffset;
|
||||||
|
Gizmos.DrawWireSphere(center, interactionRadius);
|
||||||
|
|
||||||
|
// Draw filled sphere with lower alpha
|
||||||
|
Gizmos.color = new Color(1f, 1f, 0f, 0.1f);
|
||||||
|
Gizmos.DrawSphere(center, interactionRadius);
|
||||||
|
|
||||||
|
// Draw interaction point
|
||||||
|
Gizmos.color = Color.yellow;
|
||||||
|
Gizmos.DrawSphere(center, 0.1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXInteractable.cs.meta
Normal file
2
Runtime/PSXInteractable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9b542f4ca31fa6548b8914e96dd0fae2
|
||||||
@@ -1,88 +1,87 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
public static class PSXLightingBaker
|
namespace SplashEdit.RuntimeCode
|
||||||
{
|
{
|
||||||
/// <summary>
|
public static class PSXLightingBaker
|
||||||
/// Computes the per-vertex lighting from all scene light sources.
|
|
||||||
/// Incorporates ambient, diffuse, and spotlight falloff.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vertex">The world-space position of the vertex.</param>
|
|
||||||
/// <param name="normal">The normalized world-space normal of the vertex.</param>
|
|
||||||
/// <returns>A Color representing the lit vertex.</returns>
|
|
||||||
public static Color ComputeLighting(Vector3 vertex, Vector3 normal)
|
|
||||||
{
|
{
|
||||||
Color finalColor = Color.black;
|
/// <summary>
|
||||||
|
/// Computes the per-vertex lighting from all scene light sources.
|
||||||
Light[] lights = Object.FindObjectsByType<Light>(FindObjectsSortMode.None);
|
/// Incorporates ambient, diffuse, and spotlight falloff.
|
||||||
|
/// </summary>
|
||||||
foreach (Light light in lights)
|
/// <param name="vertex">The world-space position of the vertex.</param>
|
||||||
|
/// <param name="normal">The normalized world-space normal of the vertex.</param>
|
||||||
|
/// <param name="sceneLights">Pre-gathered array of enabled scene lights (pass to avoid per-vertex FindObjectsByType).</param>
|
||||||
|
/// <returns>A Color representing the lit vertex.</returns>
|
||||||
|
public static Color ComputeLighting(Vector3 vertex, Vector3 normal, Light[] sceneLights)
|
||||||
{
|
{
|
||||||
if (!light.enabled)
|
Color finalColor = Color.black;
|
||||||
continue;
|
|
||||||
|
|
||||||
Color lightContribution = Color.black;
|
foreach (Light light in sceneLights)
|
||||||
|
|
||||||
if (light.type == LightType.Directional)
|
|
||||||
{
|
{
|
||||||
Vector3 lightDir = -light.transform.forward;
|
Color lightContribution = Color.black;
|
||||||
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir));
|
|
||||||
lightContribution = light.color * light.intensity * NdotL;
|
|
||||||
}
|
|
||||||
else if (light.type == LightType.Point)
|
|
||||||
{
|
|
||||||
Vector3 lightDir = light.transform.position - vertex;
|
|
||||||
float distance = lightDir.magnitude;
|
|
||||||
lightDir.Normalize();
|
|
||||||
|
|
||||||
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir));
|
if (light.type == LightType.Directional)
|
||||||
float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f);
|
|
||||||
lightContribution = light.color * light.intensity * NdotL * attenuation;
|
|
||||||
}
|
|
||||||
else if (light.type == LightType.Spot)
|
|
||||||
{
|
|
||||||
Vector3 L = light.transform.position - vertex;
|
|
||||||
float distance = L.magnitude;
|
|
||||||
L = L / distance;
|
|
||||||
|
|
||||||
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, L));
|
|
||||||
float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f);
|
|
||||||
|
|
||||||
float outerAngleRad = (light.spotAngle * 0.5f) * Mathf.Deg2Rad;
|
|
||||||
float innerAngleRad = outerAngleRad * 0.8f;
|
|
||||||
|
|
||||||
if (light is Light spotLight)
|
|
||||||
{
|
{
|
||||||
if (spotLight.innerSpotAngle > 0)
|
Vector3 lightDir = -light.transform.forward;
|
||||||
|
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir));
|
||||||
|
lightContribution = light.color * light.intensity * NdotL;
|
||||||
|
}
|
||||||
|
else if (light.type == LightType.Point)
|
||||||
|
{
|
||||||
|
Vector3 lightDir = light.transform.position - vertex;
|
||||||
|
float distance = lightDir.magnitude;
|
||||||
|
lightDir.Normalize();
|
||||||
|
|
||||||
|
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, lightDir));
|
||||||
|
float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f);
|
||||||
|
lightContribution = light.color * light.intensity * NdotL * attenuation;
|
||||||
|
}
|
||||||
|
else if (light.type == LightType.Spot)
|
||||||
|
{
|
||||||
|
Vector3 L = light.transform.position - vertex;
|
||||||
|
float distance = L.magnitude;
|
||||||
|
L = L / distance;
|
||||||
|
|
||||||
|
float NdotL = Mathf.Max(0f, Vector3.Dot(normal, L));
|
||||||
|
float attenuation = 1.0f / Mathf.Max(distance * distance, 0.0001f);
|
||||||
|
|
||||||
|
float outerAngleRad = (light.spotAngle * 0.5f) * Mathf.Deg2Rad;
|
||||||
|
float innerAngleRad = outerAngleRad * 0.8f;
|
||||||
|
|
||||||
|
if (light is Light spotLight)
|
||||||
{
|
{
|
||||||
innerAngleRad = (spotLight.innerSpotAngle * 0.5f) * Mathf.Deg2Rad;
|
if (spotLight.innerSpotAngle > 0)
|
||||||
|
{
|
||||||
|
innerAngleRad = (spotLight.innerSpotAngle * 0.5f) * Mathf.Deg2Rad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float cosOuter = Mathf.Cos(outerAngleRad);
|
||||||
|
float cosInner = Mathf.Cos(innerAngleRad);
|
||||||
|
float cosAngle = Vector3.Dot(L, -light.transform.forward);
|
||||||
|
|
||||||
|
if (cosAngle >= cosOuter)
|
||||||
|
{
|
||||||
|
float spotFactor = Mathf.Clamp01((cosAngle - cosOuter) / (cosInner - cosOuter));
|
||||||
|
spotFactor = Mathf.Pow(spotFactor, 4.0f);
|
||||||
|
|
||||||
|
lightContribution = light.color * light.intensity * NdotL * attenuation * spotFactor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lightContribution = Color.black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float cosOuter = Mathf.Cos(outerAngleRad);
|
finalColor += lightContribution;
|
||||||
float cosInner = Mathf.Cos(innerAngleRad);
|
|
||||||
float cosAngle = Vector3.Dot(L, -light.transform.forward);
|
|
||||||
|
|
||||||
if (cosAngle >= cosOuter)
|
|
||||||
{
|
|
||||||
float spotFactor = Mathf.Clamp01((cosAngle - cosOuter) / (cosInner - cosOuter));
|
|
||||||
spotFactor = Mathf.Pow(spotFactor, 4.0f);
|
|
||||||
|
|
||||||
lightContribution = light.color * light.intensity * NdotL * attenuation * spotFactor;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lightContribution = Color.black;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
finalColor += lightContribution;
|
finalColor.r = Mathf.Clamp(finalColor.r, 0.0f, 0.8f);
|
||||||
|
finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f);
|
||||||
|
finalColor.b = Mathf.Clamp(finalColor.b, 0.0f, 0.8f);
|
||||||
|
finalColor.a = 1f;
|
||||||
|
|
||||||
|
return finalColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
finalColor.r = Mathf.Clamp(finalColor.r, 0.0f, 0.8f);
|
|
||||||
finalColor.g = Mathf.Clamp(finalColor.g, 0.0f, 0.8f);
|
|
||||||
finalColor.b = Mathf.Clamp(finalColor.b, 0.0f, 0.8f);
|
|
||||||
finalColor.a = 1f;
|
|
||||||
|
|
||||||
return finalColor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: b707b7d499862621fb6c82aba4caa183
|
guid: 15a0e6c8af6d78e46bb65ef21c3f75fb
|
||||||
@@ -28,8 +28,17 @@ namespace SplashEdit.RuntimeCode
|
|||||||
public PSXVertex v1;
|
public PSXVertex v1;
|
||||||
public PSXVertex v2;
|
public PSXVertex v2;
|
||||||
|
|
||||||
public PSXTexture2D Texture;
|
/// <summary>
|
||||||
public readonly PSXVertex[] Vertexes => new PSXVertex[] { v0, v1, v2 };
|
/// Index into the texture list for this triangle's material.
|
||||||
|
/// -1 means untextured (vertex-color only, rendered as POLY_G3).
|
||||||
|
/// </summary>
|
||||||
|
public int TextureIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this triangle is untextured (vertex-color only).
|
||||||
|
/// Untextured triangles are rendered as GouraudTriangle (POLY_G3) on PS1.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUntextured => TextureIndex == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -77,63 +86,93 @@ namespace SplashEdit.RuntimeCode
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading.
|
/// Creates a PSXMesh from a Unity Mesh by converting its vertices, normals, UVs, and applying shading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mesh">The Unity mesh to convert.</param>
|
/// <summary>
|
||||||
/// <param name="textureWidth">Width of the texture (default is 256).</param>
|
/// Creates a PSXMesh from a Unity Renderer by extracting its mesh and materials.
|
||||||
/// <param name="textureHeight">Height of the texture (default is 256).</param>
|
/// </summary>
|
||||||
/// <param name="transform">Optional transform to convert vertices to world space.</param>
|
|
||||||
/// <returns>A new PSXMesh containing the converted triangles.</returns>
|
|
||||||
public static PSXMesh CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List<PSXTexture2D> textures)
|
public static PSXMesh CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List<PSXTexture2D> textures)
|
||||||
{
|
{
|
||||||
PSXMesh psxMesh = new PSXMesh { Triangles = new List<Tri>() };
|
|
||||||
|
|
||||||
// Get materials and mesh.
|
|
||||||
Material[] materials = renderer.sharedMaterials;
|
|
||||||
Mesh mesh = renderer.GetComponent<MeshFilter>().sharedMesh;
|
Mesh mesh = renderer.GetComponent<MeshFilter>().sharedMesh;
|
||||||
|
return BuildFromMesh(mesh, renderer, GTEScaling, transform, textures);
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate over each submesh.
|
/// <summary>
|
||||||
for (int submeshIndex = 0; submeshIndex < materials.Length; submeshIndex++)
|
/// Creates a PSXMesh from a supplied Unity Mesh with the renderer's materials.
|
||||||
|
/// </summary>
|
||||||
|
public static PSXMesh CreateFromUnityMesh(Mesh mesh, Renderer renderer, float GTEScaling, Transform transform, List<PSXTexture2D> textures)
|
||||||
|
{
|
||||||
|
return BuildFromMesh(mesh, renderer, GTEScaling, transform, textures);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PSXMesh BuildFromMesh(Mesh mesh, Renderer renderer, float GTEScaling, Transform transform, List<PSXTexture2D> textures)
|
||||||
|
{
|
||||||
|
PSXMesh psxMesh = new PSXMesh { Triangles = new List<Tri>() };
|
||||||
|
Material[] materials = renderer.sharedMaterials;
|
||||||
|
|
||||||
|
// Guard: only recalculate normals if missing
|
||||||
|
if (mesh.normals == null || mesh.normals.Length == 0)
|
||||||
|
mesh.RecalculateNormals();
|
||||||
|
|
||||||
|
if (mesh.uv == null || mesh.uv.Length == 0)
|
||||||
|
mesh.uv = new Vector2[mesh.vertices.Length];
|
||||||
|
|
||||||
|
Vector3[] smoothNormals = RecalculateSmoothNormals(mesh);
|
||||||
|
|
||||||
|
// Cache lights once for the entire mesh
|
||||||
|
Light[] sceneLights = Object.FindObjectsByType<Light>(FindObjectsSortMode.None)
|
||||||
|
.Where(l => l.enabled).ToArray();
|
||||||
|
|
||||||
|
// Precompute world positions and normals for all vertices
|
||||||
|
Vector3[] worldVertices = new Vector3[mesh.vertices.Length];
|
||||||
|
Vector3[] worldNormals = new Vector3[mesh.normals.Length];
|
||||||
|
for (int i = 0; i < mesh.vertices.Length; i++)
|
||||||
{
|
{
|
||||||
// Get the triangles for this submesh.
|
worldVertices[i] = transform.TransformPoint(mesh.vertices[i]);
|
||||||
int[] submeshTriangles = mesh.GetTriangles(submeshIndex);
|
worldNormals[i] = transform.TransformDirection(smoothNormals[i]).normalized;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the material for this submesh.
|
for (int submeshIndex = 0; submeshIndex < mesh.subMeshCount; submeshIndex++)
|
||||||
Material material = materials[submeshIndex];
|
{
|
||||||
|
int materialIndex = Mathf.Min(submeshIndex, materials.Length - 1);
|
||||||
// Get the corresponding texture for this material (assume mainTexture).
|
Material material = materials[materialIndex];
|
||||||
Texture2D texture = material.mainTexture as Texture2D;
|
Texture2D texture = material != null ? material.mainTexture as Texture2D : null;
|
||||||
PSXTexture2D psxTexture = null;
|
|
||||||
|
|
||||||
|
int textureIndex = -1;
|
||||||
if (texture != null)
|
if (texture != null)
|
||||||
{
|
{
|
||||||
// Find the corresponding PSX texture based on the Unity texture.
|
for (int i = 0; i < textures.Count; i++)
|
||||||
psxTexture = textures.FirstOrDefault(t => t.OriginalTexture == texture);
|
{
|
||||||
|
if (textures[i].OriginalTexture == texture)
|
||||||
|
{
|
||||||
|
textureIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (psxTexture == null)
|
int[] submeshTriangles = mesh.GetTriangles(submeshIndex);
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mesh data arrays.
|
|
||||||
mesh.RecalculateNormals();
|
|
||||||
Vector3[] vertices = mesh.vertices;
|
Vector3[] vertices = mesh.vertices;
|
||||||
Vector3[] normals = mesh.normals;
|
Vector3[] normals = mesh.normals;
|
||||||
Vector3[] smoothNormals = RecalculateSmoothNormals(mesh);
|
|
||||||
Vector2[] uv = mesh.uv;
|
Vector2[] uv = mesh.uv;
|
||||||
|
|
||||||
PSXVertex convertData(int index)
|
PSXVertex convertData(int index)
|
||||||
{
|
{
|
||||||
// Scale the vertex based on world scale.
|
|
||||||
Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale);
|
Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale);
|
||||||
// Transform the vertex to world space.
|
Vector3 wv = worldVertices[index];
|
||||||
Vector3 wv = transform.TransformPoint(vertices[index]);
|
Vector3 wn = worldNormals[index];
|
||||||
// Transform the normals to world space.
|
Color c = PSXLightingBaker.ComputeLighting(wv, wn, sceneLights);
|
||||||
Vector3 wn = transform.TransformDirection(smoothNormals[index]).normalized;
|
|
||||||
// Compute lighting for each vertex.
|
if (textureIndex == -1)
|
||||||
Color c = PSXLightingBaker.ComputeLighting(wv, wn);
|
{
|
||||||
// Convert vertex to PSX format, including fixed-point conversion and shading.
|
Color matColor = material != null && material.HasProperty("_Color")
|
||||||
return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index], psxTexture?.Width, psxTexture?.Height, c);
|
? material.color : Color.white;
|
||||||
|
c = new Color(c.r * matColor.r, c.g * matColor.g, c.b * matColor.b);
|
||||||
|
return ConvertToPSXVertex(v, GTEScaling, normals[index], Vector2.zero, null, null, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index],
|
||||||
|
textures[textureIndex]?.Width, textures[textureIndex]?.Height, c);
|
||||||
}
|
}
|
||||||
// Iterate through the triangles of the submesh.
|
|
||||||
for (int i = 0; i < submeshTriangles.Length; i += 3)
|
for (int i = 0; i < submeshTriangles.Length; i += 3)
|
||||||
{
|
{
|
||||||
int vid0 = submeshTriangles[i];
|
int vid0 = submeshTriangles[i];
|
||||||
@@ -147,8 +186,13 @@ namespace SplashEdit.RuntimeCode
|
|||||||
(vid1, vid2) = (vid2, vid1);
|
(vid1, vid2) = (vid2, vid1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the constructed triangle to the mesh.
|
psxMesh.Triangles.Add(new Tri
|
||||||
psxMesh.Triangles.Add(new Tri { v0 = convertData(vid0), v1 = convertData(vid1), v2 = convertData(vid2), Texture = psxTexture });
|
{
|
||||||
|
v0 = convertData(vid0),
|
||||||
|
v1 = convertData(vid1),
|
||||||
|
v2 = convertData(vid2),
|
||||||
|
TextureIndex = textureIndex
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 9025daa0c62549ee29d968f86c69eec9
|
guid: 0bde77749a0264146a4ead39946dce2f
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using Unity.AI.Navigation;
|
|
||||||
using UnityEngine.AI;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace SplashEdit.RuntimeCode
|
|
||||||
{
|
|
||||||
public struct PSXNavMeshTri
|
|
||||||
{
|
|
||||||
public PSXNavmeshVertex v0, v1, v2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct PSXNavmeshVertex
|
|
||||||
{
|
|
||||||
public short vx, vy, vz;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RequireComponent(typeof(NavMeshSurface))]
|
|
||||||
public class PSXNavMesh : MonoBehaviour
|
|
||||||
{
|
|
||||||
|
|
||||||
Mesh mesh;
|
|
||||||
|
|
||||||
[HideInInspector]
|
|
||||||
public List<PSXNavMeshTri> Navmesh { get; set; }
|
|
||||||
|
|
||||||
public void CreateNavmesh(float GTEScaling)
|
|
||||||
{
|
|
||||||
mesh = new Mesh();
|
|
||||||
Navmesh = new List<PSXNavMeshTri>();
|
|
||||||
NavMeshSurface navMeshSurface = GetComponent<NavMeshSurface>();
|
|
||||||
navMeshSurface.overrideTileSize = true;
|
|
||||||
navMeshSurface.tileSize = 16;
|
|
||||||
navMeshSurface.overrideVoxelSize = true;
|
|
||||||
navMeshSurface.voxelSize = 0.1f;
|
|
||||||
navMeshSurface.BuildNavMesh();
|
|
||||||
NavMeshTriangulation triangulation = NavMesh.CalculateTriangulation();
|
|
||||||
navMeshSurface.overrideTileSize = false;
|
|
||||||
navMeshSurface.overrideVoxelSize = false;
|
|
||||||
|
|
||||||
int[] triangles = triangulation.indices;
|
|
||||||
Vector3[] vertices = triangulation.vertices;
|
|
||||||
|
|
||||||
mesh.vertices = vertices;
|
|
||||||
mesh.triangles = triangles;
|
|
||||||
|
|
||||||
mesh.RecalculateNormals();
|
|
||||||
|
|
||||||
for (int i = 0; i < triangles.Length; i += 3)
|
|
||||||
{
|
|
||||||
int vid0 = triangles[i];
|
|
||||||
int vid1 = triangles[i + 1];
|
|
||||||
int vid2 = triangles[i + 2];
|
|
||||||
|
|
||||||
PSXNavMeshTri tri = new PSXNavMeshTri();
|
|
||||||
|
|
||||||
tri.v0.vx = PSXTrig.ConvertCoordinateToPSX(vertices[vid0].x, GTEScaling);
|
|
||||||
tri.v0.vy = PSXTrig.ConvertCoordinateToPSX(-vertices[vid0].y, GTEScaling);
|
|
||||||
tri.v0.vz = PSXTrig.ConvertCoordinateToPSX(vertices[vid0].z, GTEScaling);
|
|
||||||
|
|
||||||
tri.v1.vx = PSXTrig.ConvertCoordinateToPSX(vertices[vid1].x, GTEScaling);
|
|
||||||
tri.v1.vy = PSXTrig.ConvertCoordinateToPSX(-vertices[vid1].y, GTEScaling);
|
|
||||||
tri.v1.vz = PSXTrig.ConvertCoordinateToPSX(vertices[vid1].z, GTEScaling);
|
|
||||||
|
|
||||||
tri.v2.vx = PSXTrig.ConvertCoordinateToPSX(vertices[vid2].x, GTEScaling);
|
|
||||||
tri.v2.vy = PSXTrig.ConvertCoordinateToPSX(-vertices[vid2].y, GTEScaling);
|
|
||||||
tri.v2.vz = PSXTrig.ConvertCoordinateToPSX(vertices[vid2].z, GTEScaling);
|
|
||||||
|
|
||||||
Navmesh.Add(tri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void OnDrawGizmos()
|
|
||||||
{
|
|
||||||
if (mesh == null) return;
|
|
||||||
Gizmos.DrawMesh(mesh);
|
|
||||||
Gizmos.color = Color.green;
|
|
||||||
|
|
||||||
var vertices = mesh.vertices;
|
|
||||||
var triangles = mesh.triangles;
|
|
||||||
|
|
||||||
for (int i = 0; i < triangles.Length; i += 3)
|
|
||||||
{
|
|
||||||
Vector3 v0 = vertices[triangles[i]];
|
|
||||||
Vector3 v1 = vertices[triangles[i + 1]];
|
|
||||||
Vector3 v2 = vertices[triangles[i + 2]];
|
|
||||||
|
|
||||||
Gizmos.DrawLine(v0, v1);
|
|
||||||
Gizmos.DrawLine(v1, v2);
|
|
||||||
Gizmos.DrawLine(v2, v0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6a2f8d45e1591de1e945b3b7bdfb123b
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {fileID: 2800000, guid: d695ef52da250cdcea6c30ab1122c56e, type: 3}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
522
Runtime/PSXNavRegionBuilder.cs
Normal file
522
Runtime/PSXNavRegionBuilder.cs
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEngine;
|
||||||
|
using DotRecast.Core;
|
||||||
|
using DotRecast.Core.Numerics;
|
||||||
|
using DotRecast.Recast;
|
||||||
|
|
||||||
|
namespace SplashEdit.RuntimeCode
|
||||||
|
{
|
||||||
|
public enum NavSurfaceType : byte { Flat = 0, Ramp = 1, Stairs = 2 }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PS1 nav mesh builder using DotRecast (C# port of Recast).
|
||||||
|
/// Runs the full Recast voxelization pipeline on scene geometry,
|
||||||
|
/// then extracts convex polygons with accurate heights from the detail mesh.
|
||||||
|
/// </summary>
|
||||||
|
public class PSXNavRegionBuilder
|
||||||
|
{
|
||||||
|
public float AgentHeight = 1.8f;
|
||||||
|
public float AgentRadius = 0.3f;
|
||||||
|
public float MaxStepHeight = 0.35f;
|
||||||
|
public float WalkableSlopeAngle = 46.0f;
|
||||||
|
public float CellSize = 0.05f;
|
||||||
|
public float CellHeight = 0.025f;
|
||||||
|
public float MergePlaneError = 0.1f;
|
||||||
|
public const int MaxVertsPerRegion = 8;
|
||||||
|
|
||||||
|
private List<NavRegionExport> _regions = new();
|
||||||
|
private List<NavPortalExport> _portals = new();
|
||||||
|
private int _startRegion;
|
||||||
|
|
||||||
|
public int RegionCount => _regions.Count;
|
||||||
|
public int PortalCount => _portals.Count;
|
||||||
|
public IReadOnlyList<NavRegionExport> Regions => _regions;
|
||||||
|
public IReadOnlyList<NavPortalExport> Portals => _portals;
|
||||||
|
public VoxelCell[] DebugCells => null;
|
||||||
|
public int DebugGridW => 0;
|
||||||
|
public int DebugGridH => 0;
|
||||||
|
public float DebugOriginX => 0;
|
||||||
|
public float DebugOriginZ => 0;
|
||||||
|
public float DebugVoxelSize => 0;
|
||||||
|
|
||||||
|
public class NavRegionExport
|
||||||
|
{
|
||||||
|
public List<Vector2> vertsXZ = new();
|
||||||
|
public float planeA, planeB, planeD;
|
||||||
|
public int portalStart, portalCount;
|
||||||
|
public NavSurfaceType surfaceType;
|
||||||
|
public byte roomIndex;
|
||||||
|
public Plane floorPlane;
|
||||||
|
public List<Vector3> worldTris = new();
|
||||||
|
public List<int> sourceTriIndices = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NavPortalExport
|
||||||
|
{
|
||||||
|
public Vector2 a, b;
|
||||||
|
public int neighborRegion;
|
||||||
|
public float heightDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct VoxelCell
|
||||||
|
{
|
||||||
|
public float worldY, slopeAngle;
|
||||||
|
public bool occupied, blocked;
|
||||||
|
public int regionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>PSXRoom volumes for spatial room assignment. Set before Build().</summary>
|
||||||
|
public PSXRoom[] PSXRooms { get; set; }
|
||||||
|
|
||||||
|
public void Build(PSXObjectExporter[] exporters, Vector3 spawn)
|
||||||
|
{
|
||||||
|
_regions.Clear(); _portals.Clear(); _startRegion = 0;
|
||||||
|
|
||||||
|
// 1. Collect world-space geometry from all exporters
|
||||||
|
var allVerts = new List<float>();
|
||||||
|
var allTris = new List<int>();
|
||||||
|
CollectGeometry(exporters, allVerts, allTris);
|
||||||
|
|
||||||
|
if (allVerts.Count < 9 || allTris.Count < 3)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Nav] No geometry to build navmesh from.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] verts = allVerts.ToArray();
|
||||||
|
int[] tris = allTris.ToArray();
|
||||||
|
int nverts = allVerts.Count / 3;
|
||||||
|
int ntris = allTris.Count / 3;
|
||||||
|
|
||||||
|
// 2. Recast parameters (convert to voxel units)
|
||||||
|
float cs = CellSize;
|
||||||
|
float ch = CellHeight;
|
||||||
|
int walkableHeight = (int)Math.Ceiling(AgentHeight / ch);
|
||||||
|
int walkableClimb = (int)Math.Floor(MaxStepHeight / ch);
|
||||||
|
int walkableRadius = (int)Math.Ceiling(AgentRadius / cs);
|
||||||
|
int maxEdgeLen = (int)(12.0f / cs);
|
||||||
|
float maxSimplificationError = 1.3f;
|
||||||
|
int minRegionArea = 8;
|
||||||
|
int mergeRegionArea = 20;
|
||||||
|
int maxVertsPerPoly = 6;
|
||||||
|
float detailSampleDist = cs * 6;
|
||||||
|
float detailSampleMaxError = ch * 1;
|
||||||
|
|
||||||
|
// 3. Compute bounds with border padding
|
||||||
|
float bminX = float.MaxValue, bminY = float.MaxValue, bminZ = float.MaxValue;
|
||||||
|
float bmaxX = float.MinValue, bmaxY = float.MinValue, bmaxZ = float.MinValue;
|
||||||
|
for (int i = 0; i < verts.Length; i += 3)
|
||||||
|
{
|
||||||
|
bminX = Math.Min(bminX, verts[i]); bmaxX = Math.Max(bmaxX, verts[i]);
|
||||||
|
bminY = Math.Min(bminY, verts[i+1]); bmaxY = Math.Max(bmaxY, verts[i+1]);
|
||||||
|
bminZ = Math.Min(bminZ, verts[i+2]); bmaxZ = Math.Max(bmaxZ, verts[i+2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
float borderPad = walkableRadius * cs;
|
||||||
|
bminX -= borderPad; bminZ -= borderPad;
|
||||||
|
bmaxX += borderPad; bmaxZ += borderPad;
|
||||||
|
|
||||||
|
var bmin = new RcVec3f(bminX, bminY, bminZ);
|
||||||
|
var bmax = new RcVec3f(bmaxX, bmaxY, bmaxZ);
|
||||||
|
|
||||||
|
int gw = (int)((bmaxX - bminX) / cs + 0.5f);
|
||||||
|
int gh = (int)((bmaxZ - bminZ) / cs + 0.5f);
|
||||||
|
|
||||||
|
// 4. Run Recast pipeline
|
||||||
|
var ctx = new RcContext();
|
||||||
|
|
||||||
|
// Create heightfield
|
||||||
|
var solid = new RcHeightfield(gw, gh, bmin, bmax, cs, ch, 0);
|
||||||
|
|
||||||
|
// Mark walkable triangles
|
||||||
|
int[] areas = RcRecast.MarkWalkableTriangles(ctx, WalkableSlopeAngle, verts, tris, ntris,
|
||||||
|
new RcAreaModification(RcRecast.RC_WALKABLE_AREA));
|
||||||
|
|
||||||
|
// Rasterize
|
||||||
|
RcRasterizations.RasterizeTriangles(ctx, verts, tris, areas, ntris, solid, walkableClimb);
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
RcFilters.FilterLowHangingWalkableObstacles(ctx, walkableClimb, solid);
|
||||||
|
RcFilters.FilterLedgeSpans(ctx, walkableHeight, walkableClimb, solid);
|
||||||
|
RcFilters.FilterWalkableLowHeightSpans(ctx, walkableHeight, solid);
|
||||||
|
|
||||||
|
// Build compact heightfield
|
||||||
|
var chf = RcCompacts.BuildCompactHeightfield(ctx, walkableHeight, walkableClimb, solid);
|
||||||
|
|
||||||
|
// Erode walkable area
|
||||||
|
RcAreas.ErodeWalkableArea(ctx, walkableRadius, chf);
|
||||||
|
|
||||||
|
// Build distance field and regions
|
||||||
|
RcRegions.BuildDistanceField(ctx, chf);
|
||||||
|
RcRegions.BuildRegions(ctx, chf, minRegionArea, mergeRegionArea);
|
||||||
|
|
||||||
|
// Build contours
|
||||||
|
var cset = RcContours.BuildContours(ctx, chf, maxSimplificationError, maxEdgeLen,
|
||||||
|
(int)RcBuildContoursFlags.RC_CONTOUR_TESS_WALL_EDGES);
|
||||||
|
|
||||||
|
// Build polygon mesh
|
||||||
|
var pmesh = RcMeshs.BuildPolyMesh(ctx, cset, maxVertsPerPoly);
|
||||||
|
|
||||||
|
// Build detail mesh for accurate heights
|
||||||
|
var dmesh = RcMeshDetails.BuildPolyMeshDetail(ctx, pmesh, chf, detailSampleDist, detailSampleMaxError);
|
||||||
|
|
||||||
|
// 5. Extract polygons as NavRegions
|
||||||
|
int nvp = pmesh.nvp;
|
||||||
|
int RC_MESH_NULL_IDX = 0xffff;
|
||||||
|
|
||||||
|
for (int i = 0; i < pmesh.npolys; i++)
|
||||||
|
{
|
||||||
|
// Count valid vertices in this polygon
|
||||||
|
int nv = 0;
|
||||||
|
for (int j = 0; j < nvp; j++)
|
||||||
|
{
|
||||||
|
if (pmesh.polys[i * 2 * nvp + j] == RC_MESH_NULL_IDX) break;
|
||||||
|
nv++;
|
||||||
|
}
|
||||||
|
if (nv < 3) continue;
|
||||||
|
|
||||||
|
var region = new NavRegionExport();
|
||||||
|
var pts3d = new List<Vector3>();
|
||||||
|
|
||||||
|
for (int j = 0; j < nv; j++)
|
||||||
|
{
|
||||||
|
int vi = pmesh.polys[i * 2 * nvp + j];
|
||||||
|
|
||||||
|
// Get XZ from poly mesh (cell coords -> world)
|
||||||
|
float wx = pmesh.bmin.X + pmesh.verts[vi * 3 + 0] * pmesh.cs;
|
||||||
|
float wz = pmesh.bmin.Z + pmesh.verts[vi * 3 + 2] * pmesh.cs;
|
||||||
|
|
||||||
|
// Get accurate Y from detail mesh
|
||||||
|
float wy;
|
||||||
|
if (dmesh != null && i < dmesh.nmeshes)
|
||||||
|
{
|
||||||
|
int vbase = dmesh.meshes[i * 4 + 0];
|
||||||
|
// Detail mesh stores polygon verts first, in order
|
||||||
|
wy = dmesh.verts[(vbase + j) * 3 + 1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: coarse Y from poly mesh
|
||||||
|
wy = pmesh.bmin.Y + pmesh.verts[vi * 3 + 1] * pmesh.ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
region.vertsXZ.Add(new Vector2(wx, wz));
|
||||||
|
pts3d.Add(new Vector3(wx, wy, wz));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CCW winding
|
||||||
|
float signedArea = 0;
|
||||||
|
for (int j = 0; j < region.vertsXZ.Count; j++)
|
||||||
|
{
|
||||||
|
var a = region.vertsXZ[j];
|
||||||
|
var b = region.vertsXZ[(j + 1) % region.vertsXZ.Count];
|
||||||
|
signedArea += a.x * b.y - b.x * a.y;
|
||||||
|
}
|
||||||
|
if (signedArea < 0)
|
||||||
|
{
|
||||||
|
region.vertsXZ.Reverse();
|
||||||
|
pts3d.Reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
FitPlane(region, pts3d);
|
||||||
|
_regions.Add(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Build portals from Recast neighbor connectivity
|
||||||
|
var perRegion = new Dictionary<int, List<NavPortalExport>>();
|
||||||
|
for (int i = 0; i < _regions.Count; i++)
|
||||||
|
perRegion[i] = new List<NavPortalExport>();
|
||||||
|
|
||||||
|
// Build mapping: pmesh poly index -> region index
|
||||||
|
// (some polys may be skipped if nv < 3, so we need this mapping)
|
||||||
|
var polyToRegion = new Dictionary<int, int>();
|
||||||
|
int regionIdx = 0;
|
||||||
|
for (int i = 0; i < pmesh.npolys; i++)
|
||||||
|
{
|
||||||
|
int nv = 0;
|
||||||
|
for (int j = 0; j < nvp; j++)
|
||||||
|
{
|
||||||
|
if (pmesh.polys[i * 2 * nvp + j] == RC_MESH_NULL_IDX) break;
|
||||||
|
nv++;
|
||||||
|
}
|
||||||
|
if (nv < 3) continue;
|
||||||
|
polyToRegion[i] = regionIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < pmesh.npolys; i++)
|
||||||
|
{
|
||||||
|
if (!polyToRegion.TryGetValue(i, out int srcRegion)) continue;
|
||||||
|
|
||||||
|
int nv = 0;
|
||||||
|
for (int j = 0; j < nvp; j++)
|
||||||
|
{
|
||||||
|
if (pmesh.polys[i * 2 * nvp + j] == RC_MESH_NULL_IDX) break;
|
||||||
|
nv++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < nv; j++)
|
||||||
|
{
|
||||||
|
int neighbor = pmesh.polys[i * 2 * nvp + nvp + j];
|
||||||
|
if (neighbor == RC_MESH_NULL_IDX || (neighbor & 0x8000) != 0) continue;
|
||||||
|
if (!polyToRegion.TryGetValue(neighbor, out int dstRegion)) continue;
|
||||||
|
|
||||||
|
// Portal edge vertices from pmesh directly (NOT from region,
|
||||||
|
// which may have been reversed for CCW winding)
|
||||||
|
int vi0 = pmesh.polys[i * 2 * nvp + j];
|
||||||
|
int vi1 = pmesh.polys[i * 2 * nvp + (j + 1) % nv];
|
||||||
|
|
||||||
|
float ax = pmesh.bmin.X + pmesh.verts[vi0 * 3 + 0] * pmesh.cs;
|
||||||
|
float az = pmesh.bmin.Z + pmesh.verts[vi0 * 3 + 2] * pmesh.cs;
|
||||||
|
float bx = pmesh.bmin.X + pmesh.verts[vi1 * 3 + 0] * pmesh.cs;
|
||||||
|
float bz = pmesh.bmin.Z + pmesh.verts[vi1 * 3 + 2] * pmesh.cs;
|
||||||
|
|
||||||
|
var a2 = new Vector2(ax, az);
|
||||||
|
var b2 = new Vector2(bx, bz);
|
||||||
|
|
||||||
|
// Height delta at midpoint of portal edge
|
||||||
|
var mid = new Vector2((ax + bx) / 2, (az + bz) / 2);
|
||||||
|
float yHere = EvalY(_regions[srcRegion], mid);
|
||||||
|
float yThere = EvalY(_regions[dstRegion], mid);
|
||||||
|
|
||||||
|
perRegion[srcRegion].Add(new NavPortalExport
|
||||||
|
{
|
||||||
|
a = a2,
|
||||||
|
b = b2,
|
||||||
|
neighborRegion = dstRegion,
|
||||||
|
heightDelta = yThere - yHere
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign portals
|
||||||
|
foreach (var kvp in perRegion)
|
||||||
|
{
|
||||||
|
_regions[kvp.Key].portalStart = _portals.Count;
|
||||||
|
_regions[kvp.Key].portalCount = kvp.Value.Count;
|
||||||
|
_portals.AddRange(kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Assign rooms: spatial containment if PSXRooms provided, BFS fallback
|
||||||
|
if (PSXRooms != null && PSXRooms.Length > 0)
|
||||||
|
AssignRoomsFromPSXRooms(PSXRooms);
|
||||||
|
else
|
||||||
|
AssignRoomsByBFS();
|
||||||
|
|
||||||
|
// 8. Find start region closest to spawn
|
||||||
|
_startRegion = FindClosestRegion(spawn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollectGeometry(PSXObjectExporter[] exporters, List<float> outVerts, List<int> outTris)
|
||||||
|
{
|
||||||
|
foreach (var exporter in exporters)
|
||||||
|
{
|
||||||
|
|
||||||
|
MeshFilter mf = exporter.GetComponent<MeshFilter>();
|
||||||
|
Mesh mesh = mf?.sharedMesh;
|
||||||
|
if (mesh == null) continue;
|
||||||
|
|
||||||
|
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
|
||||||
|
Vector3[] vertices = mesh.vertices;
|
||||||
|
int[] indices = mesh.triangles;
|
||||||
|
|
||||||
|
int baseVert = outVerts.Count / 3;
|
||||||
|
foreach (var v in vertices)
|
||||||
|
{
|
||||||
|
Vector3 w = worldMatrix.MultiplyPoint3x4(v);
|
||||||
|
outVerts.Add(w.x);
|
||||||
|
outVerts.Add(w.y);
|
||||||
|
outVerts.Add(w.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter triangles: reject downward-facing normals
|
||||||
|
// (ceilings, roofs, undersides) which should never be walkable.
|
||||||
|
for (int i = 0; i < indices.Length; i += 3)
|
||||||
|
{
|
||||||
|
Vector3 v0 = worldMatrix.MultiplyPoint3x4(vertices[indices[i]]);
|
||||||
|
Vector3 v1 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 1]]);
|
||||||
|
Vector3 v2 = worldMatrix.MultiplyPoint3x4(vertices[indices[i + 2]]);
|
||||||
|
|
||||||
|
Vector3 normal = Vector3.Cross(v1 - v0, v2 - v0);
|
||||||
|
// Skip triangles whose world-space normal points downward (y < 0)
|
||||||
|
// This eliminates ceilings/roofs that Recast might incorrectly voxelize
|
||||||
|
if (normal.y < 0f) continue;
|
||||||
|
|
||||||
|
outTris.Add(indices[i] + baseVert);
|
||||||
|
outTris.Add(indices[i + 1] + baseVert);
|
||||||
|
outTris.Add(indices[i + 2] + baseVert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int FindClosestRegion(Vector3 spawn)
|
||||||
|
{
|
||||||
|
int best = 0;
|
||||||
|
float bestDist = float.MaxValue;
|
||||||
|
for (int i = 0; i < _regions.Count; i++)
|
||||||
|
{
|
||||||
|
var r = _regions[i];
|
||||||
|
// Compute centroid
|
||||||
|
float cx = 0, cz = 0;
|
||||||
|
foreach (var v in r.vertsXZ) { cx += v.x; cz += v.y; }
|
||||||
|
cx /= r.vertsXZ.Count; cz /= r.vertsXZ.Count;
|
||||||
|
float cy = EvalY(r, new Vector2(cx, cz));
|
||||||
|
|
||||||
|
float dx = spawn.x - cx, dy = spawn.y - cy, dz = spawn.z - cz;
|
||||||
|
float dist = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (dist < bestDist) { bestDist = dist; best = i; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
float EvalY(NavRegionExport r, Vector2 xz) => r.planeA * xz.x + r.planeB * xz.y + r.planeD;
|
||||||
|
|
||||||
|
void FitPlane(NavRegionExport r, List<Vector3> pts)
|
||||||
|
{
|
||||||
|
int n = pts.Count;
|
||||||
|
if (n < 3) { r.planeA = 0; r.planeB = 0; r.planeD = n > 0 ? pts[0].y : 0; r.surfaceType = NavSurfaceType.Flat; return; }
|
||||||
|
|
||||||
|
if (n == 3)
|
||||||
|
{
|
||||||
|
// Exact 3-point solve: Y = A*X + B*Z + D
|
||||||
|
double x0 = pts[0].x, z0 = pts[0].z, y0 = pts[0].y;
|
||||||
|
double x1 = pts[1].x, z1 = pts[1].z, y1 = pts[1].y;
|
||||||
|
double x2 = pts[2].x, z2 = pts[2].z, y2 = pts[2].y;
|
||||||
|
double det = (x0 - x2) * (z1 - z2) - (x1 - x2) * (z0 - z2);
|
||||||
|
if (Math.Abs(det) < 1e-12) { r.planeA = 0; r.planeB = 0; r.planeD = (float)((y0 + y1 + y2) / 3); }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double inv = 1.0 / det;
|
||||||
|
r.planeA = (float)(((y0 - y2) * (z1 - z2) - (y1 - y2) * (z0 - z2)) * inv);
|
||||||
|
r.planeB = (float)(((x0 - x2) * (y1 - y2) - (x1 - x2) * (y0 - y2)) * inv);
|
||||||
|
r.planeD = (float)(y0 - r.planeA * x0 - r.planeB * z0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Least-squares: Y = A*X + B*Z + D
|
||||||
|
double sX = 0, sZ = 0, sY = 0, sXX = 0, sXZ = 0, sZZ = 0, sXY = 0, sZY = 0;
|
||||||
|
foreach (var p in pts) { sX += p.x; sZ += p.z; sY += p.y; sXX += p.x * p.x; sXZ += p.x * p.z; sZZ += p.z * p.z; sXY += p.x * p.y; sZY += p.z * p.y; }
|
||||||
|
double det = sXX * (sZZ * n - sZ * sZ) - sXZ * (sXZ * n - sZ * sX) + sX * (sXZ * sZ - sZZ * sX);
|
||||||
|
if (Math.Abs(det) < 1e-12) { r.planeA = 0; r.planeB = 0; r.planeD = (float)(sY / n); }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double inv = 1.0 / det;
|
||||||
|
r.planeA = (float)((sXY * (sZZ * n - sZ * sZ) - sXZ * (sZY * n - sZ * sY) + sX * (sZY * sZ - sZZ * sY)) * inv);
|
||||||
|
r.planeB = (float)((sXX * (sZY * n - sZ * sY) - sXY * (sXZ * n - sZ * sX) + sX * (sXZ * sY - sZY * sX)) * inv);
|
||||||
|
r.planeD = (float)((sXX * (sZZ * sY - sZ * sZY) - sXZ * (sXZ * sY - sZY * sX) + sXY * (sXZ * sZ - sZZ * sX)) * inv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float slope = Mathf.Atan(Mathf.Sqrt(r.planeA * r.planeA + r.planeB * r.planeB)) * Mathf.Rad2Deg;
|
||||||
|
r.surfaceType = slope < 3f ? NavSurfaceType.Flat : slope < 25f ? NavSurfaceType.Ramp : NavSurfaceType.Stairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assign room indices to nav regions using PSXRoom spatial containment.
|
||||||
|
/// Each region's centroid is tested against all PSXRoom volumes. The smallest
|
||||||
|
/// containing room wins (most specific). Regions outside all rooms get 0xFF.
|
||||||
|
/// This ensures nav region room indices match the PSXRoomBuilder room indices
|
||||||
|
/// used by the rendering portal system.
|
||||||
|
/// </summary>
|
||||||
|
void AssignRoomsFromPSXRooms(PSXRoom[] psxRooms)
|
||||||
|
{
|
||||||
|
const float MARGIN = 0.5f;
|
||||||
|
Bounds[] roomBounds = new Bounds[psxRooms.Length];
|
||||||
|
for (int r = 0; r < psxRooms.Length; r++)
|
||||||
|
{
|
||||||
|
roomBounds[r] = psxRooms[r].GetWorldBounds();
|
||||||
|
roomBounds[r].Expand(MARGIN * 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _regions.Count; i++)
|
||||||
|
{
|
||||||
|
var reg = _regions[i];
|
||||||
|
// Compute region centroid from polygon vertices
|
||||||
|
float cx = 0, cz = 0;
|
||||||
|
foreach (var v in reg.vertsXZ) { cx += v.x; cz += v.y; }
|
||||||
|
cx /= reg.vertsXZ.Count; cz /= reg.vertsXZ.Count;
|
||||||
|
float cy = EvalY(reg, new Vector2(cx, cz));
|
||||||
|
Vector3 centroid = new Vector3(cx, cy, cz);
|
||||||
|
|
||||||
|
byte bestRoom = 0xFF;
|
||||||
|
float bestVolume = float.MaxValue;
|
||||||
|
for (int r = 0; r < psxRooms.Length; r++)
|
||||||
|
{
|
||||||
|
if (roomBounds[r].Contains(centroid))
|
||||||
|
{
|
||||||
|
float vol = roomBounds[r].size.x * roomBounds[r].size.y * roomBounds[r].size.z;
|
||||||
|
if (vol < bestVolume)
|
||||||
|
{
|
||||||
|
bestVolume = vol;
|
||||||
|
bestRoom = (byte)r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reg.roomIndex = bestRoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fallback room assignment via BFS over nav portal connectivity.
|
||||||
|
/// Used when no PSXRoom volumes exist (exterior scenes).
|
||||||
|
/// </summary>
|
||||||
|
void AssignRoomsByBFS()
|
||||||
|
{
|
||||||
|
byte room = 0;
|
||||||
|
var vis = new bool[_regions.Count];
|
||||||
|
for (int i = 0; i < _regions.Count; i++)
|
||||||
|
{
|
||||||
|
if (vis[i]) continue;
|
||||||
|
byte rm = room++;
|
||||||
|
var q = new Queue<int>(); q.Enqueue(i); vis[i] = true;
|
||||||
|
while (q.Count > 0)
|
||||||
|
{
|
||||||
|
int ri = q.Dequeue(); _regions[ri].roomIndex = rm;
|
||||||
|
for (int p = _regions[ri].portalStart; p < _regions[ri].portalStart + _regions[ri].portalCount; p++)
|
||||||
|
{
|
||||||
|
int nb = _portals[p].neighborRegion;
|
||||||
|
if (nb >= 0 && nb < _regions.Count && !vis[nb]) { vis[nb] = true; q.Enqueue(nb); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteToBinary(BinaryWriter writer, float gteScaling)
|
||||||
|
{
|
||||||
|
writer.Write((ushort)_regions.Count);
|
||||||
|
writer.Write((ushort)_portals.Count);
|
||||||
|
writer.Write((ushort)_startRegion);
|
||||||
|
writer.Write((ushort)0);
|
||||||
|
foreach (var r in _regions)
|
||||||
|
{
|
||||||
|
for (int v = 0; v < MaxVertsPerRegion; v++)
|
||||||
|
writer.Write(v < r.vertsXZ.Count ? PSXTrig.ConvertWorldToFixed12(r.vertsXZ[v].x / gteScaling) : 0);
|
||||||
|
for (int v = 0; v < MaxVertsPerRegion; v++)
|
||||||
|
writer.Write(v < r.vertsXZ.Count ? PSXTrig.ConvertWorldToFixed12(r.vertsXZ[v].y / gteScaling) : 0);
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-r.planeA));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-r.planeB));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(-r.planeD / gteScaling));
|
||||||
|
writer.Write((ushort)r.portalStart);
|
||||||
|
writer.Write((byte)r.portalCount);
|
||||||
|
writer.Write((byte)r.vertsXZ.Count);
|
||||||
|
writer.Write((byte)r.surfaceType);
|
||||||
|
writer.Write(r.roomIndex);
|
||||||
|
writer.Write((byte)0);
|
||||||
|
writer.Write((byte)0);
|
||||||
|
}
|
||||||
|
foreach (var p in _portals)
|
||||||
|
{
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(p.a.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(p.a.y / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(p.b.x / gteScaling));
|
||||||
|
writer.Write(PSXTrig.ConvertWorldToFixed12(p.b.y / gteScaling));
|
||||||
|
writer.Write((ushort)p.neighborRegion);
|
||||||
|
writer.Write((short)PSXTrig.ConvertToFixed12(p.heightDelta / gteScaling));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetBinarySize() => 8 + _regions.Count * 84 + _portals.Count * 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Runtime/PSXNavRegionBuilder.cs.meta
Normal file
2
Runtime/PSXNavRegionBuilder.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7446b9ee150d0994fb534c61cd894d6c
|
||||||
@@ -1,21 +1,86 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using SplashEdit.RuntimeCode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Serialization;
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
namespace SplashEdit.RuntimeCode
|
namespace SplashEdit.RuntimeCode
|
||||||
{
|
{
|
||||||
[RequireComponent(typeof(Renderer))]
|
/// <summary>
|
||||||
public class PSXObjectExporter : MonoBehaviour
|
/// Collision type for PS1 runtime
|
||||||
|
/// </summary>
|
||||||
|
public enum PSXCollisionType
|
||||||
{
|
{
|
||||||
public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>(); // Stores the converted PlayStation-style texture
|
None = 0, // No collision
|
||||||
public PSXMesh Mesh { get; set; } // Stores the converted PlayStation-style mesh
|
Solid = 1, // Solid collision - blocks movement
|
||||||
|
Trigger = 2, // Trigger - fires events but doesn't block
|
||||||
|
Platform = 3 // Platform - solid from above, passable from below
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Object behavior flags for PS1 runtime
|
||||||
|
/// </summary>
|
||||||
|
[System.Flags]
|
||||||
|
public enum PSXObjectFlags
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Static = 1 << 0, // Object never moves (can be optimized)
|
||||||
|
Dynamic = 1 << 1, // Object can move
|
||||||
|
Visible = 1 << 2, // Object is rendered
|
||||||
|
CastsShadow = 1 << 3, // Object casts shadows (future)
|
||||||
|
ReceivesShadow = 1 << 4, // Object receives shadows (future)
|
||||||
|
Interactable = 1 << 5, // Player can interact with this
|
||||||
|
AlwaysRender = 1 << 6, // Skip frustum culling for this object
|
||||||
|
}
|
||||||
|
|
||||||
|
[RequireComponent(typeof(Renderer))]
|
||||||
|
public class PSXObjectExporter : MonoBehaviour, IPSXExportable
|
||||||
|
{
|
||||||
|
public LuaFile LuaFile => luaFile;
|
||||||
|
|
||||||
|
[FormerlySerializedAs("IsActive")]
|
||||||
|
[SerializeField] private bool isActive = true;
|
||||||
|
public bool IsActive => isActive;
|
||||||
|
|
||||||
|
public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>();
|
||||||
|
public PSXMesh Mesh { get; protected set; }
|
||||||
|
|
||||||
[Header("Export Settings")]
|
[Header("Export Settings")]
|
||||||
[FormerlySerializedAs("BitDepth")]
|
[FormerlySerializedAs("BitDepth")]
|
||||||
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT; // Defines the bit depth of the texture (e.g., 4BPP, 8BPP)
|
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT;
|
||||||
|
[SerializeField] private LuaFile luaFile;
|
||||||
|
|
||||||
|
[Header("Object Flags")]
|
||||||
|
[SerializeField] private PSXObjectFlags objectFlags = PSXObjectFlags.Static | PSXObjectFlags.Visible;
|
||||||
|
|
||||||
|
[Header("Collision Settings")]
|
||||||
|
[SerializeField] private PSXCollisionType collisionType = PSXCollisionType.None;
|
||||||
|
[SerializeField] private bool exportCollisionMesh = false;
|
||||||
|
[SerializeField] private Mesh customCollisionMesh; // Optional simplified collision mesh
|
||||||
|
[Tooltip("Layer mask for collision detection (1-8)")]
|
||||||
|
[Range(1, 8)]
|
||||||
|
[SerializeField] private int collisionLayer = 1;
|
||||||
|
|
||||||
|
[Header("Navigation")]
|
||||||
|
[Tooltip("Include this object's walkable surfaces in nav region generation")]
|
||||||
|
[SerializeField] private bool generateNavigation = false;
|
||||||
|
|
||||||
[Header("Gizmo Settings")]
|
[Header("Gizmo Settings")]
|
||||||
[FormerlySerializedAs("PreviewNormals")]
|
[FormerlySerializedAs("PreviewNormals")]
|
||||||
[SerializeField] private bool previewNormals = false;
|
[SerializeField] private bool previewNormals = false;
|
||||||
[SerializeField] private float normalPreviewLength = 0.5f; // Length of the normal lines
|
[SerializeField] private float normalPreviewLength = 0.5f;
|
||||||
|
[SerializeField] private bool showCollisionBounds = true;
|
||||||
|
|
||||||
|
// Public accessors for editor and export
|
||||||
|
public PSXBPP BitDepth => bitDepth;
|
||||||
|
public PSXCollisionType CollisionType => collisionType;
|
||||||
|
public bool ExportCollisionMesh => exportCollisionMesh;
|
||||||
|
public Mesh CustomCollisionMesh => customCollisionMesh;
|
||||||
|
public int CollisionLayer => collisionLayer;
|
||||||
|
public PSXObjectFlags ObjectFlags => objectFlags;
|
||||||
|
public bool GenerateNavigation => generateNavigation;
|
||||||
|
|
||||||
|
// For assigning texture from editor
|
||||||
|
public Texture2D texture;
|
||||||
|
|
||||||
private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new();
|
private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new();
|
||||||
|
|
||||||
@@ -31,31 +96,85 @@ namespace SplashEdit.RuntimeCode
|
|||||||
Vector3[] vertices = mesh.vertices;
|
Vector3[] vertices = mesh.vertices;
|
||||||
Vector3[] normals = mesh.normals;
|
Vector3[] normals = mesh.normals;
|
||||||
|
|
||||||
Gizmos.color = Color.green; // Normal color
|
Gizmos.color = Color.green;
|
||||||
|
|
||||||
for (int i = 0; i < vertices.Length; i++)
|
for (int i = 0; i < vertices.Length; i++)
|
||||||
{
|
{
|
||||||
Vector3 worldVertex = transform.TransformPoint(vertices[i]); // Convert to world space
|
Vector3 worldVertex = transform.TransformPoint(vertices[i]);
|
||||||
Vector3 worldNormal = transform.TransformDirection(normals[i]); // Transform normal to world space
|
Vector3 worldNormal = transform.TransformDirection(normals[i]);
|
||||||
|
|
||||||
Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength);
|
Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
// Draw collision bounds when object is selected
|
||||||
|
if (showCollisionBounds && collisionType != PSXCollisionType.None)
|
||||||
|
{
|
||||||
|
MeshFilter filter = GetComponent<MeshFilter>();
|
||||||
|
Mesh collisionMesh = customCollisionMesh != null ? customCollisionMesh : (filter?.sharedMesh);
|
||||||
|
|
||||||
|
if (collisionMesh != null)
|
||||||
|
{
|
||||||
|
Bounds bounds = collisionMesh.bounds;
|
||||||
|
|
||||||
|
// Choose color based on collision type
|
||||||
|
switch (collisionType)
|
||||||
|
{
|
||||||
|
case PSXCollisionType.Solid:
|
||||||
|
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.5f); // Red
|
||||||
|
break;
|
||||||
|
case PSXCollisionType.Trigger:
|
||||||
|
Gizmos.color = new Color(0.3f, 1f, 0.3f, 0.5f); // Green
|
||||||
|
break;
|
||||||
|
case PSXCollisionType.Platform:
|
||||||
|
Gizmos.color = new Color(0.3f, 0.3f, 1f, 0.5f); // Blue
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw AABB
|
||||||
|
Matrix4x4 oldMatrix = Gizmos.matrix;
|
||||||
|
Gizmos.matrix = transform.localToWorldMatrix;
|
||||||
|
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||||
|
|
||||||
|
// Draw filled with lower alpha
|
||||||
|
Color fillColor = Gizmos.color;
|
||||||
|
fillColor.a = 0.1f;
|
||||||
|
Gizmos.color = fillColor;
|
||||||
|
Gizmos.DrawCube(bounds.center, bounds.size);
|
||||||
|
|
||||||
|
Gizmos.matrix = oldMatrix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts the object's material texture into a PlayStation-compatible texture.
|
|
||||||
/// </summary>
|
|
||||||
///
|
|
||||||
public void CreatePSXTextures2D()
|
public void CreatePSXTextures2D()
|
||||||
{
|
{
|
||||||
Renderer renderer = GetComponent<Renderer>();
|
Renderer renderer = GetComponent<Renderer>();
|
||||||
Textures.Clear();
|
Textures.Clear();
|
||||||
if (renderer != null)
|
if (renderer != null)
|
||||||
{
|
{
|
||||||
|
// If an override texture is set, use it for all submeshes
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
PSXTexture2D tex;
|
||||||
|
if (cache.ContainsKey((texture.GetInstanceID(), bitDepth)))
|
||||||
|
{
|
||||||
|
tex = cache[(texture.GetInstanceID(), bitDepth)];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tex = PSXTexture2D.CreateFromTexture2D(texture, bitDepth);
|
||||||
|
tex.OriginalTexture = texture;
|
||||||
|
cache.Add((texture.GetInstanceID(), bitDepth), tex);
|
||||||
|
}
|
||||||
|
Textures.Add(tex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Material[] materials = renderer.sharedMaterials;
|
Material[] materials = renderer.sharedMaterials;
|
||||||
|
|
||||||
foreach (Material mat in materials)
|
foreach (Material mat in materials)
|
||||||
@@ -65,14 +184,12 @@ namespace SplashEdit.RuntimeCode
|
|||||||
Texture mainTexture = mat.mainTexture;
|
Texture mainTexture = mat.mainTexture;
|
||||||
Texture2D tex2D = null;
|
Texture2D tex2D = null;
|
||||||
|
|
||||||
// Check if it's already a Texture2D
|
|
||||||
if (mainTexture is Texture2D existingTex2D)
|
if (mainTexture is Texture2D existingTex2D)
|
||||||
{
|
{
|
||||||
tex2D = existingTex2D;
|
tex2D = existingTex2D;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If not a Texture2D, try to convert
|
|
||||||
tex2D = ConvertToTexture2D(mainTexture);
|
tex2D = ConvertToTexture2D(mainTexture);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +203,7 @@ namespace SplashEdit.RuntimeCode
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
|
tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
|
||||||
tex.OriginalTexture = tex2D; // Store reference to the original texture
|
tex.OriginalTexture = tex2D;
|
||||||
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
|
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
|
||||||
}
|
}
|
||||||
Textures.Add(tex);
|
Textures.Add(tex);
|
||||||
@@ -98,10 +215,8 @@ namespace SplashEdit.RuntimeCode
|
|||||||
|
|
||||||
private Texture2D ConvertToTexture2D(Texture texture)
|
private Texture2D ConvertToTexture2D(Texture texture)
|
||||||
{
|
{
|
||||||
// Create a new Texture2D with the same dimensions and format
|
|
||||||
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
|
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
|
||||||
|
|
||||||
// Read the texture pixels
|
|
||||||
RenderTexture currentActiveRT = RenderTexture.active;
|
RenderTexture currentActiveRT = RenderTexture.active;
|
||||||
RenderTexture.active = texture as RenderTexture;
|
RenderTexture.active = texture as RenderTexture;
|
||||||
|
|
||||||
@@ -112,9 +227,16 @@ namespace SplashEdit.RuntimeCode
|
|||||||
|
|
||||||
return texture2D;
|
return texture2D;
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// Converts the object's mesh into a PlayStation-compatible mesh.
|
public PSXTexture2D GetTexture(int index)
|
||||||
/// </summary>
|
{
|
||||||
|
if (index >= 0 && index < Textures.Count)
|
||||||
|
{
|
||||||
|
return Textures[index];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void CreatePSXMesh(float GTEScaling)
|
public void CreatePSXMesh(float GTEScaling)
|
||||||
{
|
{
|
||||||
Renderer renderer = GetComponent<Renderer>();
|
Renderer renderer = GetComponent<Renderer>();
|
||||||
|
|||||||
@@ -1,11 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: bea0f31a495202580ac77bd9fd6e99f2
|
guid: a192e0a30d827ba40be5c99d32a83a12
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {fileID: 2800000, guid: e11677149a517ca5186e32dfda3ec088, type: 3}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.AI;
|
|
||||||
using UnityEngine.Serialization;
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
|
|
||||||
@@ -7,27 +6,81 @@ namespace SplashEdit.RuntimeCode
|
|||||||
{
|
{
|
||||||
public class PSXPlayer : MonoBehaviour
|
public class PSXPlayer : MonoBehaviour
|
||||||
{
|
{
|
||||||
private const float LookOutDistance = 1000f;
|
[Header("Player Dimensions")]
|
||||||
|
|
||||||
[FormerlySerializedAs("PlayerHeight")]
|
[FormerlySerializedAs("PlayerHeight")]
|
||||||
[SerializeField] private float playerHeight;
|
[Tooltip("Camera eye height above the player's feet")]
|
||||||
|
[SerializeField] private float playerHeight = 1.8f;
|
||||||
|
|
||||||
|
[Tooltip("Collision radius for wall sliding")]
|
||||||
|
[SerializeField] private float playerRadius = 0.5f;
|
||||||
|
|
||||||
|
[Header("Movement")]
|
||||||
|
[Tooltip("Walk speed in world units per second")]
|
||||||
|
[SerializeField] private float moveSpeed = 3.0f;
|
||||||
|
|
||||||
|
[Tooltip("Sprint speed in world units per second")]
|
||||||
|
[SerializeField] private float sprintSpeed = 8.0f;
|
||||||
|
|
||||||
|
[Header("Navigation")]
|
||||||
|
[Tooltip("Maximum height the agent can step up")]
|
||||||
|
[SerializeField] private float maxStepHeight = 0.35f;
|
||||||
|
|
||||||
|
[Tooltip("Maximum walkable slope angle in degrees")]
|
||||||
|
[SerializeField] private float walkableSlopeAngle = 46.0f;
|
||||||
|
|
||||||
|
[Tooltip("Voxel size in XZ plane (smaller = more accurate but slower)")]
|
||||||
|
[SerializeField] private float navCellSize = 0.05f;
|
||||||
|
|
||||||
|
[Tooltip("Voxel height (smaller = more accurate vertical resolution)")]
|
||||||
|
[SerializeField] private float navCellHeight = 0.025f;
|
||||||
|
|
||||||
|
[Header("Jump & Gravity")]
|
||||||
|
[Tooltip("Peak jump height in world units")]
|
||||||
|
[SerializeField] private float jumpHeight = 2.0f;
|
||||||
|
|
||||||
|
[Tooltip("Downward acceleration in world units per second squared (positive value)")]
|
||||||
|
[SerializeField] private float gravity = 20.0f;
|
||||||
|
|
||||||
|
// Public accessors
|
||||||
public float PlayerHeight => playerHeight;
|
public float PlayerHeight => playerHeight;
|
||||||
|
public float PlayerRadius => playerRadius;
|
||||||
|
public float MoveSpeed => moveSpeed;
|
||||||
|
public float SprintSpeed => sprintSpeed;
|
||||||
|
public float MaxStepHeight => maxStepHeight;
|
||||||
|
public float WalkableSlopeAngle => walkableSlopeAngle;
|
||||||
|
public float NavCellSize => navCellSize;
|
||||||
|
public float NavCellHeight => navCellHeight;
|
||||||
|
public float JumpHeight => jumpHeight;
|
||||||
|
public float Gravity => gravity;
|
||||||
public Vector3 CamPoint { get; protected set; }
|
public Vector3 CamPoint { get; protected set; }
|
||||||
|
|
||||||
public void FindNavmesh()
|
public void FindNavmesh()
|
||||||
{
|
{
|
||||||
if (NavMesh.SamplePosition(transform.position, out NavMeshHit hit, LookOutDistance, NavMesh.AllAreas))
|
// Raycast down from the transform to find the ground,
|
||||||
|
// then place CamPoint at ground + playerHeight
|
||||||
|
if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, 100f))
|
||||||
{
|
{
|
||||||
CamPoint = hit.position + new Vector3(0, PlayerHeight, 0);
|
CamPoint = hit.point + new Vector3(0, playerHeight, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: no ground hit, use transform directly
|
||||||
|
CamPoint = transform.position + new Vector3(0, playerHeight, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnDrawGizmos()
|
void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
FindNavmesh();
|
FindNavmesh();
|
||||||
|
|
||||||
|
// Red sphere at camera eye point
|
||||||
Gizmos.color = Color.red;
|
Gizmos.color = Color.red;
|
||||||
Gizmos.DrawSphere(CamPoint, 0.2f);
|
Gizmos.DrawSphere(CamPoint, 0.2f);
|
||||||
|
|
||||||
|
// Wireframe sphere at feet showing player radius
|
||||||
|
Gizmos.color = new Color(0f, 1f, 0f, 0.3f);
|
||||||
|
Vector3 feet = CamPoint - new Vector3(0, playerHeight, 0);
|
||||||
|
Gizmos.DrawWireSphere(feet, playerRadius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,2 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: dee32f3a19300d7a3aae7424f01c9332
|
guid: 3cc71b54d0db2604087cd4ae7781dc98
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {fileID: 2800000, guid: 4d7bd095e76e6f3df976224b15405059, type: 3}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user