20 Commits

Author SHA1 Message Date
server
7d078e4df8 issue-10: add talisman ui support 2026-04-16 22:29:08 +02:00
server
12df5a2bd6 issue-9: add sash inventory integration 2026-04-16 21:08:13 +02:00
server
09efcadbdd issue-8: add switchbot ui 2026-04-16 20:38:12 +02:00
server
ecc99a8683 issue-7: add stone queue ui 2026-04-16 20:02:14 +02:00
server
7871288b6d issue-6: add autopickup settings ui 2026-04-16 19:28:16 +02:00
server
4ef859e26f issue-5: add teleport hud 2026-04-16 18:33:07 +02:00
server
c11008d1c8 issue-4: add chat fallback for biolog submit 2026-04-16 17:36:46 +02:00
server
a52debbb2a issue-4: add biolog hud 2026-04-16 17:27:05 +02:00
root
403dddd6af Refresh private shop ad board on render 2026-04-16 10:56:45 +02:00
root
10915467c1 Smooth player gauge anchor on high FPS 2026-04-16 10:48:08 +02:00
root
780900af61 Add render FPS and perf HUD settings 2026-04-16 10:37:13 +02:00
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
1dba3e3c91 Merge pull request 'docs: design the update manager + manifest generator' (#3) from jann/m2dev-client:claude/update-manager into main
Reviewed-on: metin-server/m2dev-client#3
2026-04-14 11:54:39 +02:00
9c95590099 Merge pull request 'docs: add Linux Wine runtime guide and setup script' (#1) from jann/m2dev-client:claude/linux-wine-runtime into main
Reviewed-on: metin-server/m2dev-client#1
2026-04-14 10:30:03 +02:00
Jan Nedbal
dd0643137f scripts: add wine prefix setup script
Idempotent helper that copies the client to a writable location,
creates a fresh Wine prefix, and installs the required winetricks verbs
(vcrun2022, d3dx9, corefonts, tahoma). Re-running on an existing target
skips already-done steps.
2026-04-14 10:23:04 +02:00
Jan Nedbal
0aa8361f09 docs: add Linux Wine runtime guide
Document the interim path for running the Windows client on Linux via
Wine, verified to reach character selection on Fedora 41 with Wine 10
Staging. Main gotcha: winetricks tahoma is mandatory because the client
hard-codes Tahoma as the UI font, and without it all text renders
invisibly even though layouts are correct.
2026-04-14 10:23:04 +02:00
60 changed files with 2330 additions and 1374 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

@@ -33,6 +33,9 @@ import uiAffectShower
import uiPlayerGauge
import uiCharacter
import uiTarget
import uiAutopickup
import uiStoneQueue
import uiSwitchbot
# PRIVATE_SHOP_PRICE_LIST
import uiPrivateShopBuilder
@@ -55,6 +58,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 +218,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 +227,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 +238,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 +275,7 @@ class GameWindow(ui.ScriptWindow):
self.__ServerCommand_Build()
self.__ProcessPreservedServerCommand()
_WriteHeadlessTrace("GameWindow.__init__ done")
def __del__(self):
player.SetGameWindow(0)
@@ -133,6 +297,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 +388,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 +467,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 +1026,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 +1636,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()
@@ -1913,6 +2180,10 @@ class GameWindow(ui.ScriptWindow):
"PartyRequestDenied" : self.__PartyRequestDenied,
"horse_state" : self.__Horse_UpdateState,
"hide_horse_state" : self.__Horse_HideState,
"AutoPickupState" : self.__AutoPickupState,
"StoneQueueState" : self.__StoneQueueState,
"SwitchbotState" : self.__SwitchbotState,
"SwitchbotSlot" : self.__SwitchbotSlot,
"WarUC" : self.__GuildWar_UpdateMemberCount,
"test_server" : self.__EnableTestServerFlag,
"mall" : self.__InGameShop_Show,
@@ -1960,6 +2231,18 @@ class GameWindow(ui.ScriptWindow):
def PartyHealReady(self):
self.interface.PartyHealReady()
def __AutoPickupState(self, enabled, mode, mask, vip):
uiAutopickup.SetAutoPickupState(int(enabled), int(mode), int(mask), int(vip))
def __StoneQueueState(self, maxCount, active, current, success, fail):
uiStoneQueue.SetStoneQueueState(int(maxCount), int(active), int(current), int(success), int(fail))
def __SwitchbotState(self, speedIndex, activeCount, scrollCount, etaSeconds):
uiSwitchbot.SetSwitchbotState(int(speedIndex), int(activeCount), int(scrollCount), int(etaSeconds))
def __SwitchbotSlot(self, slotIndex, active, itemCell, attrType, minValue, attempts):
uiSwitchbot.SetSwitchbotSlotState(int(slotIndex), int(active), int(itemCell), int(attrType), int(minValue), int(attempts))
def AskSafeboxPassword(self):
self.interface.AskSafeboxPassword()
@@ -2229,4 +2512,3 @@ class GameWindow(ui.ScriptWindow):
def SkillClearCoolTime(self, slotIndex):
self.interface.SkillClearCoolTime(slotIndex)

View File

@@ -25,6 +25,9 @@ import uiSystem
import uiRestart
import uiToolTip
import uiMiniMap
import uiBiolog
import uiTeleport
import uiSwitchbot
import uiParty
import uiSafebox
import uiGuild
@@ -75,6 +78,9 @@ class Interface(object):
self.wndChat = None
self.wndMessenger = None
self.wndMiniMap = None
self.wndBiolog = None
self.wndTeleport = None
self.wndSwitchbot = None
self.wndGuild = None
self.wndGuildBuilding = None
@@ -180,6 +186,9 @@ class Interface(object):
wndDragonSoulRefine = None
wndMiniMap = uiMiniMap.MiniMap()
wndBiolog = uiBiolog.BiologWindow()
wndTeleport = uiTeleport.TeleportWindow()
wndSwitchbot = uiSwitchbot.SwitchbotWindow()
wndSafebox = uiSafebox.SafeboxWindow()
# ITEM_MALL
@@ -195,8 +204,14 @@ class Interface(object):
self.wndDragonSoul = wndDragonSoul
self.wndDragonSoulRefine = wndDragonSoulRefine
self.wndMiniMap = wndMiniMap
self.wndBiolog = wndBiolog
self.wndTeleport = wndTeleport
self.wndSwitchbot = wndSwitchbot
self.wndSafebox = wndSafebox
self.wndChatLog = wndChatLog
self.wndMiniMap.SetBiologButtonEvent(ui.__mem_func__(self.ToggleBiologWindow))
self.wndMiniMap.SetTeleportButtonEvent(ui.__mem_func__(self.ToggleTeleportWindow))
self.wndMiniMap.SetSwitchbotButtonEvent(ui.__mem_func__(self.ToggleSwitchbotWindow))
if app.ENABLE_DRAGON_SOUL_SYSTEM:
self.wndDragonSoul.SetDragonSoulRefineWindow(self.wndDragonSoulRefine)
@@ -409,6 +424,11 @@ class Interface(object):
if self.wndMiniMap:
self.wndMiniMap.Destroy()
if self.wndBiolog:
self.wndBiolog.Hide()
if self.wndSwitchbot:
self.wndSwitchbot.Hide()
if self.wndSafebox:
self.wndSafebox.Destroy()
@@ -499,6 +519,8 @@ class Interface(object):
del self.tooltipItem
del self.tooltipSkill
del self.wndMiniMap
del self.wndBiolog
del self.wndSwitchbot
del self.wndSafebox
del self.wndMall
del self.wndParty
@@ -858,6 +880,9 @@ class Interface(object):
if self.wndMiniMap:
self.wndMiniMap.Hide()
if self.wndBiolog:
self.wndBiolog.Hide()
if self.wndMessenger:
self.wndMessenger.Hide()
@@ -942,6 +967,33 @@ class Interface(object):
def MiniMapScaleDown(self):
self.wndMiniMap.ScaleDown()
def ToggleBiologWindow(self):
if False == self.wndBiolog.IsShow():
(miniMapX, miniMapY) = self.wndMiniMap.GetGlobalPosition()
self.wndBiolog.SetPosition(max(10, miniMapX - self.wndBiolog.GetWidth() - 10), miniMapY + 8)
self.wndBiolog.Show()
self.wndBiolog.SetTop()
else:
self.wndBiolog.Hide()
def ToggleTeleportWindow(self):
if False == self.wndTeleport.IsShow():
(miniMapX, miniMapY) = self.wndMiniMap.GetGlobalPosition()
self.wndTeleport.SetPosition(max(10, miniMapX - self.wndTeleport.GetWidth() - 10), miniMapY + 24)
self.wndTeleport.Show()
self.wndTeleport.SetTop()
else:
self.wndTeleport.Hide()
def ToggleSwitchbotWindow(self):
if False == self.wndSwitchbot.IsShow():
(miniMapX, miniMapY) = self.wndMiniMap.GetGlobalPosition()
self.wndSwitchbot.SetPosition(max(10, miniMapX - self.wndSwitchbot.GetWidth() - 10), miniMapY + 40)
self.wndSwitchbot.Show()
self.wndSwitchbot.SetTop()
else:
self.wndSwitchbot.Hide()
def ToggleCharacterWindow(self, state):
if False == player.IsObserverMode():
if False == self.wndCharacter.IsShow():

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

@@ -659,6 +659,7 @@ def __LoadGameWarriorEx(race, path):
## Bone
chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON, "equip_right_hand")
chrmgr.RegisterAttachingBoneName(chr.PART_ACCE, "Bip01 Spine2")
def __LoadGameAssassinEx(race, path):
## Assassin
@@ -872,6 +873,7 @@ def __LoadGameAssassinEx(race, path):
chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON, "equip_right")
chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON_LEFT, "equip_left")
chrmgr.RegisterAttachingBoneName(chr.PART_ACCE, "Bip01 Spine2")
def __LoadGameSuraEx(race, path):
## Sura
@@ -1189,6 +1191,7 @@ def __LoadGameShamanEx(race, path):
chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON, "equip_right")
chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON_LEFT, "equip_left")
chrmgr.RegisterAttachingBoneName(chr.PART_ACCE, "Bip01 Spine2")
def __LoadGameSkill():
@@ -1456,4 +1459,4 @@ def SetGuildBuilding(race, name, grade):
chrmgr.SetPathName("d:/ymir work/guild/building/%s/" % name)
chrmgr.LoadRaceData("%s%02d.msm" % (name, grade))
chrmgr.RegisterMotionMode(chr.MOTION_MODE_GENERAL)
#chrmgr.RegisterMotionData(chr.MOTION_MODE_GENERAL, chr.MOTION_DEAD, name + "_destruction.msa")
#chrmgr.RegisterMotionData(chr.MOTION_MODE_GENERAL, chr.MOTION_DEAD, name + "_destruction.msa")

View File

@@ -1,3 +1,5 @@
import os
import dbg
import app
import localeInfo
@@ -14,6 +16,66 @@ 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 _ApplyRenderFPSOverride():
rawValue = os.environ.get("M2_RENDER_FPS", "").strip()
if not rawValue:
return
try:
fps = int(rawValue)
except (TypeError, ValueError):
_WriteHeadlessTrace("Invalid M2_RENDER_FPS=%s" % rawValue)
return
app.SetFPS(fps)
_WriteHeadlessTrace("Render FPS override=%d" % fps)
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()
@@ -36,6 +98,7 @@ def RunApp():
return
app.SetCamera(1500.0, 30.0, 0.0, 180.0)
_ApplyRenderFPSOverride()
#Gets and sets the floating-point control word
#app.SetControlFP()
@@ -47,8 +110,7 @@ def RunApp():
mainStream.Create()
#mainStream.SetLoadingPhase()
mainStream.SetLoginPhase()
_SetInitialPhase(mainStream)
#mainStream.SetSelectCharacterPhase()
#mainStream.SetCreateCharacterPhase()
#mainStream.SetSelectEmpirePhase()
@@ -58,4 +120,3 @@ def RunApp():
mainStream.Destroy()
RunApp()

145
assets/root/uiAutopickup.py Normal file
View File

@@ -0,0 +1,145 @@
import net
import ui
STATE_ENABLED = 0
STATE_MODE = 0
STATE_MASK = 31
STATE_VIP = 0
OPEN_WINDOWS = []
FILTERS = (
(1, "Weapons"),
(2, "Armor"),
(4, "Yang"),
(8, "Stones"),
(16, "Materials"),
)
def SetAutoPickupState(enabled, mode, mask, vip):
global STATE_ENABLED
global STATE_MODE
global STATE_MASK
global STATE_VIP
STATE_ENABLED = 1 if int(enabled) else 0
STATE_MODE = 1 if int(mode) else 0
STATE_MASK = int(mask) & 31
STATE_VIP = 1 if int(vip) else 0
for window in OPEN_WINDOWS:
try:
window.ApplyState()
except:
pass
class AutoPickupWindow(ui.BoardWithTitleBar):
def __init__(self):
ui.BoardWithTitleBar.__init__(self)
OPEN_WINDOWS.append(self)
self.filterButtons = {}
self.AddFlag("float")
self.AddFlag("movable")
self.SetSize(280, 248)
self.SetTitleName("Auto Pickup")
self.SetCloseEvent(self.Hide)
self.__CreateChildren()
self.Hide()
def __del__(self):
if self in OPEN_WINDOWS:
OPEN_WINDOWS.remove(self)
ui.BoardWithTitleBar.__del__(self)
def Destroy(self):
if self in OPEN_WINDOWS:
OPEN_WINDOWS.remove(self)
self.ClearDictionary()
self.filterButtons = {}
def __CreateChildren(self):
self.statusLine = self.__CreateLine(15, 36)
self.rangeLine = self.__CreateLine(15, 56)
self.modeLine = self.__CreateLine(15, 76)
self.noteLine = self.__CreateLine(15, 96)
self.enableButton = self.__CreateButton(170, 34, 90, "Enable")
self.enableButton.SetEvent(self.__ToggleEnabled)
self.whitelistButton = self.__CreateButton(15, 118, 118, "Whitelist")
self.whitelistButton.SetEvent(self.__SetMode, 0)
self.blacklistButton = self.__CreateButton(142, 118, 118, "Blacklist")
self.blacklistButton.SetEvent(self.__SetMode, 1)
for index, filterData in enumerate(FILTERS):
(bit, label) = filterData
button = self.__CreateButton(15, 150 + index * 18, 245, label)
button.SetEvent(self.__ToggleFilter, bit)
self.filterButtons[bit] = button
def __CreateLine(self, x, y):
textLine = ui.TextLine()
textLine.SetParent(self)
textLine.SetPosition(x, y)
textLine.SetOutline()
textLine.Show()
return textLine
def __CreateButton(self, x, y, width, text):
button = ui.Button()
button.SetParent(self)
button.SetPosition(x, y)
button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetSize(width, 17)
button.SetText(text)
button.Show()
return button
def __Send(self, command):
net.SendChatPacket(command, 0)
def __ToggleEnabled(self):
SetAutoPickupState(0 if STATE_ENABLED else 1, STATE_MODE, STATE_MASK, STATE_VIP)
self.__Send("/autopickup enable %d" % STATE_ENABLED)
def __SetMode(self, mode):
SetAutoPickupState(STATE_ENABLED, mode, STATE_MASK, STATE_VIP)
self.__Send("/autopickup mode %s" % ("blacklist" if mode else "whitelist"))
def __ToggleFilter(self, bit):
mask = STATE_MASK ^ bit
SetAutoPickupState(STATE_ENABLED, STATE_MODE, mask, STATE_VIP)
self.__Send("/autopickup mask %d" % STATE_MASK)
def ApplyState(self):
self.statusLine.SetText("Status: %s" % ("Enabled" if STATE_ENABLED else "Disabled"))
self.rangeLine.SetText("Range: %s" % ("VIP 300 / Free 220" if STATE_VIP else "Free 220"))
self.modeLine.SetText("Mode: %s" % ("Blacklist" if STATE_MODE else "Whitelist"))
self.noteLine.SetText("Only nearby owned drops are valid")
self.enableButton.SetText("Disable" if STATE_ENABLED else "Enable")
if STATE_MODE == 0:
self.whitelistButton.Down()
self.blacklistButton.SetUp()
else:
self.whitelistButton.SetUp()
self.blacklistButton.Down()
for (bit, label) in FILTERS:
prefix = "[x]" if (STATE_MASK & bit) else "[ ]"
self.filterButtons[bit].SetText("%s %s" % (prefix, label))
def Show(self):
self.__Send("/autopickup sync")
self.ApplyState()
ui.BoardWithTitleBar.Show(self)

145
assets/root/uiBiolog.py Normal file
View File

@@ -0,0 +1,145 @@
import time
import net
import player
import quest
import ui
class BiologWindow(ui.BoardWithTitleBar):
TITLE_PREFIX = "Biolog Stage "
STAGE_TARGETS = {
1 : 10,
2 : 15,
3 : 20,
4 : 20,
5 : 25,
6 : 30,
7 : 30,
8 : 40,
9 : 40,
}
def __init__(self):
ui.BoardWithTitleBar.__init__(self)
self.lastRefreshTime = 0.0
self.AddFlag("float")
self.AddFlag("movable")
self.SetSize(230, 165)
self.SetTitleName("Biolog")
self.SetCloseEvent(self.Hide)
self.__CreateChildren()
self.Hide()
def __del__(self):
ui.BoardWithTitleBar.__del__(self)
def __CreateChildren(self):
self.statusLine = self.__CreateValueLine(15, 36)
self.stageLine = self.__CreateValueLine(15, 58)
self.itemLine = self.__CreateValueLine(15, 80)
self.progressLine = self.__CreateValueLine(15, 102)
self.cooldownLine = self.__CreateValueLine(15, 124)
submitButton = ui.Button()
submitButton.SetParent(self)
submitButton.SetPosition(134, 132)
submitButton.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
submitButton.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
submitButton.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
submitButton.SetText("Submit")
submitButton.SetEvent(self.__OnSubmit)
submitButton.Show()
self.submitButton = submitButton
def __CreateValueLine(self, x, y):
textLine = ui.TextLine()
textLine.SetParent(self)
textLine.SetPosition(x, y)
textLine.SetOutline()
textLine.Show()
return textLine
def __GetBiologData(self):
questCount = min(quest.GetQuestCount(), quest.QUEST_MAX_NUM)
for questIndex in range(questCount):
(questName, questIcon, questCounterName, questCounterValue) = quest.GetQuestData(questIndex)
if not questName.startswith(self.TITLE_PREFIX):
continue
try:
stageIndex = int(questName[len(self.TITLE_PREFIX):])
except:
continue
totalRequired = self.STAGE_TARGETS.get(stageIndex, questCounterValue)
(clockName, clockValue) = quest.GetQuestLastTime(questIndex)
return {
"stage" : stageIndex,
"itemName" : questCounterName,
"remaining" : max(questCounterValue, 0),
"required" : max(totalRequired, 0),
"clockName" : clockName,
"clockValue" : max(clockValue, 0),
}
return None
def __FormatCooldown(self, seconds):
hours = seconds // 3600
minutes = (seconds // 60) % 60
return "%02d:%02d:%02d" % (hours, minutes, seconds % 60)
def __RefreshLockedState(self):
if player.GetStatus(player.LEVEL) < 30:
self.statusLine.SetText("Status: Unlocks at level 30")
else:
self.statusLine.SetText("Status: No active biolog stage")
self.stageLine.SetText("Stage: -")
self.itemLine.SetText("Item: -")
self.progressLine.SetText("Progress: -")
self.cooldownLine.SetText("Cooldown: -")
self.submitButton.Hide()
def Refresh(self):
biologData = self.__GetBiologData()
if not biologData:
self.__RefreshLockedState()
return
submitted = max(biologData["required"] - biologData["remaining"], 0)
self.statusLine.SetText("Status: %s" % ("Ready" if biologData["clockValue"] <= 0 else "Cooldown"))
self.stageLine.SetText("Stage: %d / 9" % biologData["stage"])
self.itemLine.SetText("Item: %s" % biologData["itemName"])
self.progressLine.SetText("Progress: %d / %d" % (submitted, biologData["required"]))
if biologData["clockValue"] > 0 and len(biologData["clockName"]) > 0:
self.cooldownLine.SetText("Cooldown: %s" % self.__FormatCooldown(biologData["clockValue"]))
else:
self.cooldownLine.SetText("Cooldown: Ready")
self.submitButton.Show()
def Show(self):
self.Refresh()
ui.BoardWithTitleBar.Show(self)
def OnUpdate(self):
currentTime = time.time()
if currentTime - self.lastRefreshTime < 0.2:
return
self.lastRefreshTime = currentTime
self.Refresh()
def __OnSubmit(self):
try:
net.SendBiologSubmit()
except AttributeError:
net.SendChatPacket("/biolog_submit", 0)
self.lastRefreshTime = 0.0
self.Refresh()

233
assets/root/uiStoneQueue.py Normal file
View File

@@ -0,0 +1,233 @@
import item
import net
import player
import ui
STATE_MAX = 3
STATE_ACTIVE = 0
STATE_CURRENT = 0
STATE_SUCCESS = 0
STATE_FAIL = 0
OPEN_WINDOWS = []
def SetStoneQueueState(maxCount, active, current, success, fail):
global STATE_MAX
global STATE_ACTIVE
global STATE_CURRENT
global STATE_SUCCESS
global STATE_FAIL
STATE_MAX = max(1, int(maxCount))
STATE_ACTIVE = 1 if int(active) else 0
STATE_CURRENT = max(0, int(current))
STATE_SUCCESS = max(0, int(success))
STATE_FAIL = max(0, int(fail))
for window in OPEN_WINDOWS:
try:
window.Refresh()
except:
pass
class StoneQueueWindow(ui.BoardWithTitleBar):
PAGE_SIZE = 20
def __init__(self):
ui.BoardWithTitleBar.__init__(self)
OPEN_WINDOWS.append(self)
self.scrollSlotPos = -1
self.selectedSlots = []
self.eligibleSlots = []
self.pageIndex = 0
self.candidateButtons = []
self.queueLines = []
self.AddFlag("float")
self.AddFlag("movable")
self.SetSize(340, 355)
self.SetTitleName("Stone Queue")
self.SetCloseEvent(self.Hide)
self.__CreateChildren()
self.Hide()
def __del__(self):
if self in OPEN_WINDOWS:
OPEN_WINDOWS.remove(self)
ui.BoardWithTitleBar.__del__(self)
def Destroy(self):
if self in OPEN_WINDOWS:
OPEN_WINDOWS.remove(self)
self.ClearDictionary()
self.selectedSlots = []
self.eligibleSlots = []
self.candidateButtons = []
self.queueLines = []
def __CreateChildren(self):
self.scrollLine = self.__CreateLine(15, 36)
self.statusLine = self.__CreateLine(15, 56)
self.progressLine = self.__CreateLine(15, 76)
self.resultLine = self.__CreateLine(15, 96)
self.prevButton = self.__CreateButton(15, 118, 45, "Prev")
self.prevButton.SetEvent(self.__ChangePage, -1)
self.pageLine = self.__CreateLine(127, 122)
self.nextButton = self.__CreateButton(280, 118, 45, "Next")
self.nextButton.SetEvent(self.__ChangePage, 1)
for row in range(5):
for column in range(4):
button = self.__CreateButton(15 + column * 78, 145 + row * 24, 72, "-")
self.candidateButtons.append(button)
for index in range(8):
line = self.__CreateLine(15, 272 + index * 10)
self.queueLines.append(line)
self.startButton = self.__CreateButton(185, 328, 65, "Start")
self.startButton.SetEvent(self.__StartQueue)
self.cancelButton = self.__CreateButton(260, 328, 65, "Cancel")
self.cancelButton.SetEvent(self.__CancelQueue)
def __CreateLine(self, x, y):
textLine = ui.TextLine()
textLine.SetParent(self)
textLine.SetPosition(x, y)
textLine.SetOutline()
textLine.Show()
return textLine
def __CreateButton(self, x, y, width, text):
button = ui.Button()
button.SetParent(self)
button.SetPosition(x, y)
button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetSize(width, 17)
button.SetText(text)
button.Show()
return button
def __BuildEligibleSlots(self):
self.eligibleSlots = []
if self.scrollSlotPos < 0:
return
scrollIndex = player.GetItemIndex(self.scrollSlotPos)
for slot in range(player.INVENTORY_PAGE_SIZE * player.INVENTORY_PAGE_COUNT):
if player.REFINE_OK != player.CanRefine(scrollIndex, slot):
continue
targetIndex = player.GetItemIndex(slot)
if targetIndex == 0:
continue
item.SelectItem(targetIndex)
if item.GetItemType() != item.ITEM_TYPE_METIN:
continue
self.eligibleSlots.append(slot)
def __GetSlotLabel(self, slot):
itemVnum = player.GetItemIndex(slot)
if itemVnum == 0:
return "Slot %d" % slot
item.SelectItem(itemVnum)
return "%d:+%d" % (slot, player.GetItemGrade(slot))
def __RefreshCandidates(self):
start = self.pageIndex * self.PAGE_SIZE
end = start + self.PAGE_SIZE
pageSlots = self.eligibleSlots[start:end]
totalPages = max(1, (len(self.eligibleSlots) + self.PAGE_SIZE - 1) // self.PAGE_SIZE)
self.pageLine.SetText("Page %d / %d" % (self.pageIndex + 1, totalPages))
for index, button in enumerate(self.candidateButtons):
if index < len(pageSlots):
slot = pageSlots[index]
prefix = "[x]" if slot in self.selectedSlots else "[ ]"
button.SetText("%s %s" % (prefix, self.__GetSlotLabel(slot)))
button.Enable()
button.SetEvent(self.__ToggleSlot, slot)
else:
button.SetText("-")
button.Disable()
self.prevButton.Enable() if self.pageIndex > 0 else self.prevButton.Disable()
self.nextButton.Enable() if end < len(self.eligibleSlots) else self.nextButton.Disable()
def __RefreshQueueLines(self):
for index, line in enumerate(self.queueLines):
if index < len(self.selectedSlots):
line.SetText("Queue %d: %s" % (index + 1, self.__GetSlotLabel(self.selectedSlots[index])))
else:
line.SetText("Queue %d: -" % (index + 1))
def __ToggleSlot(self, slot):
if STATE_ACTIVE:
return
if slot in self.selectedSlots:
self.selectedSlots.remove(slot)
elif len(self.selectedSlots) < STATE_MAX:
self.selectedSlots.append(slot)
self.Refresh()
def __ChangePage(self, delta):
totalPages = max(1, (len(self.eligibleSlots) + self.PAGE_SIZE - 1) // self.PAGE_SIZE)
self.pageIndex = max(0, min(totalPages - 1, self.pageIndex + delta))
self.__RefreshCandidates()
def __StartQueue(self):
if STATE_ACTIVE or self.scrollSlotPos < 0 or not self.selectedSlots:
return
command = "/stone_queue start %d %s" % (
self.scrollSlotPos,
" ".join([str(slot) for slot in self.selectedSlots[:STATE_MAX]]),
)
net.SendChatPacket(command, 0)
def __CancelQueue(self):
net.SendChatPacket("/stone_queue cancel", 0)
def Open(self, scrollSlotPos, targetSlotPos):
self.scrollSlotPos = scrollSlotPos
self.pageIndex = 0
self.selectedSlots = []
self.__BuildEligibleSlots()
if targetSlotPos in self.eligibleSlots:
self.selectedSlots.append(targetSlotPos)
net.SendChatPacket("/stone_queue sync", 0)
self.Refresh()
self.SetTop()
self.Show()
def Refresh(self):
self.scrollLine.SetText("Scroll slot: %d" % self.scrollSlotPos)
self.statusLine.SetText("Status: %s" % ("Running" if STATE_ACTIVE else "Ready"))
self.progressLine.SetText("Progress: %d / %d" % (STATE_CURRENT, len(self.selectedSlots)))
self.resultLine.SetText("Results: %d success / %d fail / max %d" % (STATE_SUCCESS, STATE_FAIL, STATE_MAX))
self.__RefreshCandidates()
self.__RefreshQueueLines()
if STATE_ACTIVE:
self.startButton.Disable()
self.cancelButton.Enable()
else:
self.startButton.Enable()
self.cancelButton.Disable()

455
assets/root/uiSwitchbot.py Normal file
View File

@@ -0,0 +1,455 @@
import item
import net
import player
import ui
import uitooltip
STATE_SPEED = 1
STATE_ACTIVE_COUNT = 0
STATE_SCROLL_COUNT = 0
STATE_ETA_SECONDS = 0
STATE_SLOTS = []
OPEN_WINDOWS = []
_AFFECT_HELPER = None
for _slotIndex in range(5):
STATE_SLOTS.append({
"active": 0,
"itemCell": 0,
"attrType": 0,
"minValue": 0,
"attempts": 0,
})
def _GetAffectHelper():
global _AFFECT_HELPER
if _AFFECT_HELPER is None:
_AFFECT_HELPER = uitooltip.ItemToolTip()
return _AFFECT_HELPER
def _GetAffectLabel(attrType):
if attrType <= 0:
return "-"
try:
label = _GetAffectHelper()._ItemToolTip__GetAffectString(attrType, 1)
except:
label = None
if not label:
return "Attr %d" % attrType
return label
ATTR_OPTIONS = (
(item.APPLY_MAX_HP, _GetAffectLabel(item.APPLY_MAX_HP)),
(item.APPLY_MAX_SP, _GetAffectLabel(item.APPLY_MAX_SP)),
(item.APPLY_STR, _GetAffectLabel(item.APPLY_STR)),
(item.APPLY_DEX, _GetAffectLabel(item.APPLY_DEX)),
(item.APPLY_CON, _GetAffectLabel(item.APPLY_CON)),
(item.APPLY_INT, _GetAffectLabel(item.APPLY_INT)),
(item.APPLY_ATT_SPEED, _GetAffectLabel(item.APPLY_ATT_SPEED)),
(item.APPLY_MOV_SPEED, _GetAffectLabel(item.APPLY_MOV_SPEED)),
(item.APPLY_CAST_SPEED, _GetAffectLabel(item.APPLY_CAST_SPEED)),
(item.APPLY_CRITICAL_PCT, _GetAffectLabel(item.APPLY_CRITICAL_PCT)),
(item.APPLY_PENETRATE_PCT, _GetAffectLabel(item.APPLY_PENETRATE_PCT)),
(item.APPLY_ATTBONUS_MONSTER, _GetAffectLabel(item.APPLY_ATTBONUS_MONSTER)),
(item.APPLY_ATTBONUS_DEVIL, _GetAffectLabel(item.APPLY_ATTBONUS_DEVIL)),
(item.APPLY_ATTBONUS_HUMAN, _GetAffectLabel(item.APPLY_ATTBONUS_HUMAN)),
(item.APPLY_STEAL_HP, _GetAffectLabel(item.APPLY_STEAL_HP)),
(item.APPLY_STEAL_SP, _GetAffectLabel(item.APPLY_STEAL_SP)),
(item.APPLY_BLOCK, _GetAffectLabel(item.APPLY_BLOCK)),
(item.APPLY_DODGE, _GetAffectLabel(item.APPLY_DODGE)),
(item.APPLY_ATT_GRADE_BONUS, _GetAffectLabel(item.APPLY_ATT_GRADE_BONUS)),
(item.APPLY_DEF_GRADE_BONUS, _GetAffectLabel(item.APPLY_DEF_GRADE_BONUS)),
(item.APPLY_RESIST_SWORD, _GetAffectLabel(item.APPLY_RESIST_SWORD)),
(item.APPLY_RESIST_TWOHAND, _GetAffectLabel(item.APPLY_RESIST_TWOHAND)),
(item.APPLY_RESIST_DAGGER, _GetAffectLabel(item.APPLY_RESIST_DAGGER)),
(item.APPLY_RESIST_BOW, _GetAffectLabel(item.APPLY_RESIST_BOW)),
(item.APPLY_RESIST_FIRE, _GetAffectLabel(item.APPLY_RESIST_FIRE)),
(item.APPLY_RESIST_ELEC, _GetAffectLabel(item.APPLY_RESIST_ELEC)),
(item.APPLY_RESIST_MAGIC, _GetAffectLabel(item.APPLY_RESIST_MAGIC)),
(item.APPLY_RESIST_WIND, _GetAffectLabel(item.APPLY_RESIST_WIND)),
(item.APPLY_RESIST_ICE, _GetAffectLabel(item.APPLY_RESIST_ICE)),
(item.APPLY_RESIST_EARTH, _GetAffectLabel(item.APPLY_RESIST_EARTH)),
(item.APPLY_RESIST_DARK, _GetAffectLabel(item.APPLY_RESIST_DARK)),
)
SPEED_OPTIONS = (
(0, "Slow / 3s"),
(1, "Normal / 2s"),
(2, "Fast / 1s"),
)
def SetSwitchbotState(speedIndex, activeCount, scrollCount, etaSeconds):
global STATE_SPEED
global STATE_ACTIVE_COUNT
global STATE_SCROLL_COUNT
global STATE_ETA_SECONDS
STATE_SPEED = int(speedIndex)
STATE_ACTIVE_COUNT = max(0, int(activeCount))
STATE_SCROLL_COUNT = max(0, int(scrollCount))
STATE_ETA_SECONDS = max(0, int(etaSeconds))
for window in OPEN_WINDOWS:
try:
window.Refresh()
except:
pass
def SetSwitchbotSlotState(slotIndex, active, itemCell, attrType, minValue, attempts):
slotIndex = int(slotIndex)
if slotIndex < 0 or slotIndex >= len(STATE_SLOTS):
return
STATE_SLOTS[slotIndex]["active"] = 1 if int(active) else 0
STATE_SLOTS[slotIndex]["itemCell"] = int(itemCell)
STATE_SLOTS[slotIndex]["attrType"] = int(attrType)
STATE_SLOTS[slotIndex]["minValue"] = int(minValue)
STATE_SLOTS[slotIndex]["attempts"] = int(attempts)
for window in OPEN_WINDOWS:
try:
window.Refresh()
except:
pass
class SwitchbotWindow(ui.BoardWithTitleBar):
CANDIDATE_PAGE_SIZE = 12
def __init__(self):
ui.BoardWithTitleBar.__init__(self)
OPEN_WINDOWS.append(self)
self.selectedSlotIndex = 0
self.pageIndex = 0
self.candidateSlots = []
self.slotWidgets = []
self.candidateButtons = []
self.speedLabels = {}
self.attrLabels = {}
self.AddFlag("float")
self.AddFlag("movable")
self.SetSize(535, 406)
self.SetTitleName("Switchbot")
self.SetCloseEvent(self.Hide)
self.__CreateChildren()
self.Hide()
def __del__(self):
if self in OPEN_WINDOWS:
OPEN_WINDOWS.remove(self)
ui.BoardWithTitleBar.__del__(self)
def Destroy(self):
if self in OPEN_WINDOWS:
OPEN_WINDOWS.remove(self)
self.ClearDictionary()
self.slotWidgets = []
self.candidateButtons = []
self.candidateSlots = []
self.speedLabels = {}
self.attrLabels = {}
def __CreateChildren(self):
self.statusLine = self.__CreateLine(15, 36)
self.scrollLine = self.__CreateLine(15, 56)
self.etaLine = self.__CreateLine(15, 76)
self.helpLine = self.__CreateLine(15, 96)
self.speedCombo = ui.ComboBox()
self.speedCombo.SetParent(self)
self.speedCombo.SetPosition(398, 34)
self.speedCombo.SetSize(120, 18)
self.speedCombo.SetEvent(self.__OnChangeSpeed)
self.speedCombo.Show()
for (speedIndex, label) in SPEED_OPTIONS:
self.speedCombo.InsertItem(speedIndex, label)
self.speedLabels[speedIndex] = label
for slotIndex in range(5):
baseY = 126 + slotIndex * 38
row = {}
row["selectButton"] = self.__CreateButton(15, baseY, 48, "Slot %d" % (slotIndex + 1))
row["selectButton"].SetEvent(self.__SelectSlot, slotIndex)
row["itemLine"] = self.__CreateLine(72, baseY + 2)
row["statusLine"] = self.__CreateLine(72, baseY + 18)
row["attrCombo"] = ui.ComboBox()
row["attrCombo"].SetParent(self)
row["attrCombo"].SetPosition(220, baseY)
row["attrCombo"].SetSize(165, 18)
row["attrCombo"].SetEvent(lambda attrType, line=slotIndex: self.__SetAttrType(line, attrType))
row["attrCombo"].Show()
for (attrType, label) in ATTR_OPTIONS:
row["attrCombo"].InsertItem(attrType, label)
attrBar = ui.SlotBar()
attrBar.SetParent(self)
attrBar.SetPosition(392, baseY)
attrBar.SetSize(48, 18)
attrBar.Show()
row["valueBar"] = attrBar
row["valueEdit"] = ui.EditLine()
row["valueEdit"].SetParent(attrBar)
row["valueEdit"].SetPosition(3, 3)
row["valueEdit"].SetSize(42, 12)
row["valueEdit"].SetMax(5)
row["valueEdit"].SetNumberMode()
row["valueEdit"].Show()
row["startButton"] = self.__CreateButton(447, baseY, 40, "Start")
row["startButton"].SetEvent(self.__ToggleSlot, slotIndex)
row["clearButton"] = self.__CreateButton(492, baseY, 28, "X")
row["clearButton"].SetEvent(self.__ClearSlot, slotIndex)
self.slotWidgets.append(row)
self.prevButton = self.__CreateButton(15, 330, 44, "Prev")
self.prevButton.SetEvent(self.__ChangePage, -1)
self.pageLine = self.__CreateLine(80, 334)
self.nextButton = self.__CreateButton(170, 330, 44, "Next")
self.nextButton.SetEvent(self.__ChangePage, 1)
for index in range(self.CANDIDATE_PAGE_SIZE):
column = index % 2
row = index // 2
button = self.__CreateButton(15 + column * 155, 356 + row * 18, 148, "-")
button.SetEvent(self.__AssignCandidate, index)
self.candidateButtons.append(button)
self.syncButton = self.__CreateButton(305, 330, 54, "Sync")
self.syncButton.SetEvent(self.__Sync)
self.startAllButton = self.__CreateButton(366, 330, 74, "Start All")
self.startAllButton.SetEvent(self.__StartAll)
self.stopAllButton = self.__CreateButton(446, 330, 74, "Stop All")
self.stopAllButton.SetEvent(self.__StopAll)
def __CreateLine(self, x, y):
textLine = ui.TextLine()
textLine.SetParent(self)
textLine.SetPosition(x, y)
textLine.SetOutline()
textLine.Show()
return textLine
def __CreateButton(self, x, y, width, text):
button = ui.Button()
button.SetParent(self)
button.SetPosition(x, y)
button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetSize(width, 17)
button.SetText(text)
button.Show()
return button
def __Send(self, command):
net.SendChatPacket(command, 0)
def __IsEligibleItem(self, slotPos):
itemVnum = player.GetItemIndex(slotPos)
if itemVnum == 0:
return False
item.SelectItem(itemVnum)
if item.GetItemType() == item.ITEM_TYPE_COSTUME:
return False
if item.GetItemType() not in (item.ITEM_TYPE_WEAPON, item.ITEM_TYPE_ARMOR):
return False
for attrIndex in range(player.ATTRIBUTE_SLOT_MAX_NUM):
if player.GetItemAttribute(slotPos, attrIndex)[0] != 0:
return True
return False
def __GetItemLabel(self, slotPos):
itemVnum = player.GetItemIndex(slotPos)
if itemVnum == 0:
return "Slot %d" % slotPos
item.SelectItem(itemVnum)
return "%d: %s" % (slotPos, item.GetItemName())
def __BuildCandidateSlots(self):
self.candidateSlots = []
for slotPos in range(player.INVENTORY_PAGE_SIZE * player.INVENTORY_PAGE_COUNT):
if self.__IsEligibleItem(slotPos):
self.candidateSlots.append(slotPos)
def __FormatEta(self):
if STATE_ETA_SECONDS <= 0:
return "ETA: -"
minutes = STATE_ETA_SECONDS // 60
seconds = STATE_ETA_SECONDS % 60
return "ETA: %02d:%02d" % (minutes, seconds)
def __GetAttrType(self, slotIndex):
return STATE_SLOTS[slotIndex]["attrType"]
def __GetMinValue(self, slotIndex):
text = self.slotWidgets[slotIndex]["valueEdit"].GetText()
if not text:
return STATE_SLOTS[slotIndex]["minValue"]
return int(text)
def __SelectSlot(self, slotIndex):
self.selectedSlotIndex = slotIndex
self.Refresh()
def __SetAttrType(self, slotIndex, attrType):
SetSwitchbotSlotState(
slotIndex,
STATE_SLOTS[slotIndex]["active"],
STATE_SLOTS[slotIndex]["itemCell"],
attrType,
STATE_SLOTS[slotIndex]["minValue"],
STATE_SLOTS[slotIndex]["attempts"],
)
def __AssignCandidate(self, localIndex):
candidateIndex = self.pageIndex * self.CANDIDATE_PAGE_SIZE + localIndex
if candidateIndex < 0 or candidateIndex >= len(self.candidateSlots):
return
slotIndex = self.selectedSlotIndex
SetSwitchbotSlotState(
slotIndex,
0,
self.candidateSlots[candidateIndex],
STATE_SLOTS[slotIndex]["attrType"],
STATE_SLOTS[slotIndex]["minValue"],
0,
)
def __ToggleSlot(self, slotIndex):
slotState = STATE_SLOTS[slotIndex]
if slotState["active"]:
self.__Send("/switchbot stop %d" % slotIndex)
return
itemCell = slotState["itemCell"]
attrType = self.__GetAttrType(slotIndex)
minValue = self.__GetMinValue(slotIndex)
if not self.__IsEligibleItem(itemCell) or attrType <= 0 or minValue <= 0:
return
SetSwitchbotSlotState(slotIndex, 0, itemCell, attrType, minValue, slotState["attempts"])
self.__Send("/switchbot start %d %d %d %d" % (slotIndex, itemCell, attrType, minValue))
def __ClearSlot(self, slotIndex):
self.__Send("/switchbot clear %d" % slotIndex)
def __ChangePage(self, delta):
totalPages = max(1, (len(self.candidateSlots) + self.CANDIDATE_PAGE_SIZE - 1) // self.CANDIDATE_PAGE_SIZE)
self.pageIndex = max(0, min(totalPages - 1, self.pageIndex + delta))
self.Refresh()
def __OnChangeSpeed(self, speedIndex):
self.__Send("/switchbot speed %d" % int(speedIndex))
def __Sync(self):
self.__Send("/switchbot sync")
def __StartAll(self):
for slotIndex in range(len(STATE_SLOTS)):
slotState = STATE_SLOTS[slotIndex]
if slotState["active"]:
continue
itemCell = slotState["itemCell"]
attrType = self.__GetAttrType(slotIndex)
minValue = self.__GetMinValue(slotIndex)
if not self.__IsEligibleItem(itemCell) or attrType <= 0 or minValue <= 0:
continue
self.__Send("/switchbot start %d %d %d %d" % (slotIndex, itemCell, attrType, minValue))
def __StopAll(self):
self.__Send("/switchbot stop_all")
def Refresh(self):
self.__BuildCandidateSlots()
self.statusLine.SetText("Status: %d active slot(s)" % STATE_ACTIVE_COUNT)
self.scrollLine.SetText("Switch items: %d" % STATE_SCROLL_COUNT)
self.etaLine.SetText(self.__FormatEta())
self.helpLine.SetText("Select a row, assign an item, pick a target bonus and min value")
self.speedCombo.SetCurrentItem(self.speedLabels.get(STATE_SPEED, SPEED_OPTIONS[1][1]))
for slotIndex in range(len(self.slotWidgets)):
slotState = STATE_SLOTS[slotIndex]
row = self.slotWidgets[slotIndex]
row["selectButton"].SetText(("> " if self.selectedSlotIndex == slotIndex else "") + "S%d" % (slotIndex + 1))
row["itemLine"].SetText("Item: %s" % ("-" if not self.__IsEligibleItem(slotState["itemCell"]) else self.__GetItemLabel(slotState["itemCell"])))
row["statusLine"].SetText("Target: %s / tries %d / %s" % (
_GetAffectLabel(slotState["attrType"]),
slotState["attempts"],
"Running" if slotState["active"] else "Ready",
))
if slotState["attrType"] > 0:
row["attrCombo"].SetCurrentItem(_GetAffectLabel(slotState["attrType"]))
else:
row["attrCombo"].SetCurrentItem("-")
if slotState["minValue"] > 0 and row["valueEdit"].GetText() != str(slotState["minValue"]):
row["valueEdit"].SetText(str(slotState["minValue"]))
elif slotState["minValue"] <= 0 and row["valueEdit"].GetText():
row["valueEdit"].SetText("")
row["startButton"].SetText("Stop" if slotState["active"] else "Start")
start = self.pageIndex * self.CANDIDATE_PAGE_SIZE
end = start + self.CANDIDATE_PAGE_SIZE
pageSlots = self.candidateSlots[start:end]
totalPages = max(1, (len(self.candidateSlots) + self.CANDIDATE_PAGE_SIZE - 1) // self.CANDIDATE_PAGE_SIZE)
self.pageLine.SetText("Candidates %d / %d" % (self.pageIndex + 1, totalPages))
for localIndex in range(len(self.candidateButtons)):
button = self.candidateButtons[localIndex]
if localIndex < len(pageSlots):
button.SetText(self.__GetItemLabel(pageSlots[localIndex]))
button.Enable()
else:
button.SetText("-")
button.Disable()
if self.pageIndex > 0:
self.prevButton.Enable()
else:
self.prevButton.Disable()
if end < len(self.candidateSlots):
self.nextButton.Enable()
else:
self.nextButton.Disable()
def Show(self):
self.__Sync()
self.Refresh()
ui.BoardWithTitleBar.Show(self)

173
assets/root/uiTeleport.py Normal file
View File

@@ -0,0 +1,173 @@
import time
import background
import net
import player
import quest
import ui
class TeleportWindow(ui.BoardWithTitleBar):
TITLE = "Teleport System"
PRESET_BUTTONS = (
(1, "Village 1"),
(2, "Village 2"),
(3, "Valley"),
(4, "Desert"),
(5, "Sohan"),
(6, "Fireland"),
(7, "Devil"),
(8, "Cave"),
)
def __init__(self):
ui.BoardWithTitleBar.__init__(self)
self.lastRefreshTime = 0.0
self.slotRows = []
self.AddFlag("float")
self.AddFlag("movable")
self.SetSize(310, 255)
self.SetTitleName("Teleport")
self.SetCloseEvent(self.Hide)
self.__CreateChildren()
self.Hide()
def __del__(self):
ui.BoardWithTitleBar.__del__(self)
def __CreateChildren(self):
self.statusLine = self.__CreateValueLine(15, 36)
self.mapLine = self.__CreateValueLine(15, 56)
self.coordLine = self.__CreateValueLine(15, 76)
self.slotSummaryLine = self.__CreateValueLine(15, 96)
self.cooldownLine = self.__CreateValueLine(15, 116)
for index, presetData in enumerate(self.PRESET_BUTTONS):
(presetId, label) = presetData
row = index // 4
column = index % 4
button = self.__CreateButton(15 + column * 71, 142 + row * 24, 64, label)
button.SetEvent(self.__SendPresetWarp, presetId)
for slot in range(1, 6):
label = self.__CreateValueLine(15, 196 + (slot - 1) * 11)
saveButton = self.__CreateButton(100, 192 + (slot - 1) * 11, 42, "Save")
saveButton.SetEvent(self.__SendSaveSlot, slot)
useButton = self.__CreateButton(148, 192 + (slot - 1) * 11, 42, "Use")
useButton.SetEvent(self.__SendUseSlot, slot)
self.slotRows.append((label, saveButton, useButton))
def __CreateValueLine(self, x, y):
textLine = ui.TextLine()
textLine.SetParent(self)
textLine.SetPosition(x, y)
textLine.SetOutline()
textLine.Show()
return textLine
def __CreateButton(self, x, y, width, text):
button = ui.Button()
button.SetParent(self)
button.SetPosition(x, y)
button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
button.SetSize(width, 17)
button.SetText(text)
button.Show()
return button
def __GetTeleportQuestData(self):
questCount = min(quest.GetQuestCount(), quest.QUEST_MAX_NUM)
for questIndex in range(questCount):
(questName, questIcon, questCounterName, questCounterValue) = quest.GetQuestData(questIndex)
if questName != self.TITLE:
continue
maxSlots = 3
if "/" in questCounterName:
parts = questCounterName.split("/")
try:
maxSlots = int(parts[-1].strip())
except:
maxSlots = 3
(clockName, clockValue) = quest.GetQuestLastTime(questIndex)
return {
"savedCount" : max(questCounterValue, 0),
"maxSlots" : max(maxSlots, 3),
"clockName" : clockName,
"clockValue" : max(clockValue, 0),
}
return None
def __FormatCooldown(self, seconds):
return "%02d:%02d" % (seconds // 60, seconds % 60)
def __RefreshSlotRows(self, maxSlots):
for slot, row in enumerate(self.slotRows, 1):
(label, saveButton, useButton) = row
if slot <= maxSlots:
label.SetText("Slot %d" % slot)
saveButton.Enable()
useButton.Enable()
else:
label.SetText("Slot %d (VIP)" % slot)
saveButton.Disable()
useButton.Disable()
def Refresh(self):
teleportData = self.__GetTeleportQuestData()
(x, y, z) = player.GetMainCharacterPosition()
currentMap = background.GetCurrentMapName()
self.mapLine.SetText("Map: %s" % currentMap)
self.coordLine.SetText("Coords: %d, %d" % (x // 100, y // 100))
if not teleportData:
self.statusLine.SetText("Status: Waiting for teleport quest")
self.slotSummaryLine.SetText("Saved: -")
self.cooldownLine.SetText("Cooldown: -")
self.__RefreshSlotRows(3)
return
self.statusLine.SetText("Status: %s" % ("Cooldown" if teleportData["clockValue"] > 0 else "Ready"))
self.slotSummaryLine.SetText("Saved: %d / %d" % (teleportData["savedCount"], teleportData["maxSlots"]))
if teleportData["clockValue"] > 0 and len(teleportData["clockName"]) > 0:
self.cooldownLine.SetText("Cooldown: %s" % self.__FormatCooldown(teleportData["clockValue"]))
else:
self.cooldownLine.SetText("Cooldown: Ready")
self.__RefreshSlotRows(teleportData["maxSlots"])
def Show(self):
self.Refresh()
ui.BoardWithTitleBar.Show(self)
def OnUpdate(self):
currentTime = time.time()
if currentTime - self.lastRefreshTime < 0.2:
return
self.lastRefreshTime = currentTime
self.Refresh()
def __SendTeleportCommand(self, action, arg):
net.SendChatPacket("/teleport_system %s %d" % (action, arg), 0)
self.lastRefreshTime = 0.0
self.Refresh()
def __SendSaveSlot(self, slot):
self.__SendTeleportCommand("save", slot)
def __SendUseSlot(self, slot):
self.__SendTeleportCommand("saved", slot)
def __SendPresetWarp(self, presetId):
self.__SendTeleportCommand("preset", presetId)

View File

@@ -8,6 +8,7 @@ import localeInfo
import constInfo
import chrmgr
import player
import uiAutopickup
import uiPrivateShopBuilder # 占쏙옙占쏙옙호
import interfaceModule # 占쏙옙占쏙옙호
@@ -39,8 +40,14 @@ class OptionDialog(ui.ScriptWindow):
self.alwaysShowNameButtonList = []
self.showDamageButtonList = []
self.showsalesTextButtonList = []
self.autoPickupButton = None
self.autoPickupDialog = None
def Destroy(self):
if self.autoPickupDialog:
self.autoPickupDialog.Destroy()
self.autoPickupDialog = None
self.ClearDictionary()
self.__Initialize()
@@ -80,6 +87,7 @@ class OptionDialog(ui.ScriptWindow):
self.showDamageButtonList.append(GetObject("show_damage_off_button"))
self.showsalesTextButtonList.append(GetObject("salestext_on_button"))
self.showsalesTextButtonList.append(GetObject("salestext_off_button"))
self.autoPickupButton = GetObject("autopickup_button")
except:
import exception
@@ -130,6 +138,7 @@ class OptionDialog(ui.ScriptWindow):
self.showsalesTextButtonList[0].SAFE_SetEvent(self.__OnClickSalesTextOnButton)
self.showsalesTextButtonList[1].SAFE_SetEvent(self.__OnClickSalesTextOffButton)
self.autoPickupButton.SAFE_SetEvent(self.__OnClickAutoPickupButton)
self.__ClickRadioButton(self.nameColorModeButtonList, constInfo.GET_CHRNAME_COLOR_INDEX())
self.__ClickRadioButton(self.viewTargetBoardButtonList, constInfo.GET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD())
@@ -241,6 +250,15 @@ class OptionDialog(ui.ScriptWindow):
def __OnClickSalesTextOffButton(self):
systemSetting.SetShowSalesTextFlag(False)
self.RefreshShowSalesText()
def __OnClickAutoPickupButton(self):
if not self.autoPickupDialog:
self.autoPickupDialog = uiAutopickup.AutoPickupWindow()
if self.autoPickupDialog.IsShow():
self.autoPickupDialog.Hide()
else:
self.autoPickupDialog.Show()
def __CheckPvPProtectedLevelPlayer(self):
if player.GetStatus(player.LEVEL)<constInfo.PVPMODE_PROTECTED_LEVEL:

View File

@@ -11,6 +11,7 @@ import grp
import uiScriptLocale
import uiRefine
import uiAttachMetin
import uiStoneQueue
import uiPickMoney
import uiCommon
import uiPrivateShopBuilder # Prevent ItemMove while private shop is open
@@ -23,6 +24,12 @@ ITEM_FLAG_APPLICABLE = 1 << 14
class CostumeWindow(ui.ScriptWindow):
SLOT_ORDER = (
item.COSTUME_SLOT_BODY,
item.COSTUME_SLOT_HAIR,
item.COSTUME_SLOT_SASH,
)
def __init__(self, wndInventory):
import exception
@@ -87,8 +94,7 @@ class CostumeWindow(ui.ScriptWindow):
def RefreshCostumeSlot(self):
getItemVNum=player.GetItemIndex
for i in range(item.COSTUME_SLOT_COUNT):
slotNumber = item.COSTUME_SLOT_START + i
for slotNumber in self.SLOT_ORDER:
self.wndEquip.SetItemSlot(slotNumber, getItemVNum(slotNumber), 0)
self.wndEquip.RefreshSlot()
@@ -251,11 +257,13 @@ class InventoryWindow(ui.ScriptWindow):
isLoaded = 0
isOpenedCostumeWindowWhenClosingInventory = 0 # Whether costume window was open when closing inventory
isOpenedBeltWindowWhenClosingInventory = 0 # Whether belt inventory was open when closing inventory
sashAbsorbSlot = -1
def __init__(self):
ui.ScriptWindow.__init__(self)
self.isOpenedBeltWindowWhenClosingInventory = 0 # Whether belt inventory was open when closing inventory
self.sashAbsorbSlot = -1
self.__LoadWindow()
@@ -357,6 +365,10 @@ class InventoryWindow(ui.ScriptWindow):
self.attachMetinDialog = uiAttachMetin.AttachMetinDialog()
self.attachMetinDialog.Hide()
## StoneQueueDialog
self.stoneQueueDialog = uiStoneQueue.StoneQueueWindow()
self.stoneQueueDialog.Hide()
## MoneySlot
self.wndMoneySlot.SetEvent(ui.__mem_func__(self.OpenPickMoneyDialog))
@@ -407,6 +419,9 @@ class InventoryWindow(ui.ScriptWindow):
self.attachMetinDialog.Destroy()
self.attachMetinDialog = 0
self.stoneQueueDialog.Destroy()
self.stoneQueueDialog = 0
self.tooltipItem = None
self.wndItem = 0
self.wndEquip = 0
@@ -431,6 +446,7 @@ class InventoryWindow(ui.ScriptWindow):
self.equipmentTab = []
def Hide(self):
self.__AbortSashAbsorb(False)
if constInfo.GET_ITEM_QUESTION_DIALOG_STATUS():
self.OnCloseQuestionDialog()
return
@@ -455,6 +471,91 @@ class InventoryWindow(ui.ScriptWindow):
def Close(self):
self.Hide()
def __AbortSashAbsorb(self, showMessage=True):
if self.sashAbsorbSlot == -1:
return
self.sashAbsorbSlot = -1
if showMessage:
chat.AppendChat(chat.CHAT_TYPE_INFO, "Sash absorb canceled.")
def __IsSashItem(self, slotIndex):
itemVnum = player.GetItemIndex(slotIndex)
if not itemVnum:
return False
item.SelectItem(itemVnum)
return item.GetItemType() == item.ITEM_TYPE_COSTUME and item.GetItemSubType() == item.COSTUME_TYPE_SASH
def __CanAbsorbIntoSash(self, sashSlot, targetSlot):
if sashSlot == targetSlot:
return False
if player.IsEquipmentSlot(sashSlot) or player.IsEquipmentSlot(targetSlot):
return False
if player.GetItemMetinSocket(sashSlot, 0):
return False
targetVnum = player.GetItemIndex(targetSlot)
if not targetVnum:
return False
item.SelectItem(targetVnum)
itemType = item.GetItemType()
itemSubType = item.GetItemSubType()
if itemType == item.ITEM_TYPE_WEAPON:
return itemSubType != item.WEAPON_ARROW
if itemType != item.ITEM_TYPE_ARMOR:
return False
return itemSubType in (
item.ARMOR_BODY,
item.ARMOR_HEAD,
item.ARMOR_SHIELD,
item.ARMOR_WRIST,
item.ARMOR_FOOTS,
item.ARMOR_NECK,
item.ARMOR_EAR,
)
def __BeginSashAbsorb(self, sashSlot):
if player.IsEquipmentSlot(sashSlot):
chat.AppendChat(chat.CHAT_TYPE_INFO, "Unequip the sash before absorbing bonuses.")
return
if player.GetItemMetinSocket(sashSlot, 0):
chat.AppendChat(chat.CHAT_TYPE_INFO, "This sash already contains absorbed bonuses.")
return
if self.sashAbsorbSlot == sashSlot:
self.__AbortSashAbsorb()
return
self.sashAbsorbSlot = sashSlot
chat.AppendChat(chat.CHAT_TYPE_INFO, "Select a weapon or armor piece to absorb into the sash.")
def __OpenSashAbsorbQuestion(self, sashSlot, targetSlot):
targetVnum = player.GetItemIndex(targetSlot)
item.SelectItem(targetVnum)
targetName = item.GetItemName()
self.questionDialog = uiCommon.QuestionDialog()
self.questionDialog.SetText("Absorb bonuses from %s? The source item will be destroyed." % targetName)
self.questionDialog.SetAcceptEvent(ui.__mem_func__(self.__AcceptSashAbsorb))
self.questionDialog.SetCancelEvent(ui.__mem_func__(self.OnCloseQuestionDialog))
self.questionDialog.sashSlot = sashSlot
self.questionDialog.targetSlot = targetSlot
self.questionDialog.Open()
constInfo.SET_ITEM_QUESTION_DIALOG_STATUS(1)
def __AcceptSashAbsorb(self):
net.SendChatPacket("/sash absorb %d %d" % (self.questionDialog.sashSlot, self.questionDialog.targetSlot), 0)
self.__AbortSashAbsorb(False)
self.OnCloseQuestionDialog()
def SetInventoryPage(self, page):
self.inventoryPageIndex = page
self.inventoryTab[1-page].SetUp()
@@ -701,6 +802,18 @@ class InventoryWindow(ui.ScriptWindow):
itemSlotIndex = self.__InventoryLocalSlotPosToGlobalSlotPos(itemSlotIndex)
if self.sashAbsorbSlot != -1 and not mouseModule.mouseController.isAttached():
if itemSlotIndex == self.sashAbsorbSlot:
self.__AbortSashAbsorb()
return
if self.__CanAbsorbIntoSash(self.sashAbsorbSlot, itemSlotIndex):
self.__OpenSashAbsorbQuestion(self.sashAbsorbSlot, itemSlotIndex)
else:
chat.AppendChat(chat.CHAT_TYPE_INFO, "Only unequipped weapon or armor items can be absorbed.")
self.__AbortSashAbsorb(False)
return
if mouseModule.mouseController.isAttached():
attachedSlotType = mouseModule.mouseController.GetAttachedType()
attachedSlotPos = mouseModule.mouseController.GetAttachedSlotNumber()
@@ -841,20 +954,6 @@ class InventoryWindow(ui.ScriptWindow):
scrollIndex = player.GetItemIndex(scrollSlotPos)
targetIndex = player.GetItemIndex(targetSlotPos)
if player.REFINE_OK != player.CanRefine(scrollIndex, targetSlotPos):
return
###########################################################
self.__SendUseItemToItemPacket(scrollSlotPos, targetSlotPos)
#net.SendItemUseToItemPacket(scrollSlotPos, targetSlotPos)
return
###########################################################
###########################################################
#net.SendRequestRefineInfoPacket(targetSlotPos)
#return
###########################################################
result = player.CanRefine(scrollIndex, targetSlotPos)
if player.REFINE_ALREADY_MAX_SOCKET_COUNT == result:
@@ -879,7 +978,12 @@ class InventoryWindow(ui.ScriptWindow):
if player.REFINE_OK != result:
return
self.refineDialog.Open(scrollSlotPos, targetSlotPos)
item.SelectItem(targetIndex)
if item.GetItemType() == item.ITEM_TYPE_METIN:
self.stoneQueueDialog.Open(scrollSlotPos, targetSlotPos)
return
self.__SendUseItemToItemPacket(scrollSlotPos, targetSlotPos)
def DetachMetinFromItem(self, scrollSlotPos, targetSlotPos):
scrollIndex = player.GetItemIndex(scrollSlotPos)
@@ -1206,6 +1310,11 @@ class InventoryWindow(ui.ScriptWindow):
def __UseItem(self, slotIndex):
ItemVNum = player.GetItemIndex(slotIndex)
item.SelectItem(ItemVNum)
if self.__IsSashItem(slotIndex):
self.__BeginSashAbsorb(slotIndex)
return
if item.IsFlag(item.ITEM_FLAG_CONFIRM_WHEN_USE):
self.questionDialog = uiCommon.QuestionDialog()
self.questionDialog.SetText(localeInfo.INVENTORY_REALLY_USE_ITEM)

View File

@@ -222,6 +222,15 @@ class MiniMap(ui.ScriptWindow):
self.tooltipAtlasOpen = MapTextToolTip()
self.tooltipAtlasOpen.SetText(localeInfo.MINIMAP_SHOW_AREAMAP)
self.tooltipAtlasOpen.Show()
self.tooltipBiolog = MapTextToolTip()
self.tooltipBiolog.SetText("Biolog")
self.tooltipBiolog.Show()
self.tooltipTeleport = MapTextToolTip()
self.tooltipTeleport.SetText("Teleport")
self.tooltipTeleport.Show()
self.tooltipSwitchbot = MapTextToolTip()
self.tooltipSwitchbot.SetText("Switchbot")
self.tooltipSwitchbot.Show()
self.tooltipInfo = MapTextToolTip()
self.tooltipInfo.Show()
@@ -259,12 +268,21 @@ class MiniMap(ui.ScriptWindow):
self.MiniMapHideButton = 0
self.MiniMapShowButton = 0
self.AtlasShowButton = 0
self.BiologButton = 0
self.TeleportButton = 0
self.SwitchbotButton = 0
self.biologButtonEvent = None
self.teleportButtonEvent = None
self.switchbotButtonEvent = None
self.tooltipMiniMapOpen = 0
self.tooltipMiniMapClose = 0
self.tooltipScaleUp = 0
self.tooltipScaleDown = 0
self.tooltipAtlasOpen = 0
self.tooltipBiolog = 0
self.tooltipTeleport = 0
self.tooltipSwitchbot = 0
self.tooltipInfo = None
self.serverInfo = None
@@ -346,6 +364,39 @@ class MiniMap(ui.ScriptWindow):
if miniMap.IsAtlas():
self.AtlasShowButton.SetEvent(ui.__mem_func__(self.ShowAtlas))
self.BiologButton = ui.Button()
self.BiologButton.SetParent(self.OpenWindow)
self.BiologButton.SetPosition(9, 111)
self.BiologButton.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
self.BiologButton.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
self.BiologButton.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
self.BiologButton.SetText("Bio")
if self.biologButtonEvent:
self.BiologButton.SetEvent(self.biologButtonEvent)
self.BiologButton.Show()
self.TeleportButton = ui.Button()
self.TeleportButton.SetParent(self.OpenWindow)
self.TeleportButton.SetPosition(9, 132)
self.TeleportButton.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
self.TeleportButton.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
self.TeleportButton.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
self.TeleportButton.SetText("TP")
if self.teleportButtonEvent:
self.TeleportButton.SetEvent(self.teleportButtonEvent)
self.TeleportButton.Show()
self.SwitchbotButton = ui.Button()
self.SwitchbotButton.SetParent(self.OpenWindow)
self.SwitchbotButton.SetPosition(9, 153)
self.SwitchbotButton.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub")
self.SwitchbotButton.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub")
self.SwitchbotButton.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub")
self.SwitchbotButton.SetText("Sw")
if self.switchbotButtonEvent:
self.SwitchbotButton.SetEvent(self.switchbotButtonEvent)
self.SwitchbotButton.Show()
(ButtonPosX, ButtonPosY) = self.MiniMapShowButton.GetGlobalPosition()
self.tooltipMiniMapOpen.SetTooltipPosition(ButtonPosX, ButtonPosY)
@@ -361,6 +412,15 @@ class MiniMap(ui.ScriptWindow):
(ButtonPosX, ButtonPosY) = self.AtlasShowButton.GetGlobalPosition()
self.tooltipAtlasOpen.SetTooltipPosition(ButtonPosX, ButtonPosY)
(ButtonPosX, ButtonPosY) = self.BiologButton.GetGlobalPosition()
self.tooltipBiolog.SetTooltipPosition(ButtonPosX, ButtonPosY)
(ButtonPosX, ButtonPosY) = self.TeleportButton.GetGlobalPosition()
self.tooltipTeleport.SetTooltipPosition(ButtonPosX, ButtonPosY)
(ButtonPosX, ButtonPosY) = self.SwitchbotButton.GetGlobalPosition()
self.tooltipSwitchbot.SetTooltipPosition(ButtonPosX, ButtonPosY)
self.ShowMiniMap()
def Destroy(self):
@@ -441,6 +501,21 @@ class MiniMap(ui.ScriptWindow):
else:
self.tooltipAtlasOpen.Hide()
if True == self.BiologButton.IsIn():
self.tooltipBiolog.Show()
else:
self.tooltipBiolog.Hide()
if True == self.TeleportButton.IsIn():
self.tooltipTeleport.Show()
else:
self.tooltipTeleport.Hide()
if True == self.SwitchbotButton.IsIn():
self.tooltipSwitchbot.Show()
else:
self.tooltipSwitchbot.Hide()
def OnRender(self):
(x, y) = self.GetGlobalPosition()
fx = float(x)
@@ -485,3 +560,18 @@ class MiniMap(ui.ScriptWindow):
self.AtlasWindow.Hide()
else:
self.AtlasWindow.Show()
def SetBiologButtonEvent(self, event):
self.biologButtonEvent = event
if self.BiologButton:
self.BiologButton.SetEvent(event)
def SetTeleportButtonEvent(self, event):
self.teleportButtonEvent = event
if self.TeleportButton:
self.TeleportButton.SetEvent(event)
def SetSwitchbotButtonEvent(self, event):
self.switchbotButtonEvent = event
if self.SwitchbotButton:
self.SwitchbotButton.SetEvent(event)

View File

@@ -22,7 +22,7 @@ class PlayerGauge(ui.Gauge):
self.SetPosition(-100, -100)
ui.Gauge.Hide(self)
def OnUpdate(self):
def __UpdateScreenPosition(self):
playerIndex = player.GetMainCharacterIndex()
(x, y, z)=textTail.GetPosition(playerIndex)
@@ -30,6 +30,14 @@ class PlayerGauge(ui.Gauge):
isChat = textTail.IsChat(playerIndex)
ui.Gauge.SetPosition(self, int(x - self.GetWidth() // 2), int(y + 5) + isChat * 17)
def OnUpdate(self):
self.__UpdateScreenPosition()
def OnRender(self):
# Refresh the anchor every render so the gauge tracks interpolated
# character/text-tail positions on high refresh displays.
self.__UpdateScreenPosition()
def RefreshGauge(self):
self.curHP = player.GetStatus(player.HP)

View File

@@ -94,24 +94,32 @@ class PrivateShopAdvertisementBoard(ui.ThinBoard):
net.SendOnClickPacket(self.vid)
return True
def OnUpdate(self):
def __UpdateProjectedPosition(self):
if not self.vid:
return
projectVID = None
if systemSetting.IsShowSalesText():
self.Show()
x, y = chr.GetProjectPosition(self.vid, 220)
self.SetPosition(x - self.GetWidth()/2, y - self.GetHeight()/2)
projectVID = self.vid
elif player.GetMainCharacterIndex() == self.vid:
projectVID = player.GetMainCharacterIndex()
if projectVID is None:
self.Hide()
return
self.Show()
x, y = chr.GetProjectPosition(projectVID, 220)
self.SetPosition(x - self.GetWidth()/2, y - self.GetHeight()/2)
else:
for key in list(g_privateShopAdvertisementBoardDict.keys()):
if player.GetMainCharacterIndex() == key: # When the private shop is visible and closed, the player's own shop ID changes.
g_privateShopAdvertisementBoardDict[key].Show()
x, y = chr.GetProjectPosition(player.GetMainCharacterIndex(), 220)
g_privateShopAdvertisementBoardDict[key].SetPosition(x - self.GetWidth()/2, y - self.GetHeight()/2)
else:
g_privateShopAdvertisementBoardDict[key].Hide()
def OnUpdate(self):
self.__UpdateProjectedPosition()
def OnRender(self):
# Keep the board anchored to the interpolated render position instead
# of the legacy fixed update cadence.
self.__UpdateProjectedPosition()
class PrivateShopBuilder(ui.ScriptWindow):

View File

@@ -41,6 +41,9 @@ class OptionDialog(ui.ScriptWindow):
self.cameraModeButtonList = []
self.fogModeButtonList = []
self.tilingModeButtonList = []
self.renderFPSButtonList = []
self.renderFPSValues = [60, 120, 144, 240, 0]
self.performanceHUDButtonList = []
self.ctrlShadowQuality = 0
def Destroy(self):
@@ -72,6 +75,13 @@ class OptionDialog(ui.ScriptWindow):
self.fogModeButtonList.append(GetObject("fog_level2"))
self.tilingModeButtonList.append(GetObject("tiling_cpu"))
self.tilingModeButtonList.append(GetObject("tiling_gpu"))
self.renderFPSButtonList.append(GetObject("render_fps_60"))
self.renderFPSButtonList.append(GetObject("render_fps_120"))
self.renderFPSButtonList.append(GetObject("render_fps_144"))
self.renderFPSButtonList.append(GetObject("render_fps_240"))
self.renderFPSButtonList.append(GetObject("render_fps_max"))
self.performanceHUDButtonList.append(GetObject("performance_hud_off"))
self.performanceHUDButtonList.append(GetObject("performance_hud_on"))
self.tilingApplyButton=GetObject("tiling_apply")
#self.ctrlShadowQuality = GetObject("shadow_bar")
except:
@@ -106,6 +116,13 @@ class OptionDialog(ui.ScriptWindow):
self.tilingModeButtonList[0].SAFE_SetEvent(self.__OnClickTilingModeCPUButton)
self.tilingModeButtonList[1].SAFE_SetEvent(self.__OnClickTilingModeGPUButton)
self.renderFPSButtonList[0].SAFE_SetEvent(self.__OnClickRenderFPS60Button)
self.renderFPSButtonList[1].SAFE_SetEvent(self.__OnClickRenderFPS120Button)
self.renderFPSButtonList[2].SAFE_SetEvent(self.__OnClickRenderFPS144Button)
self.renderFPSButtonList[3].SAFE_SetEvent(self.__OnClickRenderFPS240Button)
self.renderFPSButtonList[4].SAFE_SetEvent(self.__OnClickRenderFPSMaxButton)
self.performanceHUDButtonList[0].SAFE_SetEvent(self.__OnClickPerformanceHUDOffButton)
self.performanceHUDButtonList[1].SAFE_SetEvent(self.__OnClickPerformanceHUDOnButton)
self.tilingApplyButton.SAFE_SetEvent(self.__OnClickTilingApplyButton)
@@ -115,6 +132,7 @@ class OptionDialog(ui.ScriptWindow):
self.__ClickRadioButton(self.fogModeButtonList, systemSetting.GetFogLevel())
# MR-14: -- END OF -- Fog update by Alaric
self.__ClickRadioButton(self.cameraModeButtonList, constInfo.GET_CAMERA_MAX_DISTANCE_INDEX())
self.RefreshRenderSettings()
if musicInfo.fieldMusic==musicInfo.METIN2THEMA:
self.selectMusicFile.SetText(uiSelectMusic.DEFAULT_THEMA)
@@ -179,6 +197,41 @@ class OptionDialog(ui.ScriptWindow):
self.__ClickRadioButton(self.fogModeButtonList, index)
def __ApplyAndSaveConfig(self):
systemSetting.ApplyConfig()
systemSetting.SaveConfig()
def __GetRenderFPSButtonIndex(self):
renderFPS = systemSetting.GetRenderFPS()
if renderFPS <= 0:
return len(self.renderFPSValues) - 1
try:
return self.renderFPSValues.index(renderFPS)
except ValueError:
bestIndex = 0
bestDistance = abs(self.renderFPSValues[0] - renderFPS)
for index in xrange(len(self.renderFPSValues) - 1):
distance = abs(self.renderFPSValues[index] - renderFPS)
if distance < bestDistance:
bestIndex = index
bestDistance = distance
return bestIndex
def RefreshRenderSettings(self):
self.__ClickRadioButton(self.renderFPSButtonList, self.__GetRenderFPSButtonIndex())
self.__ClickRadioButton(self.performanceHUDButtonList, 1 if systemSetting.IsShowPerformanceHUD() else 0)
def __SetRenderFPS(self, fps):
systemSetting.SetRenderFPS(fps)
self.__ApplyAndSaveConfig()
self.RefreshRenderSettings()
def __SetPerformanceHUD(self, isVisible):
systemSetting.SetShowPerformanceHUDFlag(1 if isVisible else 0)
self.__ApplyAndSaveConfig()
self.RefreshRenderSettings()
def __OnClickCameraModeShortButton(self):
self.__SetCameraMode(0)
@@ -194,6 +247,27 @@ class OptionDialog(ui.ScriptWindow):
def __OnClickFogModeLevel2Button(self):
self.__SetFogLevel(2)
def __OnClickRenderFPS60Button(self):
self.__SetRenderFPS(60)
def __OnClickRenderFPS120Button(self):
self.__SetRenderFPS(120)
def __OnClickRenderFPS144Button(self):
self.__SetRenderFPS(144)
def __OnClickRenderFPS240Button(self):
self.__SetRenderFPS(240)
def __OnClickRenderFPSMaxButton(self):
self.__SetRenderFPS(0)
def __OnClickPerformanceHUDOffButton(self):
self.__SetPerformanceHUD(False)
def __OnClickPerformanceHUDOnButton(self):
self.__SetPerformanceHUD(True)
def __OnChangeMusic(self, fileName):
self.selectMusicFile.SetText(fileName[:MUSIC_FILENAME_MAX_LEN])
@@ -239,6 +313,8 @@ class OptionDialog(ui.ScriptWindow):
return True
def Show(self):
self.RefreshRenderSettings()
self.__SetCurTilingMode()
ui.ScriptWindow.Show(self)
def Close(self):

View File

@@ -20,6 +20,14 @@ import constInfo
WARP_SCROLLS = [22011, 22000, 22010]
TALISMAN_ELEMENT_NAMES = {
1: "Fire",
2: "Ice",
3: "Lightning",
4: "Wind",
5: "Earth",
}
# Tooltip max width when descriptions are long
DESC_MAX_WIDTH = 280
@@ -1021,12 +1029,14 @@ class ItemToolTip(ToolTip):
isCostumeItem = 0
isCostumeHair = 0
isCostumeBody = 0
isCostumeSash = 0
if app.ENABLE_COSTUME_SYSTEM:
if item.ITEM_TYPE_COSTUME == itemType:
isCostumeItem = 1
isCostumeHair = item.COSTUME_TYPE_HAIR == itemSubType
isCostumeBody = item.COSTUME_TYPE_BODY == itemSubType
isCostumeSash = item.COSTUME_TYPE_SASH == itemSubType
#dbg.TraceError("IS_COSTUME_ITEM! body(%d) hair(%d)" % (isCostumeBody, isCostumeHair))
@@ -1117,6 +1127,16 @@ class ItemToolTip(ToolTip):
self.__AppendAccessoryMetinSlotInfo(metinSlot, constInfo.GET_BELT_MATERIAL_VNUM(itemVnum))
### Talisman ###
elif 26 == itemType:
self.__AppendLimitInformation()
self.__AppendAffectInformation()
self.__AppendAttributeInformation(attrSlot)
self.AppendWearableInformation()
self.AppendSpace(5)
elementName = TALISMAN_ELEMENT_NAMES.get(item.GetValue(0), "Unknown")
self.AppendTextLine("Element: %s" % elementName, self.NORMAL_COLOR)
## Costume Item ##
elif 0 != isCostumeItem:
self.__AppendLimitInformation()
@@ -1124,6 +1144,15 @@ class ItemToolTip(ToolTip):
self.__AppendAttributeInformation(attrSlot)
self.AppendWearableInformation()
if isCostumeSash:
self.AppendSpace(5)
absorbPct = metinSlot[1] if metinSlot and metinSlot[1] else item.GetValue(0)
self.AppendTextLine("Absorb rate: %d%%" % absorbPct, self.NORMAL_COLOR)
if metinSlot and metinSlot[0]:
self.AppendTextLine("Absorbed item vnum: %d" % metinSlot[0], self.NORMAL_COLOR)
else:
self.AppendTextLine("Right-click the sash, then select an item to absorb.", self.CONDITION_COLOR)
bHasRealtimeFlag = 0
for i in range(item.LIMIT_MAX_NUM):

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

@@ -1,8 +1,6 @@
import uiScriptLocale
import item
COSTUME_START_INDEX = item.COSTUME_SLOT_START
window = {
"name" : "CostumeWindow",
@@ -71,9 +69,9 @@ window = {
"height" : 145,
"slot" : (
{"index":COSTUME_START_INDEX+0, "x":61, "y":45, "width":32, "height":64},
{"index":COSTUME_START_INDEX+1, "x":61, "y": 8, "width":32, "height":32},
{"index":COSTUME_START_INDEX+2, "x":5, "y":145, "width":32, "height":32},
{"index":item.COSTUME_SLOT_BODY, "x":61, "y":45, "width":32, "height":64},
{"index":item.COSTUME_SLOT_HAIR, "x":61, "y": 8, "width":32, "height":32},
{"index":item.COSTUME_SLOT_SASH, "x":5, "y":145, "width":32, "height":32},
),
},
),

View File

@@ -69,6 +69,8 @@ window = {
##{"index":22, "x":75, "y":106, "width":32, "height":32},
## 새 벨트
{"index":23, "x":39, "y":106, "width":32, "height":32},
{"index":24, "x":2, "y":106, "width":32, "height":32},
{"index":25, "x":75, "y":106, "width":32, "height":32},
),
},

View File

@@ -20,7 +20,7 @@ window = {
"y" : 0,
"width" : 300,
"height" : 25*11+8,
"height" : 25*12+8,
"children" :
(
@@ -32,7 +32,7 @@ window = {
"y" : 0,
"width" : 300,
"height" : 25*11+8,
"height" : 25*12+8,
"children" :
(
@@ -433,7 +433,29 @@ window = {
"default_image" : ROOT_PATH + "middle_button_01.sub",
"over_image" : ROOT_PATH + "middle_button_02.sub",
"down_image" : ROOT_PATH + "middle_button_03.sub",
},
},
{
"name" : "autopickup_label",
"type" : "text",
"x" : LINE_LABEL_X,
"y" : 265+2,
"text" : "Auto Pickup",
},
{
"name" : "autopickup_button",
"type" : "button",
"x" : LINE_DATA_X,
"y" : 265,
"text" : "Configure",
"default_image" : ROOT_PATH + "middle_button_01.sub",
"over_image" : ROOT_PATH + "middle_button_02.sub",
"down_image" : ROOT_PATH + "middle_button_03.sub",
},
),
},
),

View File

@@ -91,6 +91,7 @@ window = {
# {"index":item.EQUIPMENT_RING2, "x":75, "y":106, "width":32, "height":32},
## »ő ş§Ć®
{"index":item.EQUIPMENT_BELT, "x":39, "y":106, "width":32, "height":32},
{"index":EQUIPMENT_START_INDEX+25, "x":75, "y":106, "width":32, "height":32},
),
},
## Dragon Soul Button

View File

@@ -1,6 +1,11 @@
import uiScriptLocale
ROOT_PATH = "d:/ymir work/ui/public/"
OPTION_RENDER_FPS = getattr(uiScriptLocale, "OPTION_RENDER_FPS", "Render FPS")
OPTION_RENDER_FPS_MAX = getattr(uiScriptLocale, "OPTION_RENDER_FPS_MAX", "MAX")
OPTION_PERFORMANCE_HUD = getattr(uiScriptLocale, "OPTION_PERFORMANCE_HUD", "Perf HUD")
OPTION_ON = getattr(uiScriptLocale, "OPTION_ON", "On")
OPTION_OFF = getattr(uiScriptLocale, "OPTION_OFF", "Off")
TEMPORARY_X = +13
TEXT_TEMPORARY_X = -10
@@ -15,7 +20,7 @@ window = {
"y" : 0,
"width" : 305,
"height" : 255,
"height" : 295,
"children" :
(
@@ -27,7 +32,7 @@ window = {
"y" : 0,
"width" : 305,
"height" : 255,
"height" : 295,
"children" :
(
@@ -261,6 +266,124 @@ window = {
"down_image" : ROOT_PATH + "middle_Button_03.sub",
},
{
"name" : "render_fps_mode",
"type" : "text",
"x" : 18,
"y" : 210 + 2,
"text" : OPTION_RENDER_FPS,
},
{
"name" : "render_fps_60",
"type" : "radio_button",
"x" : 82,
"y" : 210,
"text" : "60",
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
{
"name" : "render_fps_120",
"type" : "radio_button",
"x" : 124,
"y" : 210,
"text" : "120",
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
{
"name" : "render_fps_144",
"type" : "radio_button",
"x" : 166,
"y" : 210,
"text" : "144",
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
{
"name" : "render_fps_240",
"type" : "radio_button",
"x" : 208,
"y" : 210,
"text" : "240",
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
{
"name" : "render_fps_max",
"type" : "radio_button",
"x" : 250,
"y" : 210,
"text" : OPTION_RENDER_FPS_MAX,
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
{
"name" : "performance_hud_mode",
"type" : "text",
"x" : 18,
"y" : 235 + 2,
"text" : OPTION_PERFORMANCE_HUD,
},
{
"name" : "performance_hud_off",
"type" : "radio_button",
"x" : 110,
"y" : 235,
"text" : OPTION_OFF,
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
{
"name" : "performance_hud_on",
"type" : "radio_button",
"x" : 160,
"y" : 235,
"text" : OPTION_ON,
"default_image" : ROOT_PATH + "small_Button_01.sub",
"over_image" : ROOT_PATH + "small_Button_02.sub",
"down_image" : ROOT_PATH + "small_Button_03.sub",
},
## 그림자
# {

91
docs/linux-wine.md Normal file
View File

@@ -0,0 +1,91 @@
# Running the client on Linux with Wine
This is an interim path for playing and testing on Linux while a native Linux port is a longer-term goal. Wine runs the unmodified Windows build of `Metin2.exe` / `Metin2_Debug.exe` directly. Verified to reach the character selection screen on Fedora 41 with Wine 10 Staging; other modern distros should work the same.
Use this when you want to:
- Smoke-test the Windows binary without rebooting into Windows
- Develop server-side with a live client connected from the same machine
- Run a dev loop without owning a Windows install
## Requirements
- A recent Wine (10.x Staging tested, 9.x stable should work). Older than 8 may be rough on D3D9.
- `winetricks` for installing MSVC runtime, D3DX9 helper DLLs, core fonts, and Tahoma
- A copy of the client deploy folder (the one containing `Metin2.exe`, `Metin2_Debug.exe`, `assets/`, `pack/`, `bgm/`, `config/`, `log/`). The whole folder is ~4.3 GB.
- ~7 GB free disk for the writable client copy plus the Wine prefix
On Fedora:
```bash
sudo dnf install -y wine winetricks
```
On Debian/Ubuntu (use the WineHQ repo for a modern version):
```bash
sudo apt install -y wine winetricks
```
## One-shot setup
The easiest way is the helper script in this repo:
```bash
./scripts/setup-wine-prefix.sh /path/to/windows/client ~/metin-wine
```
This will:
1. Copy the client folder to `~/metin-wine/client` (needs to be on a writable filesystem, so an NTFS read-only mount won't do).
2. Create a fresh Wine prefix at `~/metin-wine/prefix`.
3. Install `vcrun2022`, `d3dx9`, `corefonts`, and `tahoma` via winetricks.
4. Print the launch command.
See the script itself for exact steps if you prefer to run them manually.
## Why Tahoma is required
The client hard-codes Tahoma as its UI font. On Windows this is invisible because Tahoma ships with the OS; on a fresh Wine prefix it's missing, and the result is that the login screen renders layouts and backgrounds correctly but **all text is invisible**. You can reach the server picker and character selection, you just can't read anything. Installing Tahoma via `winetricks tahoma` fixes it in one shot.
If the login screen looks right but has no readable text, this is what you're seeing.
## Launching
After setup, the launch command is just:
```bash
cd ~/metin-wine/client
WINEPREFIX=~/metin-wine/prefix wine Metin2.exe
```
Use `Metin2_Debug.exe` instead of `Metin2.exe` if you want more verbose client-side logging via `OutputDebugString`. Wine will echo those to stderr when `WINEDEBUG` includes `+seh` or you pass `+outputdebugstring`. For normal play use `-all,+err`.
## Logs and debug output
Useful `WINEDEBUG` settings:
- `WINEDEBUG=-all,+err` — quiet, only real errors. Use this for normal play.
- `WINEDEBUG=-all,+loaddll,+module,+err` — shows which DLLs Wine loads, handy when the client crashes early with a missing DLL.
- `WINEDEBUG=-all,+err,+seh` — captures the client's own `OutputDebugString` calls via SEH, which is how metin2's internal logging surfaces. Very noisy but useful when diagnosing client-side issues ("CResource::Load file not exist X", "CPythonNonPlayer::LoadNonPlayerData", etc.).
Redirect to a file and grep the signal out of the noise:
```bash
WINEDEBUG=-all,+err,+seh wine Metin2_Debug.exe >wine-run.log 2>&1
grep -E 'OutputDebugString[AW] "' wine-run.log | sed 's/.*OutputDebugString[AW] //' | sort -u
```
The client also writes its own logs to `log/` inside the client folder. Those are plain text and more readable than the Wine SEH traces.
## Known quirks
- **Wayland:** works via XWayland, no special config. If the window opens minimized or off-screen, `Alt+Tab` to find it.
- **Read-only NTFS mount:** don't try to launch from a read-only mount of your Windows partition. The client creates and writes `log/`, `config/`, and cache files; on a read-only FS the launch will be confusing. Always copy to a writable location first. `setup-wine-prefix.sh` does this for you.
- **DXVK render state warnings:** lines like `D3D9DeviceEx::SetRenderState: Unhandled render state 163` in the log are harmless. DXVK doesn't implement every legacy D3D9 render state, but the ones metin2 cares about all work.
- **SEH dispatch spam:** `dispatch_exception code=4001000a` / `4001000c` are how Windows signals `OutputDebugStringW` / `OutputDebugStringA`. They're soft exceptions, not errors. They only show up if you enable `+seh` in `WINEDEBUG`.
- **First launch is slower:** DXVK compiles its shader pipelines on first run and writes a state cache. Subsequent launches are noticeably faster.
## When to stop using Wine
This guide is for the interim. The longer-term plan is a native Linux build of the client with a free-software replacement for Granny2 animation runtime. Until that lands, Wine is the way.

97
scripts/setup-wine-prefix.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Set up a Wine prefix for running the Metin2 client on Linux.
# Idempotent: re-running on an existing prefix skips steps that are already done.
#
# Usage:
# ./scripts/setup-wine-prefix.sh <source-client-dir> <target-dir>
#
# Example:
# ./scripts/setup-wine-prefix.sh /mnt/windows_c/Users/me/metin/client ~/metin-wine
#
# Result layout:
# <target-dir>/client/ — writable copy of the client deploy folder
# <target-dir>/prefix/ — Wine prefix with required runtime deps installed
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "usage: $0 <source-client-dir> <target-dir>" >&2
echo " source-client-dir: path containing Metin2.exe, assets/, pack/, etc." >&2
echo " target-dir: directory to create (holds client/ and prefix/)" >&2
exit 2
fi
SRC=$1
DEST=$2
if [[ ! -f "$SRC/Metin2.exe" && ! -f "$SRC/Metin2_Debug.exe" ]]; then
echo "error: $SRC does not look like a client folder (no Metin2.exe or Metin2_Debug.exe)" >&2
exit 1
fi
for tool in wine winetricks; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "error: $tool not found in PATH. Install it via your package manager." >&2
exit 1
fi
done
CLIENT_DIR=$DEST/client
PREFIX_DIR=$DEST/prefix
mkdir -p "$DEST"
if [[ -d "$CLIENT_DIR" && -f "$CLIENT_DIR/Metin2.exe" ]] || [[ -d "$CLIENT_DIR" && -f "$CLIENT_DIR/Metin2_Debug.exe" ]]; then
echo "[1/3] client already present at $CLIENT_DIR, skipping copy"
else
echo "[1/3] copying client from $SRC to $CLIENT_DIR (this can take a minute)"
cp -a "$SRC" "$CLIENT_DIR"
fi
export WINEPREFIX=$PREFIX_DIR
export WINEARCH=win64
if [[ -f "$PREFIX_DIR/system.reg" ]]; then
echo "[2/3] wine prefix already exists at $PREFIX_DIR, skipping wineboot"
else
echo "[2/3] creating wine prefix at $PREFIX_DIR"
mkdir -p "$PREFIX_DIR"
wineboot --init >/dev/null 2>&1 || true
fi
# vcrun2022 — MSVC 2015-2022 runtime, required because the client is an MSVC build
# d3dx9 — D3DX9 helper DLLs (Wine implements d3d9 but not the d3dx9 helpers)
# corefonts — Arial/Courier/Times/etc., needed by some UI elements
# tahoma — the client hard-codes Tahoma as the UI font; without it, all text renders invisibly
VERBS=(vcrun2022 d3dx9 corefonts tahoma)
TO_INSTALL=()
for v in "${VERBS[@]}"; do
case $v in
vcrun2022)
if [[ -f "$PREFIX_DIR/drive_c/windows/system32/msvcp140.dll" ]]; then continue; fi ;;
d3dx9)
if [[ -f "$PREFIX_DIR/drive_c/windows/system32/d3dx9_43.dll" ]]; then continue; fi ;;
corefonts)
if [[ -f "$PREFIX_DIR/drive_c/windows/Fonts/arial.ttf" ]]; then continue; fi ;;
tahoma)
if [[ -f "$PREFIX_DIR/drive_c/windows/Fonts/tahoma.ttf" ]]; then continue; fi ;;
esac
TO_INSTALL+=("$v")
done
if [[ ${#TO_INSTALL[@]} -eq 0 ]]; then
echo "[3/3] all winetricks verbs already installed"
else
echo "[3/3] installing winetricks verbs: ${TO_INSTALL[*]}"
winetricks -q "${TO_INSTALL[@]}"
fi
echo
echo "done. to launch:"
echo
echo " cd $CLIENT_DIR"
echo " WINEPREFIX=$PREFIX_DIR wine Metin2.exe"
echo
echo "or with verbose client logging:"
echo
echo " WINEPREFIX=$PREFIX_DIR WINEDEBUG=-all,+err,+seh wine Metin2_Debug.exe"