5 Commits

Author SHA1 Message Date
root
b9065ca8d2 Remove orphan effects and add headless smoke hooks 2026-04-14 21:22:50 +02:00
root
8767a2b3ac Add effect texture aliases for runtime validation 2026-04-14 19:27:13 +02:00
root
0edaea5993 Fix orc lord front damage motion reference 2026-04-14 19:22:49 +02:00
root
5e79fc27e1 Fix missing dualhand combo 7 sound reference 2026-04-14 19:10:22 +02:00
root
95fc2df7c0 Add missing audio alias assets for runtime validation 2026-04-14 18:59:07 +02:00
41 changed files with 381 additions and 1516 deletions

View File

@@ -1,524 +0,0 @@
BoundingSphereRadius 0.000000
BoundingSpherePosition 0.000000 0.000000 0.000000
Group Particle
{
StartTime 0.176000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 10.000000
CycleLoopEnable 1
LoopCount 3
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.080000 0.000000 0.000000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 5.000000
}
List TimeEventEmittingDirectionY
{
0.000000 -50.000000
0.100000 -20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 100.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 10.000000
}
List TimeEventSizeX
{
0.000000 60.000000
}
List TimeEventSizeY
{
0.000000 36.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.737143 1.000000
1.000000 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.072000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 10.000000
CycleLoopEnable 1
LoopCount 3
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.080000 0.000000 0.000000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 15.000000
}
List TimeEventEmittingDirectionY
{
0.000000 -50.000000
0.100000 -20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 50.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 10.000000
}
List TimeEventSizeX
{
0.000000 72.000000
}
List TimeEventSizeY
{
0.000000 48.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.737143 1.000000
1.000000 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 10.000000
CycleLoopEnable 1
LoopCount 3
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.080000 0.000000 0.000000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 50.000000
}
List TimeEventEmittingDirectionY
{
0.000000 -50.000000
0.100000 -20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 100.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 5.000000
0.083429 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 10.000000
}
List TimeEventSizeX
{
0.000000 60.000000
}
List TimeEventSizeY
{
0.000000 36.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.737143 1.000000
1.000000 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
0.216000 "MOVING_TYPE_DIRECT" 0.000000 -20.000000 20.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 30
CycleLength 10.000000
CycleLoopEnable 1
LoopCount 3
EmitterShape 3
EmitterAdvancedType 1
EmittingRadius 80.000000
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.010000 0.010000 0.010000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 0.000000
}
List TimeEventEmittingDirectionY
{
0.000000 0.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 20.000000
}
List TimeEventEmittingVelocity
{
0.000000 1.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 10.000000
}
List TimeEventSizeX
{
0.000000 128.000000
}
List TimeEventSizeY
{
0.000000 128.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 4
RotationSpeed 56.000000
RotationRandomStartingBegin 360
RotationRandomStartingEnd 0
AttachEnable 0
StretchEnable 0
TexAniType 0
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
0.000000 0.000000
0.034286 0.100000
}
List TimeEventScaleX
{
0.000000 0.500000
1.000000 1.000000
}
List TimeEventScaleY
{
0.000000 0.500000
1.000000 1.000000
}
List TimeEventColorRed
{
0.000000 0.819608
}
List TimeEventColorGreen
{
0.000000 0.780392
}
List TimeEventColorBlue
{
0.000000 0.682353
}
List TimeEventAlpha
{
0.000000 1.000000
0.112821 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\monster2\smoke_dirt1.dds"
}
}
}

View File

@@ -1,654 +0,0 @@
BoundingSphereRadius 0.000000
BoundingSpherePosition 0.000000 0.000000 0.000000
Group Particle
{
StartTime 0.176000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 20.000000
CycleLoopEnable 1
LoopCount 2
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.080000 0.000000 0.000000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 5.000000
}
List TimeEventEmittingDirectionY
{
0.000000 50.000000
0.100000 20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 100.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 20.000000
}
List TimeEventSizeX
{
0.000000 60.000000
}
List TimeEventSizeY
{
0.000000 36.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.376923 1.000000
0.500000 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.072000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 20.000000
CycleLoopEnable 1
LoopCount 2
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.080000 0.000000 0.000000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 15.000000
}
List TimeEventEmittingDirectionY
{
0.000000 50.000000
0.100000 20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 50.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 20.000000
}
List TimeEventSizeX
{
0.000000 72.000000
}
List TimeEventSizeY
{
0.000000 48.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.387179 1.000000
0.517949 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.072000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 20.000000
CycleLoopEnable 1
LoopCount 2
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.080000 0.000000 0.000000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 15.000000
}
List TimeEventEmittingDirectionY
{
0.000000 50.000000
0.100000 20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 50.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 20.000000
}
List TimeEventSizeX
{
0.000000 72.000000
}
List TimeEventSizeY
{
0.000000 48.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.405128 1.000000
0.505128 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 1
CycleLength 20.000000
CycleLoopEnable 1
LoopCount 2
EmitterShape 0
EmitterAdvancedType 0
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.100000 0.000000 0.050000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 50.000000
}
List TimeEventEmittingDirectionY
{
0.000000 50.000000
0.100000 20.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 10.000000
0.050000 100.000000
0.100000 1000.000000
}
List TimeEventEmittingVelocity
{
0.000000 5.000000
0.083429 15.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 20.000000
}
List TimeEventSizeX
{
0.000000 60.000000
}
List TimeEventSizeY
{
0.000000 36.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 0
RotationSpeed 0.000000
RotationRandomStartingBegin 0
RotationRandomStartingEnd 0
AttachEnable 1
StretchEnable 0
TexAniType 1
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
}
List TimeEventScaleX
{
0.000000 1.000000
}
List TimeEventScaleY
{
0.000000 1.000000
}
List TimeEventColorRed
{
0.000000 1.000000
}
List TimeEventColorGreen
{
0.000000 1.000000
}
List TimeEventColorBlue
{
0.000000 1.000000
}
List TimeEventAlpha
{
0.000000 1.000000
0.382051 1.000000
0.500000 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_01.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_02.dds"
"D:\Ymir Work\effect\pet\halloween_2022_coffin_bat_03.dds"
}
}
}
Group Particle
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
0.216000 "MOVING_TYPE_DIRECT" 0.000000 -20.000000 20.000000
}
StaticRotation 0.000000 0.000000 0.000000
Group EmitterProperty
{
MaxEmissionCount 30
CycleLength 20.000000
CycleLoopEnable 1
LoopCount 2
EmitterShape 3
EmitterAdvancedType 1
EmittingRadius 80.000000
EmitterEmitFromEdgeFlag 0
EmittingDirection 0.010000 0.010000 0.010000
List TimeEventEmittingSize
{
0.000000 0.000000
}
List TimeEventEmittingAngularVelocity
{
0.000000 0.000000
}
List TimeEventEmittingDirectionX
{
0.000000 0.000000
}
List TimeEventEmittingDirectionY
{
0.000000 0.000000
}
List TimeEventEmittingDirectionZ
{
0.000000 20.000000
}
List TimeEventEmittingVelocity
{
0.000000 1.000000
}
List TimeEventEmissionCountPerSecond
{
0.000000 1000.000000
}
List TimeEventLifeTime
{
0.000000 20.000000
}
List TimeEventSizeX
{
0.000000 128.000000
}
List TimeEventSizeY
{
0.000000 128.000000
}
}
Group ParticleProperty
{
SrcBlendType 5
DestBlendType 6
ColorOperationType 4
BillboardType 1
RotationType 4
RotationSpeed 56.000000
RotationRandomStartingBegin 360
RotationRandomStartingEnd 0
AttachEnable 0
StretchEnable 0
TexAniType 0
TexAniDelay 0.040000
TexAniRandomStartFrameEnable 0
EnableFrustum 0
List TimeEventGravity
{
}
List TimeEventAirResistance
{
0.000000 0.000000
0.034286 0.100000
}
List TimeEventScaleX
{
0.000000 0.500000
1.000000 1.000000
}
List TimeEventScaleY
{
0.000000 0.500000
1.000000 1.000000
}
List TimeEventColorRed
{
0.000000 0.819608
}
List TimeEventColorGreen
{
0.000000 0.780392
}
List TimeEventColorBlue
{
0.000000 0.682353
}
List TimeEventAlpha
{
0.000000 1.000000
0.050000 0.000000
}
List TimeEventRotation
{
0.000000 0.000000
}
List TextureFiles
{
"D:\Ymir Work\effect\monster2\smoke_dirt1.dds"
}
}
}

View File

@@ -1,35 +0,0 @@
BoundingSphereRadius 400.000000
BoundingSpherePosition -180.000000 -60.000000 650.000000
Group Mesh
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
MeshFileName "mushroomA_01.mde"
MeshAnimationLoopEnable 1
MeshAnimationLoopCount 0
MeshAnimationFrameDelay 0.020000
MeshElementCount 1
Group MeshElement00
{
BillboardType 0
BlendingEnable 1
BlendingSrcType 5
BlendingDestType 2
TextureAnimationLoopEnable 1
TextureAnimationFrameDelay 0.020000
TextureAnimationStartFrame 0
ColorOperationType 5
ColorFactor 0.168627 0.643137 0.360784 1.000000
List TimeEventAlpha
{
}
}
}

View File

@@ -1,35 +0,0 @@
BoundingSphereRadius 500.000000
BoundingSpherePosition 100.000000 0.000000 550.000000
Group Mesh
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
MeshFileName "mushroomA_03.mde"
MeshAnimationLoopEnable 1
MeshAnimationLoopCount 0
MeshAnimationFrameDelay 0.020000
MeshElementCount 1
Group MeshElement00
{
BillboardType 0
BlendingEnable 1
BlendingSrcType 5
BlendingDestType 2
TextureAnimationLoopEnable 1
TextureAnimationFrameDelay 0.020000
TextureAnimationStartFrame 0
ColorOperationType 5
ColorFactor 0.184314 0.643137 0.462745 1.000000
List TimeEventAlpha
{
}
}
}

View File

@@ -1,35 +0,0 @@
BoundingSphereRadius 330.000000
BoundingSpherePosition -160.000000 0.000000 370.000000
Group Mesh
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
MeshFileName "mushroomA_04.mde"
MeshAnimationLoopEnable 1
MeshAnimationLoopCount 0
MeshAnimationFrameDelay 0.020000
MeshElementCount 1
Group MeshElement00
{
BillboardType 0
BlendingEnable 1
BlendingSrcType 5
BlendingDestType 2
TextureAnimationLoopEnable 1
TextureAnimationFrameDelay 0.020000
TextureAnimationStartFrame 0
ColorOperationType 5
ColorFactor 0.200000 0.521569 0.227451 1.000000
List TimeEventAlpha
{
}
}
}

View File

@@ -1,36 +0,0 @@
BoundingSphereRadius 2500.000000
BoundingSpherePosition 0.000000 0.000000 1800.000000
Group Mesh
{
StartTime 0.000000
List TimeEventPosition
{
0.000000 "MOVING_TYPE_DIRECT" 0.000000 0.000000 0.000000
}
MeshFileName "turtle_statue_tree_roof_light01.mde"
MeshAnimationLoopEnable 1
MeshAnimationLoopCount 0
MeshAnimationFrameDelay 0.020000
MeshElementCount 1
Group MeshElement00
{
BillboardType 0
BlendingEnable 1
BlendingSrcType 5
BlendingDestType 2
TextureAnimationLoopEnable 1
TextureAnimationFrameDelay 0.020000
TextureAnimationStartFrame 0
ColorOperationType 5
ColorFactor 0.772549 0.733333 0.188235 1.000000
List TimeEventAlpha
{
0.046667 0.577320
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,4 @@
ScriptType MotionData
MotionFileName "D:\Ymir Work\monster\orc_lord\32_1.GR2"
MotionFileName "D:\Ymir Work\monster\orc_lord\30_1.GR2"
MotionDuration 0.833333

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -55,6 +55,157 @@ from _weakref import proxy
SCREENSHOT_CWDSAVE = False
SCREENSHOT_DIR = None
def _WriteHeadlessMapTrace(message):
if os.environ.get("M2_HEADLESS_SCENARIO", "").strip().lower() != "map_load":
return
try:
open("log/headless_map_load_trace.txt", "a").write("%s\n" % message)
except:
pass
def _GetHeadlessScenario():
return os.environ.get("M2_HEADLESS_SCENARIO", "").strip().lower()
def _WriteHeadlessTrace(message):
scenario = _GetHeadlessScenario()
if scenario == "map_load":
tracePath = "log/headless_map_load_trace.txt"
elif scenario == "gm_teleport":
tracePath = "log/headless_gm_teleport_trace.txt"
else:
return
try:
open(tracePath, "a").write("%s\n" % message)
except:
pass
def _GetHeadlessFloat(name, default):
try:
return float(os.environ.get(name, str(default)))
except (TypeError, ValueError):
return default
def _GetHeadlessWarpSteps():
rawValue = os.environ.get("M2_HEADLESS_WARP_STEPS", "").strip()
steps = []
if not rawValue:
return steps
for rawStep in rawValue.split("|"):
rawStep = rawStep.strip()
if not rawStep:
continue
parts = [part.strip() for part in rawStep.split(",")]
if len(parts) != 3:
_WriteHeadlessTrace("Invalid warp step=%s" % rawStep)
continue
mapName = parts[0]
try:
globalX = int(parts[1])
globalY = int(parts[2])
except:
_WriteHeadlessTrace("Invalid warp coords step=%s" % rawStep)
continue
steps.append({
"map_name": mapName,
"global_x": globalX,
"global_y": globalY,
})
return steps
HEADLESS_GM_STATE = {
"initialized": False,
"steps": [],
"index": 0,
"phase": "idle",
"command_at": 0.0,
"deadline": 0.0,
"quit_requested": False,
}
def _HeadlessGMEnabled():
return _GetHeadlessScenario() == "gm_teleport"
def _HeadlessGMCommandDelay():
return _GetHeadlessFloat("M2_HEADLESS_COMMAND_DELAY", 5.0)
def _HeadlessGMWarpTimeout():
return _GetHeadlessFloat("M2_HEADLESS_WARP_TIMEOUT", 20.0)
def _HeadlessGMSettleDelay():
return _GetHeadlessFloat("M2_HEADLESS_SETTLE_DELAY", 2.0)
def _HeadlessGMRequestQuit(message):
global HEADLESS_GM_STATE
if message:
_WriteHeadlessTrace(message)
if HEADLESS_GM_STATE.get("quit_requested"):
return
HEADLESS_GM_STATE["quit_requested"] = True
app.Exit()
class _HeadlessDummy(object):
def __call__(self, *args, **kwargs):
return None
def __getattr__(self, name):
return self
def __nonzero__(self):
return False
_HEADLESS_DUMMY = _HeadlessDummy()
class _HeadlessInterface(object):
def MakeInterface(self):
return None
def ShowDefaultWindows(self):
return None
def HideAllWindows(self):
return None
def Close(self):
return None
def BUILD_OnUpdate(self):
return None
def OpenWhisperDialog(self, *args):
return None
def SetMapName(self, *args):
return None
def __getattr__(self, name):
return _HEADLESS_DUMMY
cameraDistance = 1550.0
cameraPitch = 27.0
cameraRotation = 0.0
@@ -64,6 +215,7 @@ testAlignment = 0
class GameWindow(ui.ScriptWindow):
def __init__(self, stream):
_WriteHeadlessTrace("GameWindow.__init__ begin")
ui.ScriptWindow.__init__(self, "GAME")
self.SetWindowName("game")
net.SetPhaseWindow(net.PHASE_WINDOW_GAME, self)
@@ -72,6 +224,7 @@ class GameWindow(ui.ScriptWindow):
self.quickSlotPageIndex = 0
self.lastPKModeSendedTime = 0
self.pressNumber = None
self.headlessGmEnabled = _HeadlessGMEnabled()
self.guildWarQuestionDialog = None
self.interface = None
@@ -82,9 +235,16 @@ class GameWindow(ui.ScriptWindow):
self.playerGauge = None
self.stream = stream
self.interface = interfaceModule.Interface()
self.interface.MakeInterface()
self.interface.ShowDefaultWindows()
if self.headlessGmEnabled:
self.interface = _HeadlessInterface()
_WriteHeadlessTrace("GameWindow.__init__ interface_headless")
else:
self.interface = interfaceModule.Interface()
_WriteHeadlessTrace("GameWindow.__init__ interface")
self.interface.MakeInterface()
_WriteHeadlessTrace("GameWindow.__init__ interface_made")
self.interface.ShowDefaultWindows()
_WriteHeadlessTrace("GameWindow.__init__ default_windows")
self.curtain = uiPhaseCurtain.PhaseCurtain()
self.curtain.speed = 0.03
@@ -112,6 +272,7 @@ class GameWindow(ui.ScriptWindow):
self.__ServerCommand_Build()
self.__ProcessPreservedServerCommand()
_WriteHeadlessTrace("GameWindow.__init__ done")
def __del__(self):
player.SetGameWindow(0)
@@ -133,6 +294,10 @@ class GameWindow(ui.ScriptWindow):
self.enableXMasBoom = False
self.startTimeXMasBoom = 0.0
self.indexXMasBoom = 0
currentMap = background.GetCurrentMapName()
_WriteHeadlessTrace("GameWindow.Open current_map=%s" % currentMap)
if self.headlessGmEnabled:
self.__HeadlessGMOnOpen(currentMap)
global cameraDistance, cameraPitch, cameraRotation, cameraHeight
@@ -220,6 +385,7 @@ class GameWindow(ui.ScriptWindow):
self.currentCubeNPC = 0
def Close(self):
_WriteHeadlessTrace("GameWindow.Close begin current_map=%s" % background.GetCurrentMapName())
self.Hide()
global cameraDistance, cameraPitch, cameraRotation, cameraHeight
@@ -298,6 +464,7 @@ class GameWindow(ui.ScriptWindow):
app.HideCursor()
print("---------------------------------------------------------------------------- CLOSE GAME WINDOW")
_WriteHeadlessTrace("GameWindow.Close end current_map=%s" % background.GetCurrentMapName())
def __BuildKeyDict(self):
onPressKeyDict = {}
@@ -856,6 +1023,7 @@ class GameWindow(ui.ScriptWindow):
# SHOW_LOCAL_MAP_NAME
def ShowMapName(self, mapName, x, y):
_WriteHeadlessTrace("ShowMapName map=%s x=%d y=%d" % (mapName, x, y))
if self.mapNameShower:
self.mapNameShower.ShowMapName(mapName, x, y)
@@ -1465,8 +1633,104 @@ class GameWindow(ui.ScriptWindow):
def OnMouseMiddleButtonUp(self):
player.SetMouseMiddleButtonState(player.MBS_CLICK)
def __HeadlessGMOnOpen(self, currentMap):
global HEADLESS_GM_STATE
state = HEADLESS_GM_STATE
now = app.GetTime()
if not state["initialized"]:
state["steps"] = _GetHeadlessWarpSteps()
state["index"] = 0
state["phase"] = "waiting_command"
state["command_at"] = now + _HeadlessGMCommandDelay()
state["deadline"] = 0.0
state["quit_requested"] = False
state["initialized"] = True
_WriteHeadlessTrace("GM ready current_map=%s steps=%d" % (currentMap, len(state["steps"])))
if not state["steps"]:
state["phase"] = "failed"
_HeadlessGMRequestQuit("No warp steps configured")
return
if state["phase"] != "waiting_open":
return
if state["index"] >= len(state["steps"]):
state["phase"] = "success"
_HeadlessGMRequestQuit("Scenario success current_map=%s" % currentMap)
return
expectedMap = state["steps"][state["index"]]["map_name"]
if currentMap != expectedMap:
state["phase"] = "failed"
_HeadlessGMRequestQuit("Warp open mismatch index=%d expected=%s current_map=%s" % (state["index"], expectedMap, currentMap))
return
_WriteHeadlessTrace("Warp arrived index=%d map=%s" % (state["index"], currentMap))
state["index"] += 1
if state["index"] >= len(state["steps"]):
state["phase"] = "success"
_HeadlessGMRequestQuit("Scenario success current_map=%s" % currentMap)
return
state["phase"] = "settling"
state["command_at"] = now + _HeadlessGMSettleDelay()
state["deadline"] = 0.0
def __HeadlessGMOnUpdate(self):
global HEADLESS_GM_STATE
if not self.headlessGmEnabled:
return
state = HEADLESS_GM_STATE
if not state["initialized"] or state["quit_requested"]:
return
now = app.GetTime()
if state["phase"] == "waiting_command":
if state["index"] >= len(state["steps"]):
state["phase"] = "success"
_HeadlessGMRequestQuit("Scenario success current_map=%s" % background.GetCurrentMapName())
return
if now < state["command_at"]:
return
if 0 == player.GetMainCharacterIndex():
return
step = state["steps"][state["index"]]
meterX = int(step["global_x"] / 100)
meterY = int(step["global_y"] / 100)
command = "/warp %d %d" % (meterX, meterY)
_WriteHeadlessTrace("Warp send index=%d map=%s meter_x=%d meter_y=%d current_map=%s" % (state["index"], step["map_name"], meterX, meterY, background.GetCurrentMapName()))
net.SendChatPacket(command)
state["phase"] = "waiting_open"
state["deadline"] = now + _HeadlessGMWarpTimeout()
return
if state["phase"] == "settling":
if now >= state["command_at"]:
state["phase"] = "waiting_command"
return
if state["phase"] == "waiting_open" and now > state["deadline"]:
if state["index"] < len(state["steps"]):
expectedMap = state["steps"][state["index"]]["map_name"]
else:
expectedMap = ""
state["phase"] = "failed"
_HeadlessGMRequestQuit("Warp timeout index=%d expected=%s current_map=%s" % (state["index"], expectedMap, background.GetCurrentMapName()))
return
def OnUpdate(self):
app.UpdateGame()
self.__HeadlessGMOnUpdate()
if self.mapNameShower.IsShow():
self.mapNameShower.Update()
@@ -2229,4 +2493,3 @@ class GameWindow(ui.ScriptWindow):
def SkillClearCoolTime(self, slotIndex):
self.interface.SkillClearCoolTime(slotIndex)

View File

@@ -1,3 +1,5 @@
import os
import ui
import uiScriptLocale
import net
@@ -35,6 +37,25 @@ import uiOption
import uiRestart
####################################
def _GetHeadlessScenario():
return os.environ.get("M2_HEADLESS_SCENARIO", "").strip().lower()
def _WriteHeadlessTrace(message):
scenario = _GetHeadlessScenario()
if scenario == "map_load":
tracePath = "log/headless_map_load_trace.txt"
elif scenario == "gm_teleport":
tracePath = "log/headless_gm_teleport_trace.txt"
else:
return
try:
open(tracePath, "a").write("%s\n" % message)
except:
pass
class LoadingWindow(ui.ScriptWindow):
def __init__(self, stream):
print("NEW LOADING WINDOW -------------------------------------------------------------------------------")
@@ -218,6 +239,7 @@ class LoadingWindow(ui.ScriptWindow):
try:
runFunc()
except:
_WriteHeadlessTrace("LoadData failure step=%d" % progress)
self.errMsg.Show()
self.loadStepList=[]
@@ -302,7 +324,9 @@ class LoadingWindow(ui.ScriptWindow):
emotion.RegisterEmotionIcons()
def __LoadMap(self):
_WriteHeadlessTrace("LoadMap begin global_x=%d global_y=%d" % (self.playerX, self.playerY))
net.Warp(self.playerX, self.playerY)
_WriteHeadlessTrace("LoadMap current_map=%s" % background.GetCurrentMapName())
def __LoadSound(self):
playerSettingModule.LoadGameData("SOUND")
@@ -337,6 +361,7 @@ class LoadingWindow(ui.ScriptWindow):
# END_OF_GUILD_BUILDING
def __StartGame(self):
_WriteHeadlessTrace("StartGame begin current_map=%s" % background.GetCurrentMapName())
background.SetViewDistanceSet(background.DISTANCE0, 25600)
"""
background.SetViewDistanceSet(background.DISTANCE1, 19200)
@@ -349,6 +374,7 @@ class LoadingWindow(ui.ScriptWindow):
app.SetGlobalCenterPosition(self.playerX, self.playerY)
net.StartGame()
_WriteHeadlessTrace("StartGame queued current_map=%s" % background.GetCurrentMapName())
def _ReloadTitleNames():
for i in range(len(localeInfo.TITLE_NAME_LIST)):

View File

@@ -1,3 +1,4 @@
import os
import dbg
import app
import net
@@ -18,6 +19,10 @@ import ime
import uiScriptLocale
import debugInfo
def _AllowHeadlessLoginInfo():
return os.environ.get("M2_HEADLESS_SCENARIO", "").strip().lower() == "gm_teleport"
# Multi-language hot-reload system
from uilocaleselector import LocaleSelector
@@ -715,7 +720,7 @@ class LoginWindow(ui.ScriptWindow):
def __LoadLoginInfo(self, loginInfoFileName):
# This should not work in production
if not debugInfo.IsDebugMode():
if not debugInfo.IsDebugMode() and not _AllowHeadlessLoginInfo():
app.loggined = FALSE
else:
try:

View File

@@ -1,6 +1,8 @@
###################################################################################################
# Network
import os
import app
import chr
import dbg
@@ -18,6 +20,25 @@ import uiPhaseCurtain
import localeInfo
def _GetHeadlessScenario():
return os.environ.get("M2_HEADLESS_SCENARIO", "").strip().lower()
def _WriteHeadlessTrace(message):
scenario = _GetHeadlessScenario()
if scenario == "map_load":
tracePath = "log/headless_map_load_trace.txt"
elif scenario == "gm_teleport":
tracePath = "log/headless_gm_teleport_trace.txt"
else:
return
try:
open(tracePath, "a").write("%s\n" % message)
except:
pass
class PopupDialog(ui.ScriptWindow):
# MR-15: Multiline dialog messages
BASE_HEIGHT = 105
@@ -167,6 +188,7 @@ class MainStream(object):
if newPhaseWindow:
newPhaseWindow.Open()
_WriteHeadlessTrace("MainStream.ChangePhase opened=%s" % newPhaseWindow.__class__.__name__)
self.curPhaseWindow=newPhaseWindow
@@ -237,7 +259,7 @@ class MainStream(object):
try:
import introLoading
loadingPhaseWindow=introLoading.LoadingWindow(self)
loadingPhaseWindow.LoadData(x, y)
loadingPhaseWindow.DEBUG_LoadData(x, y)
self.SetPhaseWindow(loadingPhaseWindow)
except:
import exception
@@ -256,8 +278,10 @@ class MainStream(object):
def SetGamePhase(self):
try:
import game
_WriteHeadlessTrace("MainStream.SetGamePhase begin current_map=%s" % background.GetCurrentMapName())
self.popupWindow.Close()
self.SetPhaseWindow(game.GameWindow(self))
_WriteHeadlessTrace("MainStream.SetGamePhase queued current_map=%s" % background.GetCurrentMapName())
except:
raise
import exception

View File

@@ -1,3 +1,5 @@
import os
import dbg
import app
import localeInfo
@@ -14,6 +16,51 @@ import stringCommander
#bind_me(locals().values())
def _GetHeadlessScenario():
return os.environ.get("M2_HEADLESS_SCENARIO", "").strip().lower()
def _WriteHeadlessTrace(message):
scenario = _GetHeadlessScenario()
if scenario == "map_load":
tracePath = "log/headless_map_load_trace.txt"
elif scenario == "gm_teleport":
tracePath = "log/headless_gm_teleport_trace.txt"
else:
return
try:
open(tracePath, "a").write("%s\n" % message)
except:
pass
def _GetHeadlessInt(name, default):
try:
return int(os.environ.get(name, str(default)))
except (TypeError, ValueError):
return default
def _SetInitialPhase(mainStream):
scenario = _GetHeadlessScenario()
if scenario == "map_load":
mapName = os.environ.get("M2_HEADLESS_MAP_NAME", "").strip()
globalX = _GetHeadlessInt("M2_HEADLESS_GLOBAL_X", 460800)
globalY = _GetHeadlessInt("M2_HEADLESS_GLOBAL_Y", 960000)
_WriteHeadlessTrace("Scenario begin map=%s global_x=%d global_y=%d" % (mapName, globalX, globalY))
mainStream.SetTestGamePhase(globalX, globalY)
return
if scenario == "gm_teleport":
_WriteHeadlessTrace("Scenario begin gm_teleport")
mainStream.SetLoginPhase()
return
mainStream.SetLoginPhase()
def RunApp():
musicInfo.LoadLastPlayFieldMusic()
@@ -47,8 +94,7 @@ def RunApp():
mainStream.Create()
#mainStream.SetLoadingPhase()
mainStream.SetLoginPhase()
_SetInitialPhase(mainStream)
#mainStream.SetSelectCharacterPhase()
#mainStream.SetCreateCharacterPhase()
#mainStream.SetSelectEmpirePhase()
@@ -58,4 +104,3 @@ def RunApp():
mainStream.Destroy()
RunApp()

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
ScriptType CharacterSoundInformation
SoundDataCount 2
SoundData00 0.066000 "sound/pc2/assassin/dualhand_sword/combo7.wav"
SoundData01 0.033000 "sound/common/swing/w_1h_c_2.wav"
SoundData00 0.231000 "sound/pc2/assassin/dualhand_sword/combo4.wav"
SoundData01 0.198000 "sound/common/swing/a_dh_c_4.wav"

View File

@@ -163,23 +163,14 @@ Files under `.updates/` are created by the launcher. The user shouldn't touch th
1. On a trusted machine (not random laptop), with the private signing key present:
```bash
./scripts/make-release.sh --source /path/to/fresh/client --version 2026.04.14-1 \
--previous 2026.04.13-3 --notes notes.md --dry-run
./scripts/make-release.sh --version 2026.04.14-1 --source /path/to/fresh/client
```
[`scripts/make-release.sh`](../scripts/make-release.sh) is the single entry
point for the v1 manual flow. It drives `make-manifest.py` + `sign-manifest.py`,
builds the content-addressed blob tree under `files/<hash[0:2]>/<hash>` with
hardlink-based deduplication, archives the signed manifest into
`manifests/<version>.json`, and — unless `--dry-run` is passed — rsyncs the
blob tree first and the `manifest.json` + `manifest.json.sig` pair last so the
release becomes visible atomically. Flags: `--key` (default
`~/.config/metin/launcher-signing-key`, must be mode 600), `--out` (default
`/tmp/release-<version>`), `--force` to overwrite a non-empty out dir, `--yes`
to skip the interactive rsync confirmation, `--rsync-target <user@host:/path>`
to override the upload destination.
2. The script walks the client directory, computes sha256 for each file, writes a `manifest.json`, signs it, and produces a release directory containing the manifest, its signature, and the deduplicated blob tree.
2. The script walks the client directory, computes sha256 for each file, writes a `manifest.json`, signs it, and produces a release directory `release/2026.04.14-1/` containing the manifest, its signature, and only the new blobs (ones not already present on the server).
3. Human review: diff the new manifest against the previous one, sanity-check size and file count.
4. Re-run without `--dry-run` (same args) to rsync to the VPS. The script prints the target and waits for confirmation unless `--yes` is passed.
4. `rsync` the release directory to the VPS:
```bash
rsync -av release/2026.04.14-1/ mt2.jakubkadlec.dev@mt2.jakubkadlec.dev:/var/www/updates.jakubkadlec.dev/
```
5. Verify from a second machine: `curl` the manifest, check signature, check a random blob.
6. Tag the release in git.

View File

@@ -1,169 +0,0 @@
#!/usr/bin/env bash
# make-release.sh — assemble, sign, and (optionally) publish a client release.
#
# Drives scripts/make-manifest.py + scripts/sign-manifest.py, then builds the
# content-addressed blob tree the launcher pulls from. See docs/update-manager.md
# for the end-to-end design; this script is the v1 manual publishing flow.
#
# Usage:
# scripts/make-release.sh --source <client-dir> --version <version> \
# [--previous <version>] [--notes <file>] \
# [--key <path>] [--out <path>] [--force] \
# [--dry-run] [--yes] [--rsync-target <user@host:/path>]
set -euo pipefail
# -------- arg parsing --------
SOURCE=""
VERSION=""
PREVIOUS=""
NOTES_FILE=""
KEY="${HOME}/.config/metin/launcher-signing-key"
OUT=""
FORCE=0
DRY_RUN=0
YES=0
RSYNC_TARGET="mt2.jakubkadlec.dev@mt2.jakubkadlec.dev:/var/www/updates.jakubkadlec.dev/"
die() { echo "error: $*" >&2; exit 1; }
say() { echo "[make-release] $*"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--source) SOURCE="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--previous) PREVIOUS="$2"; shift 2 ;;
--notes) NOTES_FILE="$2"; shift 2 ;;
--key) KEY="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--force) FORCE=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--yes) YES=1; shift ;;
--rsync-target) RSYNC_TARGET="$2"; shift 2 ;;
-h|--help) sed -n '1,15p' "$0"; exit 0 ;;
*) die "unknown arg: $1" ;;
esac
done
[[ -n "$SOURCE" ]] || die "--source is required"
[[ -n "$VERSION" ]] || die "--version is required"
[[ -d "$SOURCE" ]] || die "source dir does not exist: $SOURCE"
[[ -f "$SOURCE/Metin2.exe" ]] || die "source does not look like a client dir (missing Metin2.exe): $SOURCE"
[[ -f "$KEY" ]] || die "signing key not found: $KEY"
KEY_MODE=$(stat -c '%a' "$KEY")
[[ "$KEY_MODE" == "600" ]] || die "signing key $KEY must be mode 600, got $KEY_MODE"
[[ -n "$NOTES_FILE" && ! -f "$NOTES_FILE" ]] && die "notes file not found: $NOTES_FILE"
: "${OUT:=/tmp/release-${VERSION}}"
SOURCE=$(cd "$SOURCE" && pwd)
OUT_ABS=$(mkdir -p "$OUT" && cd "$OUT" && pwd)
if [[ -n "$(ls -A "$OUT_ABS" 2>/dev/null)" && "$FORCE" -ne 1 ]]; then
die "output directory $OUT_ABS is non-empty (use --force to overwrite)"
fi
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
say "source: $SOURCE"
say "version: $VERSION"
say "out: $OUT_ABS"
say "key: $KEY"
# -------- [1/6] manifest --------
say "[1/6] building manifest"
mkdir -p "$OUT_ABS/manifests" "$OUT_ABS/files"
MANIFEST="$OUT_ABS/manifest.json"
mk_args=(--source "$SOURCE" --version "$VERSION" --out "$MANIFEST")
[[ -n "$PREVIOUS" ]] && mk_args+=(--previous "$PREVIOUS")
if [[ -n "$NOTES_FILE" ]]; then
notes_text=$(cat "$NOTES_FILE")
mk_args+=(--notes "$notes_text")
fi
python3 "$SCRIPT_DIR/make-manifest.py" "${mk_args[@]}"
# -------- [2/6] sign --------
say "[2/6] signing manifest"
python3 "$SCRIPT_DIR/sign-manifest.py" --manifest "$MANIFEST" --key "$KEY"
SIG="$MANIFEST.sig"
[[ -f "$SIG" ]] || die "signature not produced"
sig_len=$(stat -c '%s' "$SIG")
[[ "$sig_len" == "64" ]] || die "signature is $sig_len bytes, expected 64"
# -------- [3/6] archive historical manifest --------
say "[3/6] archiving historical manifest -> manifests/${VERSION}.json"
cp -f "$MANIFEST" "$OUT_ABS/manifests/${VERSION}.json"
cp -f "$SIG" "$OUT_ABS/manifests/${VERSION}.json.sig"
# -------- [4/6] blob tree --------
say "[4/6] building content-addressed blob tree"
# Extract (path, sha256) pairs for launcher + every file entry.
mapfile -t PAIRS < <(jq -r '
([.launcher] + .files)
| .[]
| "\(.sha256)\t\(.path)"
' "$MANIFEST")
total_entries=${#PAIRS[@]}
unique_count=0
dedup_count=0
bytes_written=0
declare -A SEEN
for pair in "${PAIRS[@]}"; do
hash="${pair%%$'\t'*}"
rel="${pair#*$'\t'}"
src="$SOURCE/$rel"
[[ -f "$src" ]] || die "file in manifest missing from source: $rel"
if [[ -n "${SEEN[$hash]:-}" ]]; then
dedup_count=$((dedup_count + 1))
continue
fi
SEEN[$hash]=1
unique_count=$((unique_count + 1))
prefix="${hash:0:2}"
dst_dir="$OUT_ABS/files/$prefix"
dst="$dst_dir/$hash"
mkdir -p "$dst_dir"
if [[ -f "$dst" ]]; then
continue
fi
# Try hardlink first, fall back to copy across filesystems.
if ! cp -l "$src" "$dst" 2>/dev/null; then
cp "$src" "$dst"
fi
sz=$(stat -c '%s' "$dst")
bytes_written=$((bytes_written + sz))
done
say " entries: $total_entries unique blobs: $unique_count deduped: $dedup_count"
say " bytes written: $bytes_written"
# -------- [5/6] layout summary --------
say "[5/6] final layout:"
(cd "$OUT_ABS" && find . -maxdepth 2 -mindepth 1 -printf ' %p\n' | sort | head -40)
# -------- [6/6] rsync --------
if [[ "$DRY_RUN" -eq 1 ]]; then
say "[6/6] --dry-run set, skipping rsync. target would be: $RSYNC_TARGET"
exit 0
fi
say "[6/6] rsync target: $RSYNC_TARGET"
if [[ "$YES" -ne 1 ]]; then
read -r -p "continue? [y/N] " ans
[[ "$ans" == "y" || "$ans" == "Y" ]] || die "aborted by user"
fi
# Stage 1: everything except manifest.json(.sig) — blobs and historical archive.
rsync -av --delay-updates --checksum --omit-dir-times --no-perms \
--exclude 'manifest.json' --exclude 'manifest.json.sig' \
"$OUT_ABS"/ "$RSYNC_TARGET"
# Stage 2: manifest + signature, so the new release becomes visible last.
rsync -av --checksum --omit-dir-times --no-perms \
"$MANIFEST" "$SIG" "$RSYNC_TARGET"
say "done."