Compare commits

...

13 Commits

Author SHA1 Message Date
pleb be040c06d6 pleb: use ir_black textcolors 2026-07-30 10:20:09 -07:00
pleb 398b7dfa81 Polish readme 2026-07-22 12:01:50 -07:00
pleb 70920f2167 Reorganize docs 2026-07-22 11:39:41 -07:00
pleb 696b0b777f Add spell data helper and organize screenshots 2026-07-22 11:25:38 -07:00
pleb 356a637385 Reorganize profiles and capture EQL 2026-07-20 20:10:52 -07:00
pleb fd6cba7c04 Update for 2026 2026-05-28 22:03:14 -07:00
pleb 2d9be73419 Overhaul scripts for 2026 refresh 2026-05-28 22:03:01 -07:00
Isuldor f7c8dde867 new link 2019-03-22 17:37:11 -07:00
Isuldor d59348c123 Progress on removing UI decorations from chat windows. Only one border left 2019-03-22 17:34:10 -07:00
Isuldor e3bc361ed3 Add link to WSL 2019-03-22 15:21:42 -07:00
Isuldor 78f83c459e Rebuilt diff files, chatwindow needs a new minimal template after upstream changes 2019-03-16 20:43:24 -07:00
Isuldor 5c8c0d481e Ignore whitespace when patching uifiles 2019-03-16 14:59:52 -07:00
Isuldor 239183e580 Update uifiles for 20th anniversary 2019-03-16 11:27:34 -07:00
43 changed files with 4683 additions and 302 deletions
+8
View File
@@ -0,0 +1,8 @@
.venv
.DS_Store/
__pycache__/
*.pyc
*.egg-info/
dist/
.env
+3
View File
@@ -0,0 +1,3 @@
# AGENTS.md
* [EverQuest Legends spell data](/docs/eql-spell-data.md)
+59
View File
@@ -0,0 +1,59 @@
[HotButtons]
Page1Button12=M-1,A633,wGy000u0000I0000,177708,,Boots of the Long Road
Page1Button1=I2,@-1,0000000000000000,0,Pet attack,
[Defaults]
AutoConsentGroup=1
AutoConsentRaid=1
AutoConsentGuild=1
AutoConsentFellowship=1
Realism=1
Music=10
SoundVolume=10
EnvSounds=1
CombatMusic=1
[Combat]
AttackOnAssist=1
UseImpliedHealing=0
AutoAttackRangeAutoSwitch=1
SuppressOutOfRangeWarnings=0
SuppressAttackWarnings=0
AutoSkillsLO0=30,0,0,0,0,0,0,0
[Friends]
SendToUChat=0
[BlockedSpells]
BlockedSpellID0=-1
[BlockedPetSpells]
BlockedPetSpellID0=-1
[StanceWnd]
Hidden=
[ExternalTargetRoles]
Current=1^Autosave^20^0^1^1^1^2^1^3^1^4^1^5^1^6^1^7^1^8^1^9^1^10^1^11^1^12^1^13^1^14^1^15^1^16^1^17^1^18^1^19^1
[ADDITIONALFILTERS]
FilterByPlayerMode=1
FilterByExclusiveSearch=0
[Socials]
Page1Button1Name=Location
Page1Button1Color=0
Page1Button1Line1=/location
Page1Button2Name=Assist
Page1Button2Color=0
Page1Button3Name=Hide
Page1Button3Color=0
Page1Button3Line1=/hidecorpse all
Page1Button4Name=Drag
Page1Button4Color=0
Page1Button7Name=Mez
Page1Button7Color=0
Page1Button7Line1=/g mezzed %T
Page1Button5Name=PetHP
Page1Button5Color=0
Page1Button6Name=Test
Page1Button6Color=0
Page1Button6Line1=/say pause 10
Page1Button6Line2=/pause 10
Page1Button6Line3=/say test complete
Page1Button5Line1=/g our undead pet Isuldor has low hp!
Page1Button4Line1=/corpse Tanto
Page1Button2Line1=/assist main
Page1Button2Line2=/pause 6
Page1Button2Line3=/pet attack
File diff suppressed because it is too large Load Diff
+802
View File
@@ -0,0 +1,802 @@
[Defaults]
UseWASDDefault=1
GraphicsMemoryModeSwitch=1
APVOptimizations=TRUE
Sound=1
TextureQuality=1
VertexShaders=TRUE
MultiPassLighting=FALSE
PostEffects=FALSE
UseLitBatches=FALSE
ItemPlacementShowOverlay=TRUE
WindowedModeXOffset=906
WindowedModeYOffset=60
AllowResize=1
Maximized=0
AlwaysOnTop=0
ChatFontSize=3
ShowNamesLevel=4
MousePointerSpeedMod=0
ShowSpellEffects=1
MixAhead=8
TrackPlayers=1
TrackSortType=NORMAL
TrackFilterType=0
Sound44k=0
HidePlayers=0
HidePets=0
HideFamiliars=0
HideMercs=0
GraphicsMemoryModeAutoChecked=1
AllLuclinPcModelsOff=1
DefaultChannel=8
LastCharSel=0
ShowCreationHelp=0
Music=10
SoundVolume=10
BrightnessBias=0.000000
SpellParticleOpacity=1.000000
EnvironmentParticleOpacity=1.000000
GraphicsMemoryMode=2
HasViewedHotBarHelp1=1
Log=1
[VideoMode]
Width=3440
Height=1440
WindowedWidth=1920
WindowedHeight=1080
FullscreenBitsPerPixel=32
FullscreenRefreshRate=100
Fullscreen=0
[Options]
ClickThroughMask=1
Camera1-Distance=30.000000
Camera1-DirHeading=192.000000
Camera1-Heading=0.000000
Camera1-Pitch=0.000000
Camera1-Height=5.000000
Camera1-Zoom=90.000000
Camera1-Change=1
Camera2-Distance=82.000000
Camera2-DirHeading=277.000000
Camera2-Heading=0.000000
Camera2-Pitch=0.000000
Camera2-Height=18.000000
Camera2-Zoom=90.000000
Camera2-Change=1
Realism=1
ClipPlane=20
MaxFPS=360
MaxBGFPS=60
NameFlashSpeed=5
XMouseSensitivity=51
YMouseSensitivity=51
SavePersonaChat=0
PCNames=1
Sky=2
[TextColors]
#EEEEEE white (say)
User_1_Red=204
User_1_Green=204
User_1_Blue=204
#FF73FD bright magenta (tell)
User_2_Red=255
User_2_Green=156
User_2_Blue=254
#DFDFFE cyan
User_3_Red=150
User_3_Green=203
User_3_Blue=254
#A8FF60 green
User_4_Red=168
User_4_Green=255
User_4_Blue=96
#EEEEEE white (ooc)
User_5_Red=204
User_5_Green=204
User_5_Blue=204
#A8FF60 green
User_6_Red=204
User_6_Green=204
User_6_Blue=204
#FF6C60 bright red
User_7_Red=255
User_7_Green=108
User_7_Blue=96
#FF9CFE bright blue
User_8_Red=150
User_8_Green=203
User_8_Blue=254
#FF9CFE bright blue
User_9_Red=204
User_9_Green=204
User_9_Blue=204
#EEEEEE white
User_10_Red=204
User_10_Green=204
User_10_Blue=204
#FF6C60 bright red
User_11_Red=255
User_11_Green=108
User_11_Blue=96
#EEEEEE white
User_12_Red=204
User_12_Green=204
User_12_Blue=204
#EEEEEE white
User_13_Red=204
User_13_Green=204
User_13_Blue=204
#FFFFCB bright yellow
User_14_Red=255
User_14_Green=255
User_14_Blue=182
#FF9CFE bright blue
User_15_Red=204
User_15_Green=204
User_15_Blue=204
#EEEEEE white
User_16_Red=204
User_16_Green=204
User_16_Blue=204
#EEEEEE white
User_17_Red=204
User_17_Green=204
User_17_Blue=204
#EEEEEE white
User_18_Red=204
User_18_Green=204
User_18_Blue=204
#EEEEEE white
User_19_Red=204
User_19_Green=204
User_19_Blue=204
#FFFFB6 yellow
User_20_Red=204
User_20_Green=204
User_20_Blue=204
#FFFFB6 yellow
User_21_Red=204
User_21_Green=204
User_21_Blue=204
#EEEEEE white
User_22_Red=204
User_22_Green=204
User_22_Blue=204
#EEEEEE white
User_23_Red=204
User_23_Green=204
User_23_Blue=204
#EEEEEE white
User_24_Red=204
User_24_Green=204
User_24_Blue=204
#EEEEEE white
User_25_Red=204
User_25_Green=204
User_25_Blue=204
#FF73FD magenta
User_26_Red=204
User_26_Green=204
User_26_Blue=204
#EEEEEE white
User_27_Red=204
User_27_Green=204
User_27_Blue=204
#FF9CFE bright blue
User_28_Red=204
User_28_Green=204
User_28_Blue=204
#FFFFB6 yellow
User_29_Red=255
User_29_Green=255
User_29_Blue=182
#A8FF60 green
User_30_Red=204
User_30_Green=204
User_30_Blue=204
#FF9CFE bright blue
User_31_Red=204
User_31_Green=204
User_31_Blue=204
#FF6C60 bright red
User_32_Red=238
User_32_Green=238
User_32_Blue=238
#FF9CFE bright blue
User_33_Red=204
User_33_Green=204
User_33_Blue=204
#FF6C60 bright red
User_34_Red=238
User_34_Green=238
User_34_Blue=238
#D79A42 gold, DEFAULT
User_35_Red=204
User_35_Green=204
User_35_Blue=204
#6E8FB0 blue-ish, DEFAULT
User_36_Red=204
User_36_Green=204
User_36_Blue=204
#6E8FB0 blue-ish, DEFAULT
User_37_Red=204
User_37_Green=204
User_37_Blue=204
#6E8FB0 blue-ish, DEFAULT
User_38_Red=204
User_38_Green=204
User_38_Blue=204
#6E8FB0 blue-ish, DEFAULT
User_39_Red=204
User_39_Green=204
User_39_Blue=204
#6E8FB0 blue-ish, DEFAULT
User_40_Red=204
User_40_Green=204
User_40_Blue=204
#6E8FB0 blue-ish, DEFAULT
User_41_Red=110
User_41_Green=143
User_41_Blue=176
#6E8FB0 blue-ish, DEFAULT
User_42_Red=110
User_42_Green=143
User_42_Blue=176
#6E8FB0 blue-ish, DEFAULT
User_43_Red=110
User_43_Green=143
User_43_Blue=176
#6E8FB0 blue-ish, DEFAULT
User_44_Red=110
User_44_Green=143
User_44_Blue=176
#6E8FB0 blue-ish, DEFAULT
User_45_Red=204
User_45_Green=204
User_45_Blue=204
#EEEEEE white
User_46_Red=204
User_46_Green=204
User_46_Blue=204
#EEEEEE white
User_47_Red=204
User_47_Green=204
User_47_Blue=204
#FF6C60 bright red
User_48_Red=204
User_48_Green=204
User_48_Blue=204
#FF6C60 bright red
User_49_Red=255
User_49_Green=108
User_49_Blue=96
#FF6C60 bright red
User_50_Red=255
User_50_Green=108
User_50_Blue=96
#FF6C60 bright red
User_51_Red=255
User_51_Green=108
User_51_Blue=96
#EEEEEE white
User_52_Red=204
User_52_Green=204
User_52_Blue=204
#EEEEEE white
User_53_Red=238
User_53_Green=238
User_53_Blue=238
#EEEEEE white
User_54_Red=238
User_54_Green=238
User_54_Blue=238
#EEEEEE white
User_55_Red=238
User_55_Green=238
User_55_Blue=238
#EEEEEE white
User_56_Red=238
User_56_Green=238
User_56_Blue=238
#EEEEEE white
User_57_Red=238
User_57_Green=238
User_57_Blue=238
#EEEEEE white
User_58_Red=238
User_58_Green=238
User_58_Blue=238
#EEEEEE white
User_59_Red=204
User_59_Green=204
User_59_Blue=204
#FF9A42 bright gold, DEFAULT
User_60_Red=238
User_60_Green=238
User_60_Blue=238
#FF9A42 bright gold, DEFAULT
User_61_Red=238
User_61_Green=238
User_61_Blue=238
#FF9A42 bright gold, DEFAULT
User_62_Red=238
User_62_Green=238
User_62_Blue=238
#FF9A42 bright gold, DEFAULT
User_63_Red=238
User_63_Green=238
User_63_Blue=238
#FF9A42 bright gold, DEFAULT
User_64_Red=238
User_64_Green=238
User_64_Blue=238
#FF9A42 bright gold, DEFAULT
User_65_Red=215
User_65_Green=154
User_65_Blue=66
#FF9A42 bright gold, DEFAULT
User_66_Red=215
User_66_Green=154
User_66_Blue=66
#FF9A42 bright gold, DEFAULT
User_67_Red=215
User_67_Green=154
User_67_Blue=66
#FF9A42 bright gold, DEFAULT
User_68_Red=215
User_68_Green=154
User_68_Blue=66
#FF9A42 bright gold, DEFAULT
User_69_Red=238
User_69_Green=238
User_69_Blue=238
#FFFFCB bright yellow
User_70_Red=255
User_70_Green=255
User_70_Blue=203
#FF9CFE bright magenta (was hot pink..)
User_71_Red=255
User_71_Green=255
User_71_Blue=182
#C6C5FE cyan
User_72_Red=150
User_72_Green=203
User_72_Blue=254
#EEEEEE white
User_73_Red=204
User_73_Green=204
User_73_Blue=204
#EEEEEE white
User_74_Red=255
User_74_Green=108
User_74_Blue=96
#C6C5FE bright cyan
User_75_Red=223
User_75_Green=223
User_75_Blue=254
#FF6C60 bright red
User_76_Red=204
User_76_Green=204
User_76_Blue=204
#EEEEEE white
User_77_Red=204
User_77_Green=204
User_77_Blue=204
#FF9CFE bright blue
User_78_Red=204
User_78_Green=204
User_78_Blue=204
#FFFFCB bright yellow
User_79_Red=255
User_79_Green=255
User_79_Blue=182
#FFFFCB bright yellow
User_80_Red=255
User_80_Green=255
User_80_Blue=203
#EEEEEE white
User_81_Red=204
User_81_Green=204
User_81_Blue=204
#EEEEEE white
User_82_Red=204
User_82_Green=204
User_82_Blue=204
#EEEEEE white
User_83_Red=255
User_83_Green=115
User_83_Blue=253
#EEEEEE white
User_84_Red=204
User_84_Green=204
User_84_Blue=204
#EEEEEE white
User_85_Red=204
User_85_Green=204
User_85_Blue=204
#FF6C60 red
User_86_Red=255
User_86_Green=108
User_86_Blue=96
#FF9CFE bright blue
User_87_Red=150
User_87_Green=203
User_87_Blue=254
#EEEEEE white
User_88_Red=238
User_88_Green=238
User_88_Blue=238
#EEEEEE white
User_89_Red=204
User_89_Green=204
User_89_Blue=204
#EEEEEE white
User_90_Red=168
User_90_Green=255
User_90_Blue=96
#EEEEEE white
User_91_Red=204
User_91_Green=204
User_91_Blue=204
#FF7F00 orange, DEFAULT
User_92_Red=255
User_92_Green=255
User_92_Blue=182
#EEEEEE white
User_93_Red=204
User_93_Green=204
User_93_Blue=204
#EEEEEE white
User_94_Red=204
User_94_Green=204
User_94_Blue=204
#EEEEEE white
User_95_Red=204
User_95_Green=204
User_95_Blue=204
#FF6C60 bright red
User_96_Red=255
User_96_Green=108
User_96_Blue=96
#CEFFAB bright green
User_97_Red=206
User_97_Green=255
User_97_Blue=171
#FFFFCB bright yellow
User_98_Red=255
User_98_Green=255
User_98_Blue=203
#FF6C60 bright red
User_99_Red=255
User_99_Green=182
User_99_Blue=176
#C6C5FE bright cyan
User_100_Red=255
User_100_Green=255
User_100_Blue=255
#4E4E4E black
User_101_Red=204
User_101_Green=204
User_101_Blue=204
#4E4E4E black
User_102_Red=78
User_102_Green=78
User_102_Blue=78
#4E4E4E black
User_103_Red=204
User_103_Green=204
User_103_Blue=204
#EEEEEE white
User_104_Red=238
User_104_Green=238
User_104_Blue=238
#EEEEEE white
User_105_Red=238
User_105_Green=238
User_105_Blue=238
#FF6C60 red
User_106_Red=255
User_106_Green=108
User_106_Blue=96
#EEEEEE white
User_107_Red=238
User_107_Green=238
User_107_Blue=238
#EEEEEE white
User_108_Red=238
User_108_Green=238
User_108_Blue=238
#EEEEEE white
User_109_Red=238
User_109_Green=238
User_109_Blue=238
#FFFFB6 yellow
User_110_Red=255
User_110_Green=255
User_110_Blue=182
#FFFFB6 yellow
User_111_Red=255
User_111_Green=255
User_111_Blue=182
#FFFFB6 yellow
User_112_Red=255
User_112_Green=255
User_112_Blue=182
#EEEEEE white
User_113_Red=238
User_113_Green=238
User_113_Blue=238
#EEEEEE white
User_114_Red=238
User_114_Green=238
User_114_Blue=238
#EEEEEE white
User_115_Red=238
User_115_Green=238
User_115_Blue=238
#FF6C60 red
User_116_Red=255
User_116_Green=108
User_116_Blue=96
#96CBFE blue
User_117_Red=150
User_117_Green=203
User_117_Blue=254
#EEEEEE white
User_118_Red=238
User_118_Green=238
User_118_Blue=238
#96CBFE blue
User_119_Red=150
User_119_Green=203
User_119_Blue=254
#EEEEEE white
User_120_Red=238
User_120_Green=238
User_120_Blue=238
#EEEEEE white
User_121_Red=238
User_121_Green=238
User_121_Blue=238
#EEEEEE white
User_122_Red=238
User_122_Green=238
User_122_Blue=238
#EEEEEE white
User_123_Red=238
User_123_Green=238
User_123_Blue=238
#EEEEEE white
User_124_Red=238
User_124_Green=238
User_124_Blue=238
#96CBFE blue
User_125_Red=150
User_125_Green=203
User_125_Blue=254
#FF6C60 red
User_126_Red=255
User_126_Green=108
User_126_Blue=96
#96CBFE blue
User_127_Red=150
User_127_Green=203
User_127_Blue=254
#7C7C7C bright black
User_128_Red=124
User_128_Green=124
User_128_Blue=124
#A8FF60 green
User_129_Red=168
User_129_Green=255
User_129_Blue=96
#FF6C60 red
User_130_Red=255
User_130_Green=108
User_130_Blue=96
#A8FF60 green
User_131_Red=168
User_131_Green=255
User_131_Blue=96
#7C7C7C bright black
User_132_Red=124
User_132_Green=124
User_132_Blue=124
#FFFFFF bright white
User_133_Red=255
User_133_Green=255
User_133_Blue=255
#FFFFB6 yellow
User_134_Red=255
User_134_Green=255
User_134_Blue=182
#FF6C60 red
User_135_Red=255
User_135_Green=108
User_135_Blue=96
#FFFFB6 yellow
User_136_Red=255
User_136_Green=255
User_136_Blue=182
#FFFFFF bright white
User_137_Red=255
User_137_Green=255
User_137_Blue=255
#96CBFE blue
User_138_Red=150
User_138_Green=203
User_138_Blue=254
#C6C5FE cyan
User_139_Red=198
User_139_Green=197
User_139_Blue=254
#A8FF60 green
User_140_Red=168
User_140_Green=255
User_140_Blue=96
#7C7C7C bright black
User_141_Red=124
User_141_Green=124
User_141_Blue=124
#A8FF60 green
User_142_Red=168
User_142_Green=255
User_142_Blue=96
#FFB6B0 bright red
User_143_Red=255
User_143_Green=182
User_143_Blue=176
#96CBFE blue
User_144_Red=150
User_144_Green=203
User_144_Blue=254
#A8FF60 green
User_145_Red=168
User_145_Green=255
User_145_Blue=96
#FF73FD magenta
User_146_Red=255
User_146_Green=115
User_146_Blue=253
#C6C5FE cyan
User_147_Red=198
User_147_Green=197
User_147_Blue=254
#A8FF60 green
User_148_Red=168
User_148_Green=255
User_148_Blue=96
#A8FF60 green
User_149_Red=168
User_149_Green=255
User_149_Blue=96
#7C7C7C bright black
User_150_Red=124
User_150_Green=124
User_150_Blue=124
#FF6C60 red
User_151_Red=255
User_151_Green=108
User_151_Blue=96
#A8FF60 green
User_152_Red=168
User_152_Green=255
User_152_Blue=96
#A8FF60 green
User_153_Red=168
User_153_Green=255
User_153_Blue=96
#EEEEEE white
User_154_Red=238
User_154_Green=238
User_154_Blue=238
#EEEEEE white
User_155_Red=238
User_155_Green=238
User_155_Blue=238
#A8FF60 green
User_156_Red=168
User_156_Green=255
User_156_Blue=96
#96CBFE blue
User_157_Red=150
User_157_Green=203
User_157_Blue=254
[FloatingCombat]
Enabled=1
[Camera]
YPitch=49.000000
[NameplateOptions]
FactionFadeIntensity=0.510000
DrawDistance=200.499985
Size=1.000000
[KeyMaps]
KEYMAPPING_CMD_PUSH_TO_TALK_2=242
KEYMAPPING_CYCLEREPLY_1=0
KEYMAPPING_CYCLENPCTARGETS_1=15
KEYMAPPING_HOT2_1_1=1073741826
KEYMAPPING_AUTORUN_2=241
KEYMAPPING_FORWARD_1=17
KEYMAPPING_BACK_1=31
KEYMAPPING_RIGHT_1=32
KEYMAPPING_LEFT_1=30
KEYMAPPING_RIGHT_2=0
KEYMAPPING_LEFT_2=0
KEYMAPPING_STRAFE_LEFT_2=203
KEYMAPPING_STRAFE_RIGHT_2=205
KEYMAPPING_DUCK_1=44
KEYMAPPING_CENTERVIEW_1=0
KEYMAPPING_PITCHDOWN_1=0
KEYMAPPING_PITCHUP_1=0
KEYMAPPING_WHO_1=0
KEYMAPPING_DISBAND_1=0
KEYMAPPING_CAMP_1=0
KEYMAPPING_SIT_STAND_1=45
KEYMAPPING_HOT2_2_1=1073741827
KEYMAPPING_HOT2_3_1=1073741828
KEYMAPPING_HOT2_4_1=1073741829
KEYMAPPING_HOT2_5_1=0
KEYMAPPING_HOT2_6_1=0
KEYMAPPING_HOT2_7_1=0
KEYMAPPING_HOT2_8_1=0
KEYMAPPING_CMD_TOGGLE_CLAIM_WIN_1=0
KEYMAPPING_CMD_TOGGLE_AURAWND_1=0
KEYMAPPING_CMD_PUSH_TO_TALK_1=0
KEYMAPPING_TETHER_CAMERA_1=0
KEYMAPPING_USER1_CAMERA_1=0
KEYMAPPING_CHASE_CAMERA_1=0
KEYMAPPING_STRAFE_LEFT_1=16
KEYMAPPING_STRAFE_RIGHT_1=18
KEYMAPPING_AUTOPRIM_1=536870928
KEYMAPPING_HOTPAGE1_1_1=0
KEYMAPPING_HOTPAGE1_2_1=0
KEYMAPPING_HOTPAGE1_3_1=0
KEYMAPPING_HOTPAGE1_4_1=0
KEYMAPPING_HOTPAGE1_5_1=0
KEYMAPPING_HOTPAGE1_6_1=0
KEYMAPPING_HOTPAGE1_7_1=0
KEYMAPPING_HOTPAGE1_8_1=0
KEYMAPPING_HOTPAGE1_9_1=0
KEYMAPPING_HOTPAGE1_10_1=0
KEYMAPPING_TOGGLE_BANDOLIER_1=268435504
KEYMAPPING_TOGGLE_BUFFWIN_1=0
KEYMAPPING_OPEN_INV_BAGS_1=0
KEYMAPPING_CLOSE_INV_BAGS_1=0
KEYMAPPING_TARGETNPC_2=0
KEYMAPPING_TOGGLECAM_1=536870979
KEYMAPPING_MOUSELOOK_1=536871000
KEYMAPPING_FULLSCREEN_1=536870980
KEYMAPPING_HOT2_5_2=0
KEYMAPPING_HOT2_9_2=1073741857
KEYMAPPING_HOT1_9_2=33
KEYMAPPING_HOT2_10_2=1073741858
KEYMAPPING_HOT1_10_2=34
KEYMAPPING_HOT2_8_2=0
KEYMAPPING_TOGGLETARGET_1=41
KEYMAPPING_HOT1_8_2=0
KEYMAPPING_HOT1_7_2=0
KEYMAPPING_CMD_CLIPBOARD_PASTE_1=536870959
KEYMAPPING_CMD_TOGGLEVOICEWIN_1=0
KEYMAPPING_CMD_TOGGLE_BLOCKEDBUFFWIN_2=0
KEYMAPPING_HOT1_5_2=0
KEYMAPPING_HOT1_6_2=0
KEYMAPPING_HOTPAGE1_1_2=0
KEYMAPPING_TOGGLE_TARGETWIN_1=0
KEYMAPPING_TOGGLE_SPELLSWIN_1=0
KEYMAPPING_TOGGLE_PARTYWIN_1=0
KEYMAPPING_TOGGLE_PLAYERWIN_1=0
KEYMAPPING_TOGGLE_HOTBOX1WIN_1=0
KEYMAPPING_TOGGLE_HOTBOX4WIN_1=0
KEYMAPPING_HOT2_6_2=0
KEYMAPPING_HOT2_7_2=0
KEYMAPPING_TOGGLE_MAILWIN_1=0
KEYMAPPING_COMBAT_1=0
KEYMAPPING_CAST5_2=0
KEYMAPPING_CAST6_2=536870929
KEYMAPPING_CAST7_2=536870930
KEYMAPPING_CAST8_2=536870956
KEYMAPPING_CAST9_2=536870957
KEYMAPPING_CAST10_2=536870958
KEYMAPPING_TARGETPC_2=240
KEYMAPPING_TOGGLE_INV_BAGS_1=48
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
source_dir="${EQL_GAME_PATH:-${HOME}/Games/EverQuestLegends}"
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
if [[ ! -d "${source_dir}" ]]; then
echo "Error: EverQuest Legends directory not found: ${source_dir}" >&2
echo "Set EQL_GAME_PATH to the game install directory." >&2
exit 1
fi
if [[ ! -f "${source_dir}/eqclient.ini" ]]; then
echo "Error: eqclient.ini not found in ${source_dir}" >&2
exit 1
fi
shopt -s nullglob
player_configs=(
"${source_dir}"/[!_]*_*.ini
)
# The client creates Backup_N copies while saving layouts. They are transient
# snapshots, not the current configuration we track.
configs=("${source_dir}/eqclient.ini")
for config in "${player_configs[@]}"; do
[[ "$(basename -- "${config}")" == *Backup* ]] && continue
configs+=("${config}")
done
# Player settings use Character_Server.ini, Character_Server_Class.ini, and
# UI_Character_Server_Class.ini names. Launcher INIs do not match this shape.
for config in "${configs[@]}"; do
destination="${script_dir}/$(basename -- "${config}")"
cp -- "${config}" "${destination}"
chmod 0644 "${destination}"
echo "Updated eql/$(basename -- "${config}")"
done
echo "Captured ${#configs[@]} config file(s) from ${source_dir}"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
destination_dir="${EQL_GAME_PATH:-${HOME}/Games/EverQuestLegends}"
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
configs=(
"eqclient.ini"
"Pleb_oggok_LO1.ini"
"UI_Pleb_oggok_LO1.ini"
)
if [[ ! -d "${destination_dir}" ]]; then
echo "Error: EverQuest Legends directory not found: ${destination_dir}" >&2
echo "Set EQL_GAME_PATH to the game install directory." >&2
exit 1
fi
for config in "${configs[@]}"; do
source="${script_dir}/${config}"
if [[ ! -f "${source}" ]]; then
echo "Error: tracked configuration not found: ${source}" >&2
exit 1
fi
done
for config in "${configs[@]}"; do
source="${script_dir}/${config}"
destination="${destination_dir}/${config}"
cp -- "${source}" "${destination}"
echo "Updated eql/${config}"
done
echo "Pushed ${#configs[@]} config file(s) to ${destination_dir}"
+80
View File
@@ -0,0 +1,80 @@
# AGENTS.md
EverQuest client config and a **minimal classic UI skin** (`UISkin=isuldor`) maintained as unified diffs against `uifiles/default/`.
## Docs
* Install paths and INI layout: [docs/steam-linux-paths.md](docs/steam-linux-paths.md)
* 2026 theme refresh (what broke, what we changed): [docs/ui-theme-2026-refresh.md](docs/ui-theme-2026-refresh.md)
## Patch-set model (advice)
We patch **six** `EQUI_*.xml` files plus two `dark_tile_*.tga` textures — not a full skin fork. That is intentional.
| Do | Avoid |
|----|--------|
| Keep the patch set small (persistent on-screen windows only) | Copying all of `uifiles/default/` into git |
| Patch `uifiles/default/` → install to `uifiles/isuldor/` | Patching `uiresources/` / `AtlasSkin` (HTML/Cohtml panels) |
| Run `bash ui/verify.sh` before every commit and after every game patch | Relying on `patch --fuzz 3` without a dry-run |
| Regenerate diffs with `bash ui/install.sh --update` (CRLF-aware) | Hand-editing diffs or `diff -u` from LF-only XML |
| Budget **manual re-merge** for `EQUI_PlayerWindow` after major client updates | Expecting every patch to apply for years unchanged |
**Highest-churn files:** `EQUI_PlayerWindow.xml` (roles, aggro, meters), then `EQUI_HotButtonWnd.xml` (many slot animations). Everything else has been stable across years.
**Pin the client you verified against:** `ui/.verified-build` (eqgame mtime, patch list). Refresh with `bash ui/verify.sh --record` after a successful in-game check.
## Non-obvious details
* **Two UI trees:** `UISkin``uifiles/<name>/` (XML we patch). `AtlasSkin``uiresources/<name>/` (modern panels; **out of scope** — stay stock Default).
* **Stock XML is CRLF.** `uifiles/default/*.xml` use Windows line endings. LF-only merged files produce giant whole-file diffs; `install.sh --update` normalizes to CRLF when the default file is CRLF.
* **`patch --fuzz 3`** helps when line numbers drift (e.g. `EQUI_Animations`) but can apply the wrong hunk if context is ambiguous. Verify always uses dry-run; treat any future `FAILED` on PlayerWindow as a hand-merge, not a fuzz tweak.
* **Textures:** Register new `.tga` only in `EQUI_Animations.diff`; reference by filename elsewhere. `install.sh` copies `ui/*.tga` into `uifiles/isuldor/`.
* **Theme vs layout:** XML = chrome/templates. Window positions and fade % live in `UI_<Char>_<Server>_<Class>.ini` (`UISkin`, fades). Chat colors in `eqclient.ini` `[TextColors]`. Client updates can stomp INIs — keep canonical copies in this repo.
* **`EQType` on labels:** The player-window theme uses shadow labels and specific EQTypes (e.g. HP `17`, mana `124`) so text overlays gauges; stock uses different types — do not “fix” to match default without checking in-game.
* **Required stub labels:** `HPPerLabel` / `ManPercLabel` / `FatiguePercLabel` must exist as minimal `<Label>` pieces (ScreenID only) or the client breaks.
* **Install script:** Use `bash ui/install.sh` (shebang can fail on some mounts). `EQ_GAME_PATH` overrides auto-detect (Steam Linux path is tried first).
* **Game path detection:** Looks for `eqgame.exe` in install root (including Proton/Steam Linux `.exe` name).
## Lint / verify (run before commit)
From repo root:
```bash
# 1. Patches apply cleanly against live uifiles/default/
bash ui/verify.sh
# 2. Optional: fail if eqgame.exe changed since ui/.verified-build
bash ui/verify.sh --strict
# 3. After game patch + in-game smoke test — update pin file
bash ui/verify.sh --record
```
**Install + verify together:**
```bash
bash ui/install.sh && bash ui/verify.sh
```
**After editing `uifiles/isuldor/` in-game or by hand:**
```bash
bash ui/install.sh --update # regenerate *.diff (CRLF-safe)
bash ui/verify.sh --record
```
**Manual checks** (no automated linter for EQ UI XML):
* No duplicate `<Pieces>` entries in `EQUI_PlayerWindow` (easy to introduce when merging).
* `grep -c Player_HPLabelShadow` in built XML — should appear twice (element + one Pieces line), not three.
* Diff size sanity: `EQUI_PlayerWindow.diff` should be hundreds of lines, not 1500+ (signals CRLF/LF mismatch).
**In-game smoke test** (after client update): player window (HP/mana/XP/AA, group roles, aggro), hotbars, chat, spell window; inventory/AA should still look like **Default**.
## Workflow summary
```
game patch → bash ui/install.sh → bash ui/verify.sh
→ fix failed hunks (usually EQUI_PlayerWindow) → install.sh --update → verify.sh --record
→ set UISkin=isuldor in UI INI → login smoke test
```
+121
View File
@@ -0,0 +1,121 @@
# Revisiting EverQuest
Whenever I play this game, I invariably change most of the default settings. This document exists to remind of what to do when I will inevitably try to play this game again (and again, and again..). While keeping a copy of eqclient.ini has been sufficient at times, I've noticed the game client tramples much of the config after merging updates.
### Maximized Fullscreen
The game client can be coerced to run in a borderless windowed mode. Window resizing should be disabled first, otherwise the mouse will be offset incorrectly. You can find this setting in game under:
*Options* > *Display* > *Allow window resizing*
Or in eqclient.ini:
```ini
AllowResize=0
```
An alternative way to do this is to leave window resizing enabled, and manually fix the mouse offset. Might be useful if you really want to resize the window to something less than full screen (maybe to run more than one eq client).
```ini
WindowedModeXOffset=2
WindowedModeYOffset=1
AllowResize=1
```
The window borders can be removed by a third party utility such as this [autohotkey script](http://gaming.stackexchange.com/a/17307/7413). Just make sure to run the script as Admin, and perform the resize after logging into the character. If the resize occurs on the character selection screen, the renderer won't realize and you'll need to reset the resolution to fix screen stretching.
```autohotkey
^!h::
IfWinExist EverQuest
{
WinSet, Style, -0xC00000 ; hide title bar
WinSet, Style, -0x40000 ; hide thickframe/sizebox
WinMove, , , 0, 0, 1920, 1080
}
return
```
### Texture Flickering (aka Z-Fighting)
In-game lighting appears to be [broken by default](https://forums.daybreakgames.com/eq/index.php?threads/technical-question-about-dynamic-lighting.251405/) on modern systems, with no in-game UI option that sufficiently resolves it. The workaround is to use the command: /dynamic off
```ini
ShowDynamicLights=0
```
Shadows will eventually begin mis-rendering all over the place and occasionally just start flickering. Turn off shadows in game!
I like to tweak the default FPS limits.
```ini
MaxFPS=60
MaxBGFPS=60
```
The windowed gamma feature should be off by default now, but just in case:
```ini
WindowedGamma=0
```
### Controls
Mouselook sensitivity can be adjusted under mouse options. The camera mode can be toggled with F9. Roll the mousewheel while in the default first person camera mode to freely adjust it into a reasonably comfortable third-person mode.
Definitely enable "Click Through Self" in General Options. Otherwise you'll have a hard time clicking anything remotely near your character. The target selection isn't quite so advanced.
### Hotkeys
Custom keyboard configuration is in eqclient.ini [KeyMaps] section. Note that on a fresh install, [KeyMaps] won't be there. I tend to unbind keys for infrequently used features that are accessible through the in-game menu.
### Minimal User Interface
A good UI gets out of your way. [My minimal UI](https://i.imgur.com/mcO4oAA.jpg) is available in this repo in diffs. Maintaining my custom UI changes as a patch set makes integrating official updates easier. I like to set the fade transparency level for most windows to 0%. The exceptions are the hotbars, spellbars and chat windows. I've avoided changing UI panels that are not persistently on screen in order to minimize the long-term maintenance.
**Out of scope:** `AtlasSkin` / `uiresources/` (guild, achievements, overseer, and other HTML panels) — only the classic `UISkin` XML layer is themed; modern panels stay stock Default.
Verify patches: `bash ui/verify.sh` (see [AGENTS.md](AGENTS.md)).
If you've installed [bash on Windows](https://docs.microsoft.com/en-us/windows/wsl/install-win10), you could install the UI with the following commands:
```bash
git clone https://github.com/isuldor/eqclient-conf
cd eqclient-conf/ui
./install.sh
```
Copy UI_Player_server.ini into the game directory and try loading the window layout from it under General settings.
### Text colors
Chat colors are stored as separate RGB values in eqclient.ini [TextColors] section. The default color scheme is a relatively dark palette. I've implemented the ir_black theme and it can be found in the relevant section in the included eqclient.ini in this repo. This is an ideal light text on dark background scheme, so it'll only make sense if you have dark chat windows like I do.
I have experimented with [gradient backgrounds for chat windows](/docs/gradient-10.jpg), but the result wasn't great because I could only get 10 levels of transparency from a tga texture ingame.
### (Optionally) Reorganize game data
There's over 4.3 thousand files sitting in the root game directory. If that triggers your OCD (or you hate having to look for the same file over and over again), you can [reorganize everquest](https://www.eqinterface.com/forums/showthread.php?t=21379) using symlinks. This made a little more sense back when I used a slow spinning disk rather than striped ssd storage.
### Extras
Useful information can be found at various third party websites:
* [EQLogParser](https://github.com/kauffman12/EQLogParser) open source alternative to [Gamparse](http://gambosoft.eqresource.com/gamparsegettingstarted.php)
* [Brewall](http://www.eqmaps.info/) has detailed maps
* [EQStats](http://www.eqstats.net/) has a spell and item database
* [Raidloot](http://www.raidloot.com/) item and spell info
* [Lucy](http://lucy.allakhazam.com/) is a spell database
* [Allakhazam](http://eq.allakhazam.com/) records items and quests info
* [Magelo](//eq.magelo.com/) has player profiles
* [Traders Corner](http://www.eqtraders.com/) has tradeskill info
* [Beimeith](http://www.elitegamerslounge.com/home/progress/) is a server-wide leaderboard
Some of the oldest websites about EverQuest are still around. These are rich archives of EQ lore, quests and discussion:
* [Safehouse](https://thesafehouse.org/forums/forum/everquest-wing)
* [Shaman's Crucible](http://www.shamanscrucible.com/forum/)
* [Druid's Grove](http://thedruidsgrove.org/archive/eq/)
* [Paladins of Norrath](https://www.tapatalk.com/groups/paladinsofnorrath/index.php)
* [GU Comics](http://www.gucomics.com/comic/?cdate=20000710)
There is plenty of lore in Norrath created by the developers. But the real story is about the [players](https://www.tapatalk.com/groups/sacredomen/we-are-all-old-now-t6985.html) [within](https://www.tapatalk.com/groups/sacredomen/hi-t6995.html) the [game](https://www.tapatalk.com/groups/sacredomen/tholuxe-paells-original-guild-manifestos-t6991.html), and the communities they created.
* There is a brief [lore](https://www.everquest.com/lore) page on the official website
* [Wikia](http://everquest.wikia.com/wiki/Lore) has a nascent lore article
### Fixing Allakhazam
Ala is an ad-supported website with useful free content. I think they have a subscription membership that goes with Wowhead, which seems like a great idea if you use it frequently. Their advertisements even seem to get through uBlock Origin. Here are some UBO filters that you should totally never use. I'll aim to [contribute to their wiki](http://everquest.allakhazam.com/wiki.html?p=Special%3AUser_contributions&user=Isuldor) rather than be tormented by their advertisements.
```
everquest.allakhazam.com###logo
everquest.allakhazam.com###row-top
everquest.allakhazam.com###col-right
everquest.allakhazam.com##a.pw-button
everquest.allakhazam.com###gdpr-dashboard
everquest.allakhazam.com###gdpr-toggle
everquest.allakhazam.com###zul-bar
everquest.allakhazam.com###horizontal-bg
everquest.allakhazam.com##footer
everquest.allakhazam.com###wrapperDiv
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

+63
View File
@@ -0,0 +1,63 @@
Here are the default UI and settings paths in your Steam install:
**Install root:** `~/.local/share/Steam/steamapps/common/Everquest F2P`
---
## Default UI paths
EverQuest splits UI into two parallel trees, both keyed off skin names in the UI INI:
| Setting | Default value | Path |
|---------|---------------|------|
| `UISkin` | `Default` | `uifiles/default/` |
| `AtlasSkin` | `Default` | `uiresources/default/` |
### `uifiles/default/` — classic UI skin
- **173** `EQUI_*.xml` window/layout files (e.g. `EQUI_PlayerWindow.xml`, `EQUI_HotButtonWnd.xml`)
- Textures: `.tga`, `.dds`
- Subfolders: `loadscreens/`, `overseer/`
### `uiresources/default/` — newer UI resources
- HTML/JS/CSS for modern panels (`index.html`, `index.js`, `main.css`, `cohtml.js`)
- Subfolders: `assets/images/`, `fonts/`, `img/`, `windows/`
### Other UI-related folder
- `uifiles/classic_spell_icons/` — alternate spell icon set only (not a full skin)
Custom skins go under `uifiles/<SkinName>/` and are selected with `UISkin=<SkinName>` in the character UI INI (your repos `ui/install.sh` script targets `uifiles/isuldor/` this way).
---
## Settings paths
All of these live in the **game install root** (not under `~/.config` or similar):
| File / folder | Purpose |
|---------------|---------|
| `eqclient.ini` | Global client settings: video, audio, graphics, `[Options]`, `[TextColors]`, `[KeyMaps]`, etc. |
| `defaults.ini` | Shipped factory defaults for UI window layouts and positions (reset reference) |
| `UI_<Char>_<Server>_<Class>.ini` | Per-character UI state: window positions, fades, and `UISkin` / `AtlasSkin` |
| `<Char>_<Server>_<Class>.ini` | Per-character game settings: hotbars, combat, abilities, etc. |
| `_characters.ini` | Character registry (`Character0=Isuldor,bertox` on your install) |
| `userdata/` | Address book, ignore list, guild notes (`AddressBook.txt`, `IgnoreList.txt`, `GN_*.txt`) |
**LaunchPad / login UI** (separate from in-game UI):
| File | Purpose |
|------|---------|
| `eqlsClient.ini` | Launcher URLs and client config |
| `eqlsUIConfig.ini` | LaunchPad window positions |
| `eqlsPlayerData.ini` | LaunchPad player data |
| `LaunchPad.ini` / `LaunchPad-user.ini` | LaunchPad settings |
---
## Your current character
For `Isuldor` on `bertox`:
- UI layout: `UI_Isuldor_bertox_PAL.ini``UISkin=Default`, `AtlasSkin=Default`
- Character settings: `Isuldor_bertox_PAL.ini`
So right now youre on the stock UI at `uifiles/default/` and `uiresources/default/`, with layout/state stored in those per-character INI files in the install root.
+141
View File
@@ -0,0 +1,141 @@
# Isuldor UI theme refresh (March 2022 → May 2026)
This article records what changed in EverQuests stock UI between our last theme maintenance pass and this refresh, what we did to bring the **isuldor** minimal skin forward, and how to install and maintain it on the current client.
Paths for the local Steam/Linux install are in [steam-linux-paths.md](steam-linux-paths.md).
## What the theme does
The isuldor skin is a small set of patches against `uifiles/default/` (not a full skin replacement). It keeps only the windows that stay on screen most of the time and strips chrome so they fade into a dark layout:
| File | Purpose |
|------|---------|
| `EQUI_Templates.xml` | Adds `WDT_Def2_Minimal` and `WDT_Inner_Null` draw templates (dark tiled chrome, borderless inner panels) |
| `EQUI_Animations.xml` | Registers `dark_tile_64.tga` / `dark_tile_192.tga` and a 1×1 titlebar placeholder animation |
| `EQUI_HotButtonWnd.xml` | Replaces hotbar slot art with dark tiles; smaller 32×32 normal state |
| `EQUI_ChatWindow.xml` | Borderless chat shell; null inner template on input/output |
| `EQUI_CastSpellWnd.xml` | Transparent spell book shell; no close box or outer border |
| `EQUI_PlayerWindow.xml` | Compact player frame: overlaid HP/mana text, inline XP/AA XP bars, transparent shell |
Textures live in `ui/dark_tile_*.tga` and are copied beside the patched XML under `uifiles/isuldor/`.
The theme does **not** patch `uiresources/default/` (`AtlasSkin`). Modern HTML/Cohtml panels (guild, achievements, overseer, etc.) keep the stock look.
## Refresh summary (May 2026)
| Patch | Result against May 2026 `default` |
|-------|-----------------------------------|
| `EQUI_Animations` | Applies cleanly (line offsets shifted; hunks still match) |
| `EQUI_CastSpellWnd` | Applies cleanly |
| `EQUI_ChatWindow` | Applies cleanly |
| `EQUI_HotButtonWnd` | Applies cleanly |
| `EQUI_Templates` | Applies cleanly |
| `EQUI_PlayerWindow` | **Required rewrite** — see below |
`ui/install.sh` was updated to auto-detect the Steam Linux path (`~/.local/share/Steam/steamapps/common/Everquest F2P`) and honor `EQ_GAME_PATH`. The skin was rebuilt under `uifiles/isuldor/` on this machine.
## Why `EQUI_PlayerWindow` broke
The March 2022 patch set was generated against an older `EQUI_PlayerWindow.xml`. Three classes of drift caused `patch` to fail on hunks 57:
### 1. Label layout offsets
Stock UI moved percent labels (`HPPerLabel`, `ManPercLabel`, `FatiguePercLabel`) from `LeftAnchorOffset` **24** to **34**. Our diff still described the old coordinates, so context lines no longer matched.
### 2. New controls after the fatigue block
Daybreak added group-role buttons and aggro readouts **below** the fatigue labels, for example:
- `PW_GroupRoleTank`, `PW_GroupRoleAssist`, `PW_GroupRolePuller`, `PW_GroupRoleMarkNPC`
- `PW_AggroPctPlayerLabel`, `PW_AggroNameSecondaryLabel`, `PW_AggroPctSecondaryLabel`
- Parcel/mail icons and attack-indicator animations
The 2019 theme inserted custom **XP** and **AA XP** gauges in that slot. The refresh keeps those custom gauges but places them **immediately before** the group-role buttons so stock pieces are preserved.
### 3. Removed / relocated elements
- `PW_VoiceVolume` no longer exists; the old diff referenced it as a line anchor near the screen definition.
- `EQType` values for dynamic HP/mana text changed in stock UI (`19`/`20`); the theme uses `17` / `124` with shadow labels and live data from the client.
### 4. Line endings
Stock XML under `uifiles/default/` uses **CRLF**. Regenerating diffs from LF-only edits produces a whole-file diff and breaks `patch`. When updating `EQUI_PlayerWindow.diff`, normalize the built `isuldor` file to CRLF before `diff -u` (see [Maintenance](#maintenance)).
## Stock UI churn since March 2022
On this install, `uifiles/default/` still has **173** `EQUI_*.xml` window files (unchanged count), but **49** of them have filesystem dates after 2022-03-01. Notable areas touched by Daybreak in that window:
- **Player / target / pet** — `EQUI_PlayerWindow.xml`, `EQUI_TargetWindow.xml`, `EQUI_PetInfoWindow.xml`, blocked-buff windows
- **Inventory & loot** — `EQUI_Inventory.xml`, `EQUI_AdvancedLootWnd.xml`, `EQUI_LootWnd.xml`, `EQUI_DragonHoardWnd.xml`, banks, barter, tradeskill depot
- **Progression UI** — `EQUI_AAWindow.xml`, `EQUI_TaskWnd.xml`, `EQUI_TaskOverlayWnd.xml`, achievements
- **Social / commerce** — guild management/bank, overseer, real estate, merchant, purchase flows
- **Options & login** — `EQUI_OptionsWindow.xml`, character list/create, respawn selector
None of those files are in our patch set by design (see README: avoid maintaining rarely visible windows). After a major client patch, spot-check the six patched windows in-game; everything else stays on `Default`.
`uiresources/default/` (selected by `AtlasSkin`) continues to grow independently; expect modern panels to look stock even with `UISkin=isuldor`.
## Install and enable
From the repo:
```bash
cd ui
bash install.sh # or: EQ_GAME_PATH="/path/to/EverQuest" bash install.sh
```
Set the skin on a character in the UI INI in the game install root:
```ini
UISkin=isuldor
AtlasSkin=Default
```
(`UI_Vulch_coirnav.ini` in this repo is an example.) Reload UI layout from that INI under **Options → General** if windows look wrong after the first login.
Fade amounts (0% on most windows, higher on hotbars/chat) remain in the per-character UI INI, not in the XML patches.
## Maintenance
**Rebuild skin from patches**
```bash
bash ui/install.sh
```
**Refresh patches after editing `uifiles/isuldor/` in-game or by hand**
```bash
bash ui/install.sh --update
```
**Regenerate only `EQUI_PlayerWindow.diff` after hand-merging**
```bash
GAME="$HOME/.local/share/Steam/steamapps/common/Everquest F2P"
sed 's/$/\r/' "$GAME/uifiles/isuldor/EQUI_PlayerWindow.xml" > /tmp/pw.crlf.xml
diff -u "$GAME/uifiles/default/EQUI_PlayerWindow.xml" /tmp/pw.crlf.xml \
> ui/EQUI_PlayerWindow.diff
```
**Verify all patches before committing**
```bash
bash ui/verify.sh
bash ui/verify.sh --record # after in-game smoke test; updates ui/.verified-build
```
## Testing checklist
After each client update:
1. Run `bash ui/install.sh` and confirm no `FAILED` hunks.
2. Log in with `UISkin=isuldor` — check player window (HP/mana/XP/AA), hotbars, chat, spell gem window.
3. Open group roles and aggro labels on the player window; confirm they still render and click.
4. Toggle a few rarely used stock windows (inventory, AA) — they should look like **Default**, not broken.
## References
- [steam-linux-paths.md](steam-linux-paths.md) — install root, `UISkin` / `AtlasSkin`, character INI files
- [README.md](../README.md) — general client tuning (fullscreen, lighting, chat colors)
- Screenshot of the minimal layout: [imgur mcO4oAA](https://i.imgur.com/mcO4oAA.jpg) (2019; layout is similar after refresh)
+60 -41
View File
@@ -583,71 +583,90 @@ User_102_Blue=78
User_103_Red=204
User_103_Green=204
User_103_Blue=204
#EEEEEE white
User_104_Red=238
User_104_Green=238
User_104_Blue=238
#EEEEEE white
User_105_Red=238
User_105_Green=238
User_105_Blue=238
#FF6C60 red
User_104_Red=204
User_104_Green=204
User_104_Blue=204
#FF6C60 red
User_105_Red=204
User_105_Green=204
User_105_Blue=204
User_106_Red=255
User_106_Green=106
User_106_Green=108
User_106_Blue=96
User_107_Red=204
User_107_Green=204
User_107_Blue=204
User_108_Red=204
User_108_Green=204
User_108_Blue=204
User_109_Red=204
User_109_Green=204
User_109_Blue=204
#EEEEEE white
User_107_Red=238
User_107_Green=238
User_107_Blue=238
#EEEEEE white
User_108_Red=238
User_108_Green=238
User_108_Blue=238
#EEEEEE white
User_109_Red=238
User_109_Green=238
User_109_Blue=238
#FFFFB6 yellow
User_110_Red=255
User_110_Green=255
User_110_Blue=182
#FFFFB6 yellow
User_111_Red=255
User_111_Green=255
User_111_Blue=182
#FFFFB6 yellow
User_112_Red=255
User_112_Green=255
User_112_Blue=182
User_113_Red=204
User_113_Green=204
User_113_Blue=204
User_114_Red=204
User_114_Green=204
User_114_Blue=204
User_115_Red=204
User_115_Green=204
User_115_Blue=204
#EEEEEE white
User_113_Red=238
User_113_Green=238
User_113_Blue=238
#EEEEEE white
User_114_Red=238
User_114_Green=238
User_114_Blue=238
#EEEEEE white
User_115_Red=238
User_115_Green=238
User_115_Blue=238
#FF6C60 red
User_116_Red=255
User_116_Green=108
User_116_Blue=96
#96CBFE blue
User_117_Red=150
User_117_Green=203
User_117_Blue=254
User_118_Red=204
User_118_Green=204
User_118_Blue=204
#EEEEEE white
User_118_Red=238
User_118_Green=238
User_118_Blue=238
#96CBFE blue
User_119_Red=150
User_119_Green=203
User_119_Blue=254
User_120_Red=204
User_120_Green=204
User_120_Blue=204
#EEEEEE white
User_120_Red=238
User_120_Green=238
User_120_Blue=238
#EEEEEE white
User_121_Red=238
User_121_Green=238
User_121_Blue=238
User_122_Red=204
User_122_Green=204
User_122_Blue=204
User_123_Red=204
User_123_Green=204
User_123_Blue=204
User_124_Red=204
User_124_Green=204
User_124_Blue=204
#EEEEEE white
User_122_Red=238
User_122_Green=238
User_122_Blue=238
#EEEEEE white
User_123_Red=238
User_123_Green=238
User_123_Blue=238
#EEEEEE white
User_124_Red=238
User_124_Green=238
User_124_Blue=238
[VideoMode]
Width=1920
Height=1080
+1
View File
@@ -0,0 +1 @@
/home/pleb/.local/share/Steam/steamapps/common/Everquest F2P/eqclient.ini
+7
View File
@@ -0,0 +1,7 @@
# Last install verified to apply all UI patches. Refresh with: bash ui/verify.sh --record
verified_date=2026-05-26T11:02:00-07:00
eqgame_mtime=2026-05-22 10:52:08.000000000 -0700
EQUI_PlayerWindow_mtime=2022-09-15 08:04:02.000000000 -0700
game_path=/home/pleb/.local/share/Steam/steamapps/common/Everquest F2P
patch_count=6
patches=EQUI_Animations,EQUI_CastSpellWnd,EQUI_ChatWindow,EQUI_HotButtonWnd,EQUI_PlayerWindow,EQUI_Templates
@@ -1,5 +1,5 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_Animations.xml 2018-06-11 17:34:54.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_Animations.xml 2018-08-15 17:47:01.046516300 -0700
--- /mnt/d/EverQuest/uifiles/default/EQUI_Animations.xml 2019-02-25 13:12:11.000000000 -0800
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_Animations.xml 2019-03-22 16:57:36.616215400 -0700
@@ -93,6 +93,18 @@
<CY>256</CY>
</Size>
@@ -19,11 +19,11 @@
<TextureInfo item="wnd_bg_dark_rock.tga">
<Size>
<CX>256</CX>
@@ -6183,6 +6195,20 @@
@@ -6216,6 +6228,20 @@
</Frames>
</Ui2DAnimation>
<!-- Chat Window Border -->
+ <Ui2DAnimation item="A_ChatWindowTitlePlaceholder">
+ <Ui2DAnimation item="A_WindowTitlePlaceholder">
+ <Cycle>true</Cycle>
+ <Frames>
+ <Texture>window_pieces05.tga</Texture>
@@ -40,7 +40,7 @@
<Ui2DAnimation item="A_ChatWindowTitleLeft">
<Cycle>true</Cycle>
<Frames>
@@ -8237,6 +8263,25 @@
@@ -8213,6 +8239,25 @@
</Ui2DAnimation>
<!-- Border for pieces inside windows -->
@@ -1,5 +1,5 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_CastSpellWnd.xml 2018-05-25 12:25:14.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_CastSpellWnd.xml 2018-08-15 14:09:46.474666500 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_CastSpellWnd.xml 2019-03-22 16:48:13.098523100 -0700
@@ -357,13 +357,13 @@
<CY>373</CY>
</Size>
+29
View File
@@ -0,0 +1,29 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_ChatWindow.xml 2019-03-12 13:58:42.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_ChatWindow.xml 2019-03-22 17:11:42.695289600 -0700
@@ -3,7 +3,7 @@
<Schema xmlns="EverQuestData" xmlns:dt="EverQuestDataTypes" />
<Editbox item="CW_ChatInput">
<ScreenID>CW_ChatInput</ScreenID>
- <DrawTemplate>WDT_Inner</DrawTemplate>
+ <DrawTemplate>WDT_Inner_Null</DrawTemplate>
<RelativePosition>true</RelativePosition>
<AutoStretch>true</AutoStretch>
<LeftAnchorOffset>2</LeftAnchorOffset>
@@ -17,7 +17,7 @@
</Editbox>
<STMLbox item="CW_ChatOutput">
<ScreenID>CW_ChatOutput</ScreenID>
- <DrawTemplate>WDT_Inner</DrawTemplate>
+ <DrawTemplate>WDT_Inner_Null</DrawTemplate>
<RelativePosition>true</RelativePosition>
<Style_VScroll>true</Style_VScroll>
<AutoStretch>true</AutoStretch>
@@ -41,7 +41,7 @@
<Style_HScroll>false</Style_HScroll>
<Style_Transparent>true</Style_Transparent>
<DrawTemplate>WDT_Def2</DrawTemplate>
- <Style_Border>true</Style_Border>
+ <Style_Border>false</Style_Border>
<Style_Sizable>false</Style_Sizable>
<Escapable>false</Escapable>
<Pieces>CW_ChatOutput</Pieces>
@@ -1,5 +1,5 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_HotButtonWnd.xml 2018-05-25 12:26:04.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_HotButtonWnd.xml 2018-08-15 17:46:19.405548300 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_HotButtonWnd.xml 2019-03-22 16:48:13.136738400 -0700
@@ -4,17 +4,13 @@
<Ui2DAnimation item="A_HotButton1Normal">
<Cycle>true</Cycle>
@@ -1,5 +1,5 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_PlayerWindow.xml 2018-04-09 20:54:48.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_PlayerWindow.xml 2018-08-14 16:01:57.964793400 -0700
--- "/home/pleb/.local/share/Steam/steamapps/common/Everquest F2P/uifiles/default/EQUI_PlayerWindow.xml" 2022-09-15 08:04:02.000000000 -0700
+++ /tmp/pw.crlf.xml 2026-05-26 11:02:39.246834677 -0700
@@ -272,8 +272,8 @@
<EndCapRight>A_GaugeEndCapRight</EndCapRight>
</GaugeDrawTemplate>
@@ -22,40 +22,9 @@
<LeftAnchorOffset>2</LeftAnchorOffset>
<RightAnchorToLeft>false</RightAnchorToLeft>
<Style_Tooltip>false</Style_Tooltip>
@@ -363,7 +363,7 @@
<RightAnchorToLeft>false</RightAnchorToLeft>
<LeftAnchorToLeft>false</LeftAnchorToLeft>
</Button>
-
+
<Gauge item="Player_HP">
<ScreenID>PlayerHP</ScreenID>
<TextColor>
@@ -375,10 +375,10 @@
<TextOffsetX>1</TextOffsetX>
<TextOffsetY>-2</TextOffsetY>
<GaugeOffsetY>34</GaugeOffsetY>
- <FillTint>
+ <FillTint>
<R>240</R>
<G>0</G>
- <B>0</B>
+ <B>0</B>
</FillTint>
<EQType>1</EQType>
<GaugeDrawTemplate>
@@ -388,43 +388,48 @@
<EndCapLeft>A_GaugeEndCapLeft</EndCapLeft>
<EndCapRight>A_GaugeEndCapRight</EndCapRight>
</GaugeDrawTemplate>
- <AutoStretch>true</AutoStretch>
+ <AutoStretch>true</AutoStretch>
<TopAnchorOffset>1</TopAnchorOffset>
<BottomAnchorOffset>45</BottomAnchorOffset>
<LeftAnchorOffset>2</LeftAnchorOffset>
@@ -395,36 +395,41 @@
<RightAnchorOffset>1</RightAnchorOffset>
- <RightAnchorToLeft>false</RightAnchorToLeft>
+ <RightAnchorToLeft>false</RightAnchorToLeft>
<RightAnchorToLeft>false</RightAnchorToLeft>
</Gauge>
-
- <Label item="Player_HPLabel">
@@ -98,7 +67,7 @@
<AutoStretch>true</AutoStretch>
- <TopAnchorOffset>35</TopAnchorOffset>
- <BottomAnchorOffset>45</BottomAnchorOffset>
- <LeftAnchorOffset>24</LeftAnchorOffset>
- <LeftAnchorOffset>34</LeftAnchorOffset>
+ <TopAnchorOffset>24</TopAnchorOffset>
+ <BottomAnchorOffset>42</BottomAnchorOffset>
+ <LeftAnchorOffset>2</LeftAnchorOffset>
@@ -155,7 +124,7 @@
<AutoStretch>true</AutoStretch>
- <TopAnchorOffset>45</TopAnchorOffset>
- <BottomAnchorOffset>55</BottomAnchorOffset>
- <LeftAnchorOffset>24</LeftAnchorOffset>
- <LeftAnchorOffset>34</LeftAnchorOffset>
+ <TopAnchorOffset>39</TopAnchorOffset>
+ <BottomAnchorOffset>57</BottomAnchorOffset>
+ <LeftAnchorOffset>2</LeftAnchorOffset>
@@ -294,7 +263,7 @@
<AutoStretch>true</AutoStretch>
- <TopAnchorOffset>55</TopAnchorOffset>
- <BottomAnchorOffset>65</BottomAnchorOffset>
- <LeftAnchorOffset>24</LeftAnchorOffset>
- <LeftAnchorOffset>34</LeftAnchorOffset>
+ <TopAnchorOffset>75</TopAnchorOffset>
+ <BottomAnchorOffset>87</BottomAnchorOffset>
+ <LeftAnchorOffset>2</LeftAnchorOffset>
@@ -314,9 +283,9 @@
<RightAnchorToLeft>false</RightAnchorToLeft>
- <Style_Tooltip>false</Style_Tooltip>
</Label>
<Button item="PW_VoiceVolume">
<ScreenID>PW_VoiceVolume</ScreenID>
@@ -696,12 +812,12 @@
<Button item="PW_GroupRoleTank">
<ScreenID>GroupRoleTank</ScreenID>
@@ -684,12 +800,12 @@
</Size>
<Style_VScroll>false</Style_VScroll>
<Style_HScroll>false</Style_HScroll>
@@ -331,12 +300,12 @@
<Style_Sizable>true</Style_Sizable>
<Style_ClientMovable>true</Style_ClientMovable>
<Escapable>false</Escapable>
@@ -709,13 +825,21 @@
@@ -697,13 +813,21 @@
<Pieces>Pet_HP</Pieces>
<Pieces>Player_Mana</Pieces>
<Pieces>Player_Fatigue</Pieces>
+ <Pieces>Player_Exp</Pieces>
+ <Pieces>Player_AAExp</Pieces>
+ <Pieces>Player_Exp</Pieces>
+ <Pieces>Player_AAExp</Pieces>
<Pieces>Player_CombatTimer</Pieces>
+ <Pieces>Player_HPLabelShadow</Pieces>
<Pieces>Player_HPLabel</Pieces>
@@ -346,14 +315,14 @@
<Pieces>Player_ManaPercLabel</Pieces>
<Pieces>Player_FatigueLabel</Pieces>
<Pieces>Player_FatiguePercLabel</Pieces>
+ <Pieces>Player_ExpLabelShadow</Pieces>
+ <Pieces>Player_ExpLabel</Pieces>
+ <Pieces>Player_AAExpLabelShadow</Pieces>
+ <Pieces>Player_AAExpLabel</Pieces>
+ <Pieces>Player_ExpLabelShadow</Pieces>
+ <Pieces>Player_ExpLabel</Pieces>
+ <Pieces>Player_AAExpLabelShadow</Pieces>
+ <Pieces>Player_AAExpLabel</Pieces>
<Pieces>Player_CombatTimerLabel</Pieces>
<Pieces>PW_CombatStateAnim</Pieces>
<Pieces>PW_NewMailIcon</Pieces>
@@ -735,4 +859,4 @@
@@ -722,4 +846,4 @@
<Pieces>PW_AggroNameSecondaryLabel</Pieces>
<Pieces>PW_AggroPctSecondaryLabel</Pieces>
</Screen>
@@ -1,10 +1,10 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_Templates.xml 2018-08-15 15:11:08.219902200 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_Templates.xml 2018-08-15 17:27:26.694903200 -0700
@@ -638,6 +638,51 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_Templates.xml 2017-09-07 15:13:29.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_Templates.xml 2019-03-22 17:10:59.440953300 -0700
@@ -638,6 +638,64 @@
<OverlapBottom>0</OverlapBottom>
</Titlebar>
</WindowDrawTemplate>
+ <WindowDrawTemplate item="WDT_minimalChat_Def">
+ <WindowDrawTemplate item="WDT_Def2_Minimal">
+ <Background>dark_tile_192.tga</Background>
+ <CloseBox>
+ <Normal>A_CloseBtnNormal</Normal>
@@ -34,6 +34,13 @@
+ <Disabled>A_MaximizeBtnDisabled</Disabled>
+ <PressedFlyby>A_MaximizeBtnPressedFlyby</PressedFlyby>
+ </MaximizeBox>
+ <TileBox>
+ <Normal>A_TileBtnNormal</Normal>
+ <Pressed>A_TileBtnPressed</Pressed>
+ <Flyby>A_TileBtnFlyby</Flyby>
+ <Disabled>A_TileBtnDisabled</Disabled>
+ <PressedFlyby>A_TileBtnPressedFlyby</PressedFlyby>
+ </TileBox>
+ <Border>
+ <Top>A_InnerFramePlaceholder</Top>
+ <Right>A_InnerFramePlaceholder</Right>
@@ -45,10 +52,16 @@
+ <OverlapBottom>0</OverlapBottom>
+ </Border>
+ <Titlebar>
+ <Left>A_ChatWindowTitlePlaceholder</Left>
+ <Right>A_WindowTitlePlaceholder</Right>
+ <Left>A_WindowTitlePlaceholder</Left>
+ <Middle>A_WindowTitlePlaceholder</Middle>
+ <OverlapLeft>0</OverlapLeft>
+ <OverlapTop>0</OverlapTop>
+ <OverlapRight>0</OverlapRight>
+ <OverlapBottom>0</OverlapBottom>
+ </Titlebar>
+ </WindowDrawTemplate>
+ <WindowDrawTemplate item="less_WDT_Inner" />
+ <WindowDrawTemplate item="WDT_Inner_Null" />
<WindowDrawTemplate item="WDT_Inner">
<Background>wnd_bg_light_rock.tga</Background>
<VSBTemplate>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+39
View File
@@ -0,0 +1,39 @@
# Shared EverQuest install root detection. Source from other ui/*.sh scripts.
find_game_path() {
if [[ -n "${EQ_GAME_PATH:-}" && -f "${EQ_GAME_PATH}/eqgame.exe" ]]; then
echo "${EQ_GAME_PATH}"
return 0
fi
local steam_linux="${HOME}/.local/share/Steam/steamapps/common/Everquest F2P"
if [[ -f "${steam_linux}/eqgame.exe" ]]; then
echo "${steam_linux}"
return 0
fi
if [[ -f "/mnt/d/EverQuest/eqgame.exe" ]]; then
echo "/mnt/d/EverQuest"
return 0
fi
local game_location
game_location=$(find /mnt -type f -name eqgame.exe ! -path '*RECYCLE.BIN*' 2>/dev/null | head -n1)
if [[ -n "${game_location}" ]]; then
dirname "${game_location}"
return 0
fi
return 1
}
resolve_game_path() {
local game_path="${EQ_GAME_PATH:-}"
if [[ -z "${game_path}" ]] || [[ ! -f "${game_path}/eqgame.exe" ]]; then
if ! game_path=$(find_game_path); then
echo "Error: EverQuest was not found! Set EQ_GAME_PATH to your install root." >&2
return 1
fi
echo "Using GAME_PATH=${game_path}" >&2
fi
echo "${game_path}"
}
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Rebuild custom UI files using patches
# install.sh — apply patches to uifiles/isuldor/
# install.sh --update — regenerate *.diff from uifiles/isuldor/ (CRLF-normalized)
#
# Set EQ_GAME_PATH to your install root, or rely on auto-detection.
set -euo pipefail
UI_NAME=isuldor
SCRIPT_PATH=$(dirname "${0}")
# shellcheck source=eq-path.sh
source "${SCRIPT_PATH}/eq-path.sh"
GAME_PATH=$(resolve_game_path) || exit 1
UI_FOLDER="${GAME_PATH}/uifiles/${UI_NAME}"
DEFAULT_UI="${GAME_PATH}/uifiles/default"
# Stock uifiles/default/*.xml use CRLF; LF-only isuldor files produce whole-file diffs.
crlf_temp() {
local src=$1
local dest=$2
sed 's/$/\r/' "${src}" > "${dest}"
}
needs_crlf_diff() {
local default_xml=$1
file "${default_xml}" 2>/dev/null | grep -q CRLF
}
# Regenerate patch set from built skin
if [[ "${1:-}" == "-u" || "${1:-}" == "--update" ]]; then
echo "Updating patch set from ${UI_FOLDER}..."
UI_FILES=( $( find "${UI_FOLDER}" -name '*.xml' -exec basename {} .xml \; ))
for UI_FILE in "${UI_FILES[@]}"; do
default_xml="${DEFAULT_UI}/${UI_FILE}.xml"
isuldor_xml="${UI_FOLDER}/${UI_FILE}.xml"
out_diff="${SCRIPT_PATH}/${UI_FILE}.diff"
if needs_crlf_diff "${default_xml}"; then
crlf_temp "${isuldor_xml}" "${out_diff}.isuldor.crlf"
diff -u "${default_xml}" "${out_diff}.isuldor.crlf" > "${out_diff}"
rm -f "${out_diff}.isuldor.crlf"
else
diff -u "${default_xml}" "${isuldor_xml}" > "${out_diff}"
fi
echo " ${UI_FILE}.diff"
done
echo "Run: bash ui/verify.sh --record"
exit 0
fi
mkdir -vp "${UI_FOLDER}"
cp "${SCRIPT_PATH}"/*.tga "${UI_FOLDER}/"
UI_FILES=( $( find "${SCRIPT_PATH}" -name '*.diff' -exec basename {} .diff \; ))
for UI_FILE in "${UI_FILES[@]}"; do
if ! patch --ignore-whitespace --fuzz 3 -o "${UI_FOLDER}/${UI_FILE}.xml" \
"${DEFAULT_UI}/${UI_FILE}.xml" < "${SCRIPT_PATH}/${UI_FILE}.diff"; then
echo "Error: patch failed for ${UI_FILE}" >&2
exit 1
fi
done
echo "Installed ${#UI_FILES[@]} files to ${UI_FOLDER}"
echo "Set UISkin=${UI_NAME} in your character UI INI, then: bash ui/verify.sh --record"
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
# Dry-run all UI patches against uifiles/default. Exit 1 on any failure.
#
# Usage:
# bash ui/verify.sh # verify patches only
# bash ui/verify.sh --strict # also warn if eqgame.exe mtime differs from ui/.verified-build
# bash ui/verify.sh --record # verify, then refresh ui/.verified-build from current install
set -euo pipefail
SCRIPT_PATH=$(dirname "${0}")
# shellcheck source=eq-path.sh
source "${SCRIPT_PATH}/eq-path.sh"
STRICT=0
RECORD=0
for arg in "$@"; do
case "${arg}" in
--strict) STRICT=1 ;;
--record) RECORD=1 ;;
-h|--help)
echo "Usage: verify.sh [--strict] [--record]"
exit 0
;;
*)
echo "Unknown option: ${arg}" >&2
exit 2
;;
esac
done
GAME_PATH=$(resolve_game_path) || exit 1
DEFAULT_UI="${GAME_PATH}/uifiles/default"
PATCH_DIR="${SCRIPT_PATH}"
VERIFIED_FILE="${PATCH_DIR}/.verified-build"
if [[ ! -d "${DEFAULT_UI}" ]]; then
echo "Error: missing ${DEFAULT_UI}" >&2
exit 1
fi
shopt -s nullglob
diffs=( "${PATCH_DIR}"/*.diff )
if [[ ${#diffs[@]} -eq 0 ]]; then
echo "Error: no *.diff files in ${PATCH_DIR}" >&2
exit 1
fi
FAIL=0
for diff_file in "${diffs[@]}"; do
base=$(basename "${diff_file}" .diff)
default_xml="${DEFAULT_UI}/${base}.xml"
if [[ ! -f "${default_xml}" ]]; then
echo "FAIL: ${base} — missing ${default_xml}"
FAIL=1
continue
fi
if patch --dry-run --ignore-whitespace --fuzz 3 -o /dev/null \
"${default_xml}" < "${diff_file}" >/dev/null 2>&1; then
echo "OK: ${base}"
else
echo "FAIL: ${base}"
patch --dry-run --ignore-whitespace --fuzz 3 -o /dev/null \
"${default_xml}" < "${diff_file}" 2>&1 | tail -5
FAIL=1
fi
done
# Required theme textures
for tga in dark_tile_64.tga dark_tile_192.tga; do
if [[ ! -f "${PATCH_DIR}/${tga}" ]]; then
echo "FAIL: missing texture ${PATCH_DIR}/${tga}"
FAIL=1
fi
done
if [[ ${FAIL} -ne 0 ]]; then
echo "Verify failed." >&2
exit 1
fi
echo "All ${#diffs[@]} patches apply cleanly against ${DEFAULT_UI}"
if [[ ${STRICT} -eq 1 && -f "${VERIFIED_FILE}" ]]; then
pinned=$(grep -E '^eqgame_mtime=' "${VERIFIED_FILE}" | cut -d= -f2- | tr -d '\r\n' || true)
current=$(stat -c '%y' "${GAME_PATH}/eqgame.exe" 2>/dev/null | tr -d '\r\n')
if [[ -n "${pinned}" && "${pinned}" != "${current}" ]]; then
echo "WARN: eqgame.exe mtime differs from ui/.verified-build" >&2
echo " pinned: ${pinned}" >&2
echo " current: ${current}" >&2
echo " Re-run: bash ui/verify.sh --record (after confirming patches still OK in-game)" >&2
exit 1
fi
fi
if [[ ${RECORD} -eq 1 ]]; then
eq_mtime=$(stat -c '%y' "${GAME_PATH}/eqgame.exe")
pw_mtime=$(stat -c '%y' "${DEFAULT_UI}/EQUI_PlayerWindow.xml" 2>/dev/null || echo unknown)
patch_list=$(basename -a "${diffs[@]}" .diff | paste -sd, -)
cat > "${VERIFIED_FILE}" <<EOF
# Last install verified to apply all UI patches. Refresh with: bash ui/verify.sh --record
verified_date=$(date -Iseconds)
eqgame_mtime=${eq_mtime}
EQUI_PlayerWindow_mtime=${pw_mtime}
game_path=${GAME_PATH}
patch_count=${#diffs[@]}
patches=${patch_list}
EOF
echo "Recorded ${VERIFIED_FILE}"
fi
exit 0
+17 -107
View File
@@ -1,117 +1,27 @@
# Revisiting EverQuest
Whenever I play this game, I invariably change most of the default settings. This document exists to remind of what to do when I will inevitably try to play this game again (and again, and again..). While keeping a copy of eqclient.ini has been sufficient at times, I've noticed the game client tramples much of the config after merging updates.
# EQ Configuration
### Maximized Fullscreen
The game client can be coerced to run in a borderless windowed mode. Window resizing should be disabled first, otherwise the mouse will be offset incorrectly. You can find this setting in game under:
My configuration notes for EverQuest and some quick and dirty scripts.
*Options* > *Display* > *Allow window resizing*
## Scripts
Or in eqclient.ini:
```ini
AllowResize=0
```
* [Spell Data Explorer](/docs/eql-spell-data.md)
* `EverQuestLegends/pull.sh` — Pull Legends config files into the repo
* `EverQuestLegends/push.sh` — Push tracked Legends config files to the game
* `scripts/eql_spells.py` — Builds and queries a local spell-data index.
* `EverQuestLive/ui/install.sh` — Builds and installs the custom Isuldor UI.
* `EverQuestLive/ui/verify.sh` — Verifies UI patches against game defaults.
* `EverQuestLive/ui/eq-path.sh` — Finds the EverQuest installation directory.
An alternative way to do this is to leave window resizing enabled, and manually fix the mouse offset. Might be useful if you really want to resize the window to something less than full screen (maybe to run more than one eq client).
```ini
WindowedModeXOffset=2
WindowedModeYOffset=1
AllowResize=1
```
## Characters
The window borders can be removed by a third party utility such as this [autohotkey script](http://gaming.stackexchange.com/a/17307/7413). Just make sure to perform the resize after logging into the character. If the resize occurs on the character selection screen, the renderer won't realize and you'll need to reset the resolution to fix screen stretching.
```autohotkey
^!h::
IfWinExist EverQuest
{
WinSet, Style, -0xC00000 ; hide title bar
WinSet, Style, -0x40000 ; hide thickframe/sizebox
WinMove, , , 0, 0, 1920, 1080
}
return
```
EQ Legends
### Texture Flickering (aka Z-Fighting)
In-game lighting appears to be [broken by default](https://forums.daybreakgames.com/eq/index.php?threads/technical-question-about-dynamic-lighting.251405/) on modern systems, with no in-game UI option that sufficiently resolves it. The workaround is to use the command: /dynamic off
```ini
ShowDynamicLights=0
```
Shadows will eventually begin mis-rendering all over the place and occasionally just start flickering. Turn off shadows in game!
* Pleb on Oggok
I like to tweak the default FPS limits.
```ini
MaxFPS=60
MaxBGFPS=60
```
EQ Live
The windowed gamma feature should be off by default now, but just in case:
```ini
WindowedGamma=0
```
* Vulch on Coirnav
### Controls
Mouselook sensitivity can be adjusted under mouse options. The camera mode can be toggled with F9. Roll the mousewheel while in the default first person camera mode to freely adjust it into a reasonably comfortable third-person mode.
## Links
Definitely enable "Click Through Self" in General Options. Otherwise you'll have a hard time clicking anything remotely near your character. The target selection isn't quite so advanced.
### Hotkeys
Custom keyboard configuration is in eqclient.ini [KeyMaps] section. Note that on a fresh install, [KeyMaps] won't be there. I tend to unbind keys for infrequently used features that are accessible through the in-game menu.
### Minimal User Interface
A good UI gets out of your way. [My minimal UI](https://i.imgur.com/mcO4oAA.jpg) is available in this repo in diffs. Maintaining my custom UI changes as a patch set makes integrating official updates easier. I like to set the fade transparency level for most windows to 0%. The exceptions are the hotbars, spellbars and chat windows. I've avoided changing UI panels that are not persistently on screen in order to minimize the long-term maintenance.
Assuming you're using bash on Windows 10 with bash installed, you could install the UI with the following commands:
```bash
git clone https://github.com/isuldor/eqclient-conf
cd eqclient-conf/ui
./install
```
Copy UI_Player_server.ini into the game directory and try loading the window layout from it under General settings.
### Text colors
Chat colors are stored as separate RGB values in eqclient.ini [TextColors] section. The default color scheme is a relatively dark palette. I've implemented the ir_black theme and it can be found in the relevant section in the included eqclient.ini in this repo. This is an ideal light text on dark background scheme, so it'll only make sense if you have dark chat windows like I do.
I have experimented with [gradient backgrounds for chat windows](http://i.imgur.com/LwK7kyO.jpg), but the result wasn't great because I could only get 10 levels of transparency from a tga texture ingame.
### (Optionally) Reorganize game data
There's over 4.3 thousand files sitting in the root game directory. If that triggers your OCD (or you hate having to look for the same file over and over again), you can [reorganize everquest](//www.eqinterface.com/forums/showthread.php?t=21379) using symlinks. This made a little more sense back when I used a slow spinning disk rather than striped ssd storage.
### Extras
Useful information can be found at various third party websites:
* [Gambosoft](http://gambosoft.eqresource.com/gamparsegettingstarted.php) has a log parser (tip: for new characters, reduce the "discard if less than" damage option)
* [Brewall](http://www.eqmaps.info/) has detailed maps
* [EQStats](http://www.eqstats.net/) has a spell and item database
* [Lucy](http://lucy.allakhazam.com/) is a spell database
* [Allakhazam](http://eq.allakhazam.com/) records items and quests info
* [Magelo](//eq.magelo.com/) has player profiles
* [Traders Corner](http://www.eqtraders.com/) has tradeskill info
* [Beimeith](http://www.elitegamerslounge.com/home/progress/) is a server-wide leaderboard
* [Coirnav Progress](https://www.coirnavprogress.com/) has info and a leaderboard for the latest time-locked progression server
Some of the oldest websites about EverQuest are still around. These are rich archives of EQ lore, quests and discussion:
* [Safehouse](https://thesafehouse.org/forums/forum/everquest-wing)
* [Shaman's Crucible](http://www.shamanscrucible.com/forum/)
* [Druid's Grove](http://thedruidsgrove.org/archive/eq/)
* [Paladins of Norrath](https://www.tapatalk.com/groups/paladinsofnorrath/index.php)
* [GU Comics](http://www.gucomics.com/comic/?cdate=20000710)
There is plenty of lore in Norrath created by the developers. But the real story is about the [players](https://www.tapatalk.com/groups/sacredomen/we-are-all-old-now-t6985.html) [within](https://www.tapatalk.com/groups/sacredomen/hi-t6995.html) the [game](https://www.tapatalk.com/groups/sacredomen/tholuxe-paells-original-guild-manifestos-t6991.html), and the communities they created.
* There is a brief [lore](https://www.everquest.com/lore) page on the official website
* [Wikia](http://everquest.wikia.com/wiki/Lore) has a nascent lore article
### Fixing Allakhazam
Ala is an ad-supported website with useful free content. I think they have a subscription membership that goes with Wowhead, which seems like a great idea if you use it frequently. Their advertisements even seem to get through uBlock Origin. Here are some UBO filters that you should totally never use. I'll aim to [contribute to their wiki](http://everquest.allakhazam.com/wiki.html?p=Special%3AUser_contributions&user=Isuldor) rather than be tormented by their advertisements.
```
everquest.allakhazam.com###logo
everquest.allakhazam.com###row-top
everquest.allakhazam.com###col-right
everquest.allakhazam.com##a.pw-button
everquest.allakhazam.com###gdpr-dashboard
everquest.allakhazam.com###gdpr-toggle
everquest.allakhazam.com###zul-bar
everquest.allakhazam.com###horizontal-bg
everquest.allakhazam.com##footer
everquest.allakhazam.com###wrapperDiv
```
* Vulch's [Actually minimal](https://www.eqinterface.com/forums/showthread.php?t=22113) UI
+59
View File
@@ -0,0 +1,59 @@
# EverQuest Legends spell data
`scripts/eql-spells` creates a local SQLite search index from the EQL client
files. The client installation remains the source of truth: neither the raw
files nor the generated database are tracked in this repository.
## Sources and cache
The command reads these files from the EQL game directory:
* `spells_us.txt` — spell ID, name, and client numeric fields.
* `spells_us_str.txt` — caster, target, and fade messages.
* `dbstr_us.txt` — spell descriptions (`type` `6`).
It resolves that directory in this order:
1. `--game-path PATH`
2. `EQL_GAME_PATH`
3. `/home/pleb/Games/EverQuestLegends`
The default generated index is
`$XDG_CACHE_HOME/eqclient-conf/eql-spells.sqlite`, or
`~/.cache/eqclient-conf/eql-spells.sqlite` when `XDG_CACHE_HOME` is unset.
Use `--db PATH` to use a different index, such as one for a test fixture.
Each query compares the client files' paths, sizes, nanosecond modification
times, and `:crc` sidecars (when supplied) with index metadata. It rebuilds
automatically after an EQL update. `build` always forces a rebuild.
## Commands
```bash
# Force an index build.
scripts/eql-spells build
# Exact lookup by numeric spell ID or case-insensitive exact name.
scripts/eql-spells get 4
scripts/eql-spells get 'Summon Waterstone'
# Include the complete original caret-delimited row and its fields.
scripts/eql-spells get 4 --raw
# Name substring lookup and full-text name/description search.
scripts/eql-spells find water --limit 10
scripts/eql-spells search 'underwater breathing'
# Use a different EQL installation or cache location.
scripts/eql-spells --game-path /path/to/EverQuestLegends --db /tmp/spells.sqlite search fire
```
Successful commands emit JSON on stdout. `get` returns `spell`; `find` and
`search` return `results`. All include source fingerprint metadata. Add
`--text` to query commands for compact human-readable output.
The first version intentionally exposes only reliably joined fields: ID, name,
description, cast messages, and (with `--raw`) the complete original row. The
numeric columns in `spells_us.txt` have no embedded schema, so class, level,
mana, and effect filters should be added only after their mapping has been
verified for this particular client build.
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Command-line entry point for the local EQL spell-data index."""
from eql_spells import main
if __name__ == "__main__":
raise SystemExit(main())
+479
View File
@@ -0,0 +1,479 @@
"""Build and query a local EverQuest Legends spell-data index.
The client files are the source of truth. This module only creates a local,
rebuildable SQLite cache and deliberately does not assign meaning to the
undocumented positional fields in spells_us.txt.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sqlite3
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
SCHEMA_VERSION = 2
DEFAULT_GAME_PATH = Path("/home/pleb/Games/EverQuestLegends")
SOURCE_FILENAMES = ("spells_us.txt", "spells_us_str.txt", "dbstr_us.txt")
MESSAGE_COLUMNS = (
"caster_me",
"caster_other",
"casted_me",
"casted_other",
"spell_gone",
)
class EqlSpellsError(Exception):
"""An expected user-facing command error."""
class UsageError(EqlSpellsError):
"""The command line is invalid."""
class DataError(EqlSpellsError):
"""The EQL source files or index are invalid."""
class JSONArgumentParser(argparse.ArgumentParser):
def error(self, message: str) -> None:
raise UsageError(message)
@dataclass(frozen=True)
class SourceInfo:
game_path: Path
fingerprint: dict[str, Any]
def default_cache_path() -> Path:
cache_home = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
return cache_home / "eqclient-conf" / "eql-spells.sqlite"
def resolve_game_path(value: str | None) -> Path:
if value:
return Path(value).expanduser().resolve()
if os.environ.get("EQL_GAME_PATH"):
return Path(os.environ["EQL_GAME_PATH"]).expanduser().resolve()
return DEFAULT_GAME_PATH
def source_info(game_path: Path) -> SourceInfo:
game_path = game_path.resolve()
if not game_path.is_dir():
raise DataError(
f"EverQuest Legends directory not found: {game_path}. "
"Set --game-path or EQL_GAME_PATH."
)
sources: list[dict[str, Any]] = []
for filename in SOURCE_FILENAMES:
path = game_path / filename
if not path.is_file():
raise DataError(f"Required spell data file not found: {path}")
try:
stat = path.stat()
crc_path = Path(f"{path}:crc")
crc = crc_path.read_text(encoding="ascii").strip() if crc_path.is_file() else None
except OSError as error:
raise DataError(f"Unable to inspect {path}: {error}") from error
sources.append(
{
"name": filename,
"path": str(path),
"size": stat.st_size,
"mtime_ns": stat.st_mtime_ns,
"crc": crc,
}
)
fingerprint = {"schema_version": SCHEMA_VERSION, "sources": sources}
return SourceInfo(game_path=game_path, fingerprint=fingerprint)
def connect(path: Path) -> sqlite3.Connection:
connection = sqlite3.connect(path)
connection.row_factory = sqlite3.Row
return connection
def create_schema(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
PRAGMA foreign_keys = ON;
CREATE TABLE metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE spells (
spell_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
name_fold TEXT NOT NULL,
raw_line TEXT NOT NULL,
fields_json TEXT NOT NULL
);
CREATE TABLE spell_messages (
spell_id INTEGER PRIMARY KEY REFERENCES spells(spell_id),
caster_me TEXT NOT NULL DEFAULT '',
caster_other TEXT NOT NULL DEFAULT '',
casted_me TEXT NOT NULL DEFAULT '',
casted_other TEXT NOT NULL DEFAULT '',
spell_gone TEXT NOT NULL DEFAULT ''
);
CREATE TABLE descriptions (
spell_id INTEGER PRIMARY KEY REFERENCES spells(spell_id),
description TEXT NOT NULL
);
CREATE INDEX spells_name_fold_idx ON spells(name_fold);
CREATE VIRTUAL TABLE spell_fts USING fts5(name, description, tokenize = 'porter unicode61');
"""
)
def source_lines(path: Path) -> Iterable[tuple[int, str]]:
try:
with path.open("r", encoding="utf-8", errors="replace", newline="") as handle:
for line_number, line in enumerate(handle, start=1):
yield line_number, line.rstrip("\r\n")
except OSError as error:
raise DataError(f"Unable to read {path}: {error}") from error
def parse_spell_rows(path: Path) -> list[tuple[int, str, str, str, str]]:
rows: list[tuple[int, str, str, str, str]] = []
seen: set[int] = set()
for line_number, line in source_lines(path):
if not line:
continue
fields = line.split("^")
if len(fields) < 2 or not fields[1]:
raise DataError(f"Malformed spell row in {path}:{line_number}")
try:
spell_id = int(fields[0])
except ValueError as error:
raise DataError(f"Malformed spell ID in {path}:{line_number}") from error
if spell_id in seen:
raise DataError(f"Duplicate spell ID {spell_id} in {path}:{line_number}")
seen.add(spell_id)
rows.append((spell_id, fields[1], fields[1].casefold(), line, json.dumps(fields)))
if not rows:
raise DataError(f"No spell rows found in {path}")
return rows
def parse_messages(path: Path) -> list[tuple[int, str, str, str, str, str]]:
rows: list[tuple[int, str, str, str, str, str]] = []
seen: set[int] = set()
for line_number, line in source_lines(path):
if not line or line.startswith("#SPELLINDEX^"):
continue
fields = line.split("^")
if len(fields) < 6:
raise DataError(f"Malformed message row in {path}:{line_number}")
try:
spell_id = int(fields[0])
except ValueError as error:
raise DataError(f"Malformed message spell ID in {path}:{line_number}") from error
if spell_id in seen:
raise DataError(f"Duplicate message spell ID {spell_id} in {path}:{line_number}")
seen.add(spell_id)
rows.append((spell_id, *fields[1:6]))
return rows
def parse_descriptions(path: Path, spell_ids: set[int]) -> list[tuple[int, str]]:
rows: list[tuple[int, str]] = []
seen: set[int] = set()
for line_number, line in source_lines(path):
if not line:
continue
fields = line.split("^")
if len(fields) < 3 or fields[1] != "6":
continue
try:
spell_id = int(fields[0])
except ValueError as error:
raise DataError(f"Malformed description spell ID in {path}:{line_number}") from error
# dbstr_us.txt is a shared string table. Type-6 entries outside the
# client spell table belong to other UI data and are not spell rows.
if spell_id not in spell_ids:
continue
if spell_id in seen:
raise DataError(f"Duplicate spell description for ID {spell_id} in {path}:{line_number}")
seen.add(spell_id)
rows.append((spell_id, fields[2]))
return rows
def validate_joins(spell_ids: set[int], messages: list[tuple[int, str, str, str, str, str]]) -> None:
orphan_messages = [row[0] for row in messages if row[0] not in spell_ids]
if orphan_messages:
raise DataError(f"Message row references unknown spell ID {orphan_messages[0]}")
def build_index(db_path: Path, info: SourceInfo) -> dict[str, int]:
spells_path, messages_path, descriptions_path = (
info.game_path / filename for filename in SOURCE_FILENAMES
)
spells = parse_spell_rows(spells_path)
messages = parse_messages(messages_path)
spell_ids = {row[0] for row in spells}
descriptions = parse_descriptions(descriptions_path, spell_ids)
validate_joins(spell_ids, messages)
db_path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{db_path.name}.", suffix=".tmp", dir=db_path.parent
)
os.close(descriptor)
temporary_path = Path(temporary_name)
try:
connection = connect(temporary_path)
try:
create_schema(connection)
with connection:
connection.executemany(
"INSERT INTO spells (spell_id, name, name_fold, raw_line, fields_json) VALUES (?, ?, ?, ?, ?)",
spells,
)
connection.executemany(
"""INSERT INTO spell_messages
(spell_id, caster_me, caster_other, casted_me, casted_other, spell_gone)
VALUES (?, ?, ?, ?, ?, ?)""",
messages,
)
connection.executemany(
"INSERT INTO descriptions (spell_id, description) VALUES (?, ?)", descriptions,
)
connection.execute(
"""INSERT INTO spell_fts (rowid, name, description)
SELECT spells.spell_id, spells.name, COALESCE(descriptions.description, '')
FROM spells LEFT JOIN descriptions USING (spell_id)"""
)
connection.execute(
"INSERT INTO metadata (key, value) VALUES (?, ?)",
("source", json.dumps({"game_path": str(info.game_path), "fingerprint": info.fingerprint}, sort_keys=True)),
)
count = connection.execute("SELECT count(*) FROM spells").fetchone()[0]
fts_count = connection.execute("SELECT count(*) FROM spell_fts").fetchone()[0]
if count != len(spells) or fts_count != len(spells):
raise DataError("Built index failed row-count validation")
finally:
connection.close()
os.replace(temporary_path, db_path)
except Exception:
temporary_path.unlink(missing_ok=True)
raise
return {"spells": len(spells), "messages": len(messages), "descriptions": len(descriptions)}
def stored_source(db_path: Path) -> dict[str, Any] | None:
if not db_path.is_file():
return None
try:
connection = connect(db_path)
try:
row = connection.execute("SELECT value FROM metadata WHERE key = 'source'").fetchone()
finally:
connection.close()
return json.loads(row[0]) if row else None
except (OSError, sqlite3.Error, json.JSONDecodeError):
return None
def ensure_index(db_path: Path, info: SourceInfo) -> tuple[bool, dict[str, int] | None]:
expected = {"game_path": str(info.game_path), "fingerprint": info.fingerprint}
if stored_source(db_path) == expected:
return False, None
return True, build_index(db_path, info)
def source_metadata(connection: sqlite3.Connection) -> dict[str, Any]:
row = connection.execute("SELECT value FROM metadata WHERE key = 'source'").fetchone()
if row is None:
raise DataError("Index metadata is missing")
return json.loads(row[0])
def spell_from_row(row: sqlite3.Row, include_raw: bool = False) -> dict[str, Any]:
messages = {column: row[column] for column in MESSAGE_COLUMNS}
result: dict[str, Any] = {
"id": row["spell_id"],
"name": row["name"],
"description": row["description"],
"messages": messages,
}
if include_raw:
result["raw"] = {"line": row["raw_line"], "fields": json.loads(row["fields_json"])}
return result
SELECT_SPELL = """
SELECT spells.spell_id, spells.name, spells.raw_line, spells.fields_json,
descriptions.description,
COALESCE(spell_messages.caster_me, '') AS caster_me,
COALESCE(spell_messages.caster_other, '') AS caster_other,
COALESCE(spell_messages.casted_me, '') AS casted_me,
COALESCE(spell_messages.casted_other, '') AS casted_other,
COALESCE(spell_messages.spell_gone, '') AS spell_gone
FROM spells
LEFT JOIN descriptions USING (spell_id)
LEFT JOIN spell_messages USING (spell_id)
"""
def get_spell(connection: sqlite3.Connection, value: str, include_raw: bool) -> dict[str, Any]:
if value.isdecimal():
row = connection.execute(SELECT_SPELL + " WHERE spells.spell_id = ?", (int(value),)).fetchone()
if row is None:
raise DataError(f"No spell found with ID {value}")
return spell_from_row(row, include_raw)
rows = connection.execute(
SELECT_SPELL + " WHERE spells.name_fold = ? ORDER BY spells.spell_id", (value.casefold(),)
).fetchall()
if not rows:
raise DataError(f"No spell found with exact name {value!r}")
if len(rows) > 1:
ids = [row["spell_id"] for row in rows]
raise DataError(f"Exact name {value!r} is ambiguous; matching IDs: {ids}")
return spell_from_row(rows[0], include_raw)
def find_spells(connection: sqlite3.Connection, text: str, limit: int) -> list[dict[str, Any]]:
escaped = text.casefold().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
rows = connection.execute(
"""SELECT spells.spell_id, spells.name, descriptions.description
FROM spells LEFT JOIN descriptions USING (spell_id)
WHERE spells.name_fold LIKE ? ESCAPE '\\'
ORDER BY spells.name_fold, spells.spell_id LIMIT ?""",
(f"%{escaped}%", limit),
).fetchall()
return [{"id": row["spell_id"], "name": row["name"], "description": row["description"]} for row in rows]
def fts_query(text: str) -> str:
terms = re.findall(r"[^\W_]+", text, flags=re.UNICODE)
if not terms:
raise UsageError("search text must contain at least one letter or number")
return " AND ".join(f'"{term}"' for term in terms)
def search_spells(connection: sqlite3.Connection, text: str, limit: int) -> list[dict[str, Any]]:
rows = connection.execute(
"""SELECT spells.spell_id, spells.name, descriptions.description, bm25(spell_fts) AS score
FROM spell_fts
JOIN spells ON spells.spell_id = spell_fts.rowid
LEFT JOIN descriptions USING (spell_id)
WHERE spell_fts MATCH ?
ORDER BY score, spells.name_fold, spells.spell_id LIMIT ?""",
(fts_query(text), limit),
).fetchall()
return [
{"id": row["spell_id"], "name": row["name"], "description": row["description"]}
for row in rows
]
def parse_limit(value: str) -> int:
try:
limit = int(value)
except ValueError as error:
raise argparse.ArgumentTypeError("limit must be an integer") from error
if not 1 <= limit <= 100:
raise argparse.ArgumentTypeError("limit must be between 1 and 100")
return limit
def parser() -> argparse.ArgumentParser:
argument_parser = JSONArgumentParser(prog="eql-spells", description=__doc__)
argument_parser.add_argument("--game-path", help="EverQuest Legends install directory")
argument_parser.add_argument("--db", type=Path, help="SQLite index path (default: user cache)")
subparsers = argument_parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("build", help="force a complete index rebuild")
get_parser = subparsers.add_parser("get", help="look up a spell by ID or exact name")
get_parser.add_argument("value", help="numeric spell ID or exact spell name")
get_parser.add_argument("--raw", action="store_true", help="include original client row and fields")
get_parser.add_argument("--text", action="store_true", help="render compact text instead of JSON")
for command, help_text in (("find", "find spell names"), ("search", "search spell names and descriptions")):
query_parser = subparsers.add_parser(command, help=help_text)
query_parser.add_argument("text")
query_parser.add_argument("--limit", type=parse_limit, default=20)
query_parser.add_argument("--text", dest="as_text", action="store_true", help="render compact text instead of JSON")
return argument_parser
def text_result(command: str, payload: dict[str, Any]) -> str:
if command == "get":
spell = payload["spell"]
lines = [f"{spell['id']}\t{spell['name']}"]
if spell["description"]:
lines.append(spell["description"])
for label, value in spell["messages"].items():
if value:
lines.append(f"{label}: {value}")
return "\n".join(lines)
return "\n".join(
f"{result['id']}\t{result['name']}" + (f"\t{result['description']}" if result["description"] else "")
for result in payload["results"]
)
def emit_json(value: dict[str, Any], stream: Any = sys.stdout) -> None:
print(json.dumps(value, ensure_ascii=False, sort_keys=True), file=stream)
def main(argv: list[str] | None = None) -> int:
try:
arguments = parser().parse_args(argv)
game_path = resolve_game_path(arguments.game_path)
db_path = (arguments.db.expanduser() if arguments.db else default_cache_path()).resolve()
info = source_info(game_path)
if arguments.command == "build":
counts = build_index(db_path, info)
emit_json({"built": counts, "db": str(db_path), "source": {"game_path": str(info.game_path), "fingerprint": info.fingerprint}})
return 0
rebuilt, _ = ensure_index(db_path, info)
connection = connect(db_path)
try:
source = source_metadata(connection)
if arguments.command == "get":
payload = {"source": source, "spell": get_spell(connection, arguments.value, arguments.raw)}
as_text = arguments.text
elif arguments.command == "find":
payload = {"source": source, "results": find_spells(connection, arguments.text, arguments.limit)}
as_text = arguments.as_text
else:
payload = {"source": source, "results": search_spells(connection, arguments.text, arguments.limit)}
as_text = arguments.as_text
if rebuilt:
payload["index_rebuilt"] = True
if as_text:
print(text_result(arguments.command, payload))
else:
emit_json(payload)
finally:
connection.close()
return 0
except UsageError as error:
emit_json({"error": {"code": "usage", "message": str(error)}}, sys.stderr)
return 2
except (DataError, OSError, sqlite3.Error) as error:
emit_json({"error": {"code": "data", "message": str(error)}}, sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
+4
View File
@@ -0,0 +1,4 @@
1^6^Launches a bolt of fire at your target.^0^
2^6^Lets your target breathe underwater for a short time.^0^
3^6^Launches a ball of fire at your target.^0^
1^5^Fire^0^
+3
View File
@@ -0,0 +1,3 @@
1^Fire Bolt^0^alpha
2^Water Breathing^0^beta
3^Fireball^0^gamma
+4
View File
@@ -0,0 +1,4 @@
#SPELLINDEX^CASTERMETXT^CASTEROTHERTXT^CASTEDMETXT^CASTEDOTHERTXT^SPELLGONE^
1^You cast Fire Bolt.^%1 casts Fire Bolt.^Fire burns you.^Fire burns %1.^The fire fades.^
2^You breathe water.^%1 breathes water.^You can breathe water.^%1 can breathe water.^The water magic fades.^
3^^^^^^
+108
View File
@@ -0,0 +1,108 @@
"""End-to-end tests for the dependency-free EQL spell CLI."""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CLI = ROOT / "scripts" / "eql-spells"
FIXTURE = ROOT / "tests" / "fixtures" / "eql-spells"
class EqlSpellsCliTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.work = Path(self.temporary.name)
self.game = self.work / "game"
shutil.copytree(FIXTURE, self.game)
self.db = self.work / "cache" / "spells.sqlite"
def tearDown(self) -> None:
self.temporary.cleanup()
def run_cli(self, *arguments: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(CLI), "--game-path", str(self.game), "--db", str(self.db), *arguments],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
def json_stdout(self, *arguments: str) -> dict:
result = self.run_cli(*arguments)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads(result.stdout)
def test_build_and_exact_lookup_by_id_and_name(self) -> None:
built = self.json_stdout("build")
self.assertEqual(built["built"], {"descriptions": 3, "messages": 3, "spells": 3})
self.assertTrue(self.db.is_file())
by_id = self.json_stdout("get", "1")
self.assertEqual(by_id["spell"]["name"], "Fire Bolt")
self.assertEqual(by_id["spell"]["description"], "Launches a bolt of fire at your target.")
self.assertEqual(by_id["spell"]["messages"]["caster_me"], "You cast Fire Bolt.")
self.assertIn("fingerprint", by_id["source"])
by_name = self.json_stdout("get", "water breathing")
self.assertEqual(by_name["spell"]["id"], 2)
def test_find_search_and_raw_output(self) -> None:
found = self.json_stdout("find", "fire")
self.assertEqual([spell["name"] for spell in found["results"]], ["Fire Bolt", "Fireball"])
searched = self.json_stdout("search", "breathing underwater")
self.assertEqual([spell["id"] for spell in searched["results"]], [2])
raw = self.json_stdout("get", "1", "--raw")
self.assertEqual(raw["spell"]["raw"]["line"], "1^Fire Bolt^0^alpha")
self.assertEqual(raw["spell"]["raw"]["fields"], ["1", "Fire Bolt", "0", "alpha"])
def test_query_automatically_rebuilds_after_source_change(self) -> None:
self.json_stdout("build")
descriptions = self.game / "dbstr_us.txt"
with descriptions.open("a", encoding="utf-8") as handle:
handle.write("2^6^Lets your target breathe underwater indefinitely.^0^\n")
result = self.run_cli("search", "indefinitely")
self.assertEqual(result.returncode, 1)
self.assertIn("Duplicate spell description", json.loads(result.stderr)["error"]["message"])
# Replace the existing description rather than adding a duplicate.
descriptions.write_text(
"1^6^Launches a bolt of fire at your target.^0^\n"
"2^6^Lets your target breathe underwater indefinitely.^0^\n"
"3^6^Launches a ball of fire at your target.^0^\n"
"1^5^Fire^0^\n",
encoding="utf-8",
)
rebuilt = self.json_stdout("search", "indefinitely")
self.assertTrue(rebuilt["index_rebuilt"])
self.assertEqual(rebuilt["results"][0]["id"], 2)
def test_malformed_data_and_invalid_query_return_structured_errors(self) -> None:
(self.game / "spells_us.txt").write_text("bad row\n", encoding="utf-8")
malformed = self.run_cli("build")
self.assertEqual(malformed.returncode, 1)
self.assertEqual(json.loads(malformed.stderr)["error"]["code"], "data")
shutil.copy2(FIXTURE / "spells_us.txt", self.game / "spells_us.txt")
invalid_limit = self.run_cli("find", "fire", "--limit", "101")
self.assertEqual(invalid_limit.returncode, 2)
self.assertEqual(json.loads(invalid_limit.stderr)["error"]["code"], "usage")
missing = self.run_cli("get", "999")
self.assertEqual(missing.returncode, 1)
self.assertEqual(json.loads(missing.stderr)["error"]["code"], "data")
if __name__ == "__main__":
unittest.main()
-43
View File
@@ -1,43 +0,0 @@
--- /mnt/d/EverQuest/uifiles/default/EQUI_ChatWindow.xml 2013-04-12 18:05:57.000000000 -0700
+++ /mnt/d/EverQuest/uifiles/isuldor/EQUI_ChatWindow.xml 2018-08-15 16:30:05.988353700 -0700
@@ -3,7 +3,7 @@
<Schema xmlns="EverQuestData" xmlns:dt="EverQuestDataTypes" />
<Editbox item="CW_ChatInput">
<ScreenID>CWChatInput</ScreenID>
- <DrawTemplate>WDT_Inner</DrawTemplate>
+ <DrawTemplate>less_WDT_Inner</DrawTemplate>
<RelativePosition>true</RelativePosition>
<AutoStretch>true</AutoStretch>
<LeftAnchorOffset>2</LeftAnchorOffset>
@@ -13,11 +13,10 @@
<TopAnchorToTop>false</TopAnchorToTop>
<RightAnchorToLeft>false</RightAnchorToLeft>
<BottomAnchorToTop>false</BottomAnchorToTop>
- <Style_Transparent>true</Style_Transparent>
</Editbox>
<STMLbox item="CW_ChatOutput">
<ScreenID>CWChatOutput</ScreenID>
- <DrawTemplate>WDT_Inner</DrawTemplate>
+ <DrawTemplate>less_WDT_Inner</DrawTemplate>
<RelativePosition>true</RelativePosition>
<Style_VScroll>true</Style_VScroll>
<AutoStretch>true</AutoStretch>
@@ -27,8 +26,6 @@
<BottomAnchorOffset>22</BottomAnchorOffset>
<RightAnchorToLeft>false</RightAnchorToLeft>
<BottomAnchorToTop>false</BottomAnchorToTop>
- <Style_Border>true</Style_Border>
- <Style_Transparent>true</Style_Transparent>
</STMLbox>
<Screen item="ChatWindow">
<RelativePosition>false</RelativePosition>
@@ -42,8 +39,7 @@
</Size>
<Style_VScroll>false</Style_VScroll>
<Style_HScroll>false</Style_HScroll>
- <Style_Transparent>false</Style_Transparent>
- <DrawTemplate>WDT_Def2</DrawTemplate>
+ <DrawTemplate>WDT_minimalChat_Def</DrawTemplate>
<Style_Titlebar>true</Style_Titlebar>
<Style_Closebox>true</Style_Closebox>
<Style_Minimizebox>true</Style_Minimizebox>
-49
View File
@@ -1,49 +0,0 @@
#!/bin/bash
# Rebuild custom UI files using patches
# -u, --update to update the patch set with new changes
GAME_PATH=/mnt/d/EverQuest
UI_NAME=isuldor
if [[ ! -d "${GAME_PATH}/uifiles/default" ]]
then
echo "Notice: Game path not set, searching for EverQuest..."
GAME_LOCATION=$(find /mnt -type f -name eqgame.exe ! -path '*RECYCLE.BIN*' 2>/dev/null | head -n1)
if [[ ${GAME_LOCATION} ]]
then
GAME_PATH=$(dirname ${GAME_LOCATION})
echo "Setting GAME_PATH=${GAME_PATH}"
else
echo "Error: EverQuest was not found!"
exit 1
fi
fi
SCRIPT_PATH=$(dirname ${0})
UI_FOLDER="${GAME_PATH}/uifiles/${UI_NAME}"
# We can update our patch set with changes
if [[ "-u" == ${1} || "--update" == ${1} ]]
then
echo "Updating patch set!"
# rm -v "${SCRIPT_PATH}/EQUI_*.diff"
UI_FILES=( $( find "${UI_FOLDER}" -name '*.xml' -exec basename {} .xml \; ))
for UI_FILE in "${UI_FILES[@]}"
do
diff -u "${GAME_PATH}/uifiles/default/${UI_FILE}.xml" \
"${UI_FOLDER}/${UI_FILE}.xml" > ${SCRIPT_PATH}/${UI_FILE}.diff
done
exit 0
fi
# Build custom UI directory
mkdir -vp "${UI_FOLDER}"
cp ${SCRIPT_PATH}/*.tga "${UI_FOLDER}/"
# Build an array of all diffs to patch, stripping the extension
UI_FILES=( $( find "${SCRIPT_PATH}" -name '*.diff' -exec basename {} .diff \; ))
for UI_FILE in "${UI_FILES[@]}"
do
patch -o "${UI_FOLDER}/${UI_FILE}.xml" "${GAME_PATH}/uifiles/default/${UI_FILE}.xml" < ${SCRIPT_PATH}/${UI_FILE}.diff
done