19 Commits

Author SHA1 Message Date
d9f54a8354 Fix test again 2025-03-27 19:49:57 +01:00
dc8da8ab63 Fix overlapping capture groups 2025-03-27 19:43:06 +01:00
24262a7dca Remove old unused xml files 2025-03-27 19:31:54 +01:00
d77b13c363 Update regression test 2025-03-27 19:31:20 +01:00
a9c60a3698 Neatly align log columns 2025-03-27 19:26:14 +01:00
66bcf21d79 Add goroutine numbers to log lines 2025-03-27 19:19:39 +01:00
e847e5c3ce Make little better logging 2025-03-27 18:53:02 +01:00
9a70c9696e Fix logger 2025-03-27 18:46:28 +01:00
9cea103042 Implement a more better logging solution 2025-03-27 17:53:43 +01:00
81d8259dfc Comment out some xml tests for now
We're not working on it for now
2025-03-27 17:46:43 +01:00
5c5fbac63f Make doublestar a little cleaner 2025-03-27 17:21:13 +01:00
3e818e61c7 Add string.format shortcut to lua 2025-03-27 15:38:07 +01:00
001470ffe4 Fix up regression tests 2025-03-27 15:37:57 +01:00
d88a76c4e2 Fix logging of groups 2025-03-26 22:26:02 +01:00
d3a1f1bd96 Rework regex grouping to avoid changing the same area twice 2025-03-26 22:24:19 +01:00
07a5f3f1a4 Add replaceCommands to avoid index suicide 2025-03-26 21:55:34 +01:00
e2257e082a Add a regression test 2025-03-26 21:55:17 +01:00
b3fce4244d Fix regex for numbers to support negative numbers 2025-03-26 18:30:21 +01:00
bd443067b6 Add support for !num inside of named capture groups 2025-03-26 14:04:39 +01:00
20 changed files with 4738 additions and 1916 deletions

8
.vscode/launch.json vendored
View File

@@ -10,11 +10,11 @@
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}", "program": "${workspaceFolder}",
"cwd": "C:/Users/Administrator/Seafile/Games-Barotrauma",
"args": [ "args": [
"-mode=json", "LightComponent!anyrange=\"(!num)\"",
"$..name", "*4",
"v='pero'", "**/*.xml"
"test.json"
] ]
} }
] ]

View File

@@ -1,651 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Talents>
<Talent identifier="powerarmor">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="5,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.powerarmor">
<Replace tag="[bonusmovement]" value="25" color="gui.green"/>
</Description>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.exosuit" color="gui.orange"/>
</Description>
<AbilityGroupInterval interval="0.9">
<Conditions>
<AbilityConditionHasItem tags="deepdivinglarge" />
</Conditions>
<Abilities>
<CharacterAbilityModifyStat stattype="MovementSpeed" value="0.25" />
</Abilities>
</AbilityGroupInterval>
<AddedRecipe itemidentifier="exosuit"/>
</Talent>
<Talent identifier="foolhardy">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="4,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.lowhealthstatboost">
<Replace tag="[health]" value="50" color="gui.green"/>
</Description>
<Description tag="talentdescription.additionalstattype">
<Replace tag="[amount]" value="20" color="gui.green"/>
<Replace tag="[stattype]" value="stattypenames.physicalresistance" color="gui.orange"/>
</Description>
<AbilityGroupInterval interval="0.9">
<Conditions>
<AbilityConditionAboveVitality invert="true" vitalitypercentage="0.5"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true" multiplyafflictionsbymaxvitality="true">
<Affliction identifier="foolhardy" amount="1.0"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupInterval>
</Talent>
<Talent identifier="berserker">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="3,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.lowhealthstatboost">
<Replace tag="[health]" value="50" color="gui.green"/>
</Description>
<Description tag="talentdescription.additionalstattype">
<Replace tag="[amount]" value="20" color="gui.green"/>
<Replace tag="[stattype]" value="stattypenames.meleedamagebonus" color="gui.orange"/>
</Description>
<AbilityGroupInterval interval="0.9">
<Conditions>
<AbilityConditionAboveVitality invert="true" vitalitypercentage="0.5"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true" multiplyafflictionsbymaxvitality="true">
<Affliction identifier="berserker" amount="1.0"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupInterval>
</Talent>
<Talent identifier="mudraptorwrestler">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="2,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.mudraptorwrestler">
<Replace tag="[amount]" value="50" color="gui.green"/>
</Description>
<Description tag="talentdescription.additionalstattypeself">
<Replace tag="[amount]" value="10" color="gui.green"/>
<Replace tag="[stattype]" value="stattypenames.physicalresistance" color="gui.orange"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnAttack">
<Conditions>
<AbilityConditionAttackData weapontype="NoWeapon,Melee" />
<AbilityConditionCharacter>
<Conditional group="eq mudraptor" />
</AbilityConditionCharacter>
</Conditions>
<Abilities>
<CharacterAbilityModifyAttackData addeddamagemultiplier="0.5"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveResistance resistanceid="damage" multiplier="0.9"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="heavylifting">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="1,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.heavylifting">
<Replace tag="[amount]" value="20" color="gui.green"/>
</Description>
<AbilityGroupInterval interval="0.9">
<Conditions>
<AbilityConditionHoldingItem tags="alienartifact,crate"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyStat stattype="MovementSpeed" value="0.2"/>
</Abilities>
</AbilityGroupInterval>
</Talent>
<Talent identifier="iamthatguy">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="0,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.iamthatguy">
<Replace tag="[amount]" value="20" color="gui.green"/>
</Description>
<Description tag="talentdescription.skillbonus">
<Replace tag="[amount]" value="20" color="gui.green"/>
<Replace tag="[skillname]" value="stattypenames.weaponsskillbonus" color="gui.orange"/>
</Description>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.heavywrench" color="gui.orange"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="WeaponsSkillBonus" value="20"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnAddDamageAffliction">
<Abilities>
<CharacterAbilityModifyAffliction afflictionidentifiers="blunttrauma" addedmultiplier="0.2" />
</Abilities>
</AbilityGroupEffect>
<AddedRecipe itemidentifier="heavywrench"/>
</Talent>
<Talent identifier="robotics">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="3,7" sheetelementsize="128,128"/>
<Description tag="talentdescription.robotics"/>
<Description tag="talentdescription.roboticsreminder">
<Replace tag="[amount]" value="2" color="gui.green"/>
</Description>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.defensebotspawner,entityname.defensebotammobox" color="gui.orange"/>
</Description>
<AddedRecipe itemidentifier="defensebotspawner"/>
<AddedRecipe itemidentifier="defensebotammobox"/>
</Talent>
<Talent identifier="ironstorm">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="7,5" sheetelementsize="128,128"/>
<Description tag="talentdescription.ironstorm">
<Replace tag="[chance]" value="10" color="gui.green"/>
</Description>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.scrapcannon" color="gui.orange"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilitySetMetadataInt identifier="tiermodifieroverride" value="3"/>
</Abilities>
</AbilityGroupEffect>
<AddedRecipe itemidentifier="scrapcannon"/>
</Talent>
<Talent identifier="residualwaste">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="6,5" sheetelementsize="128,128"/>
<Description tag="talentdescription.residualwaste">
<Replace tag="[chance]" value="20" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnItemDeconstructedMaterial">
<Conditions>
<AbilityConditionServerRandom randomChance="0.2"/>
<!-- don't allow duplicating genetic materials, and prevent infinite FPGA circuits -->
<AbilityConditionItem tags="geneticmaterial,unidentifiedgeneticmaterial,circuitboxcomponent,lightcomponent" invert="true"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyValue multiplyvalue="2"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="massproduction">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="6,1" sheetelementsize="128,128"/>
<Description tag="talentdescription.massproduction">
<Replace tag="[chance]" value="40" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnItemFabricatedIngredients">
<Conditions>
<AbilityConditionServerRandom randomChance="0.4" />
</Conditions>
<Abilities>
<CharacterAbilityRemoveRandomIngredient>
<AbilityConditionItem category="Material"/>
</CharacterAbilityRemoveRandomIngredient>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="toolmaintenance">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="5,5" sheetelementsize="128,128"/>
<Description tag="talentdescription.toolmaintenance">
<Replace tag="[amount]" value="1" color="gui.green"/>
</Description>
<!-- Give once when unlocking the talent -->
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGivePermanentStat statidentifier="tool~toolmaintenance" stattype="IncreaseFabricationQuality" value="1" targetallies="true" setvalue="true"/>
</Abilities>
</AbilityGroupEffect>
<!-- Give every 60 seconds for late comers -->
<AbilityGroupInterval interval="60">
<Abilities>
<CharacterAbilityGivePermanentStat statidentifier="tool~toolmaintenance" stattype="IncreaseFabricationQuality" value="1" targetallies="true" setvalue="true"/>
</Abilities>
</AbilityGroupInterval>
</Talent>
<Talent identifier="miner">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="2,3" sheetelementsize="428,428"/>
<Description tag="talentdescription.miner">
<Replace tag="[probability]" value="320" color="gui.green"/>
</Description>
<Description tag="talentdescription.gainoredetachspeed">
<Replace tag="[amount]" value="1600" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="RepairToolDeattachTimeMultiplier" value="1"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnItemDeconstructedMaterial">
<Conditions>
<AbilityConditionServerRandom randomchance="12.8"/>
<AbilityConditionItem tags="ore"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyValue multiplyvalue="2"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="retrofit">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="3,5" sheetelementsize="128,128"/>
<Description tag="talentdescription.retrofit" />
<Description tag="talentdescription.doesnotstack" />
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilitySetMetadataInt identifier="tiermodifiers.increasewallhealth" value="1"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="ironman">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="6,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.ironhelmet,entityname.makeshiftarmor" color="gui.orange"/>
</Description>
<AddedRecipe itemidentifier="ironhelmet"/>
<AddedRecipe itemidentifier="makeshiftarmor"/>
</Talent>
<Talent identifier="oiledmachinery">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="4,5" sheetelementsize="128,128"/>
<Description tag="talentdescription.oiledmachinery">
<Replace tag="[amount]" value="50" color="gui.green"/>
</Description>
<Description tag="talentdescription.doesnotstack" />
<AbilityGroupInterval interval="60">
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="fabricator" stattype="FabricationSpeed" value="1.5" />
<CharacterAbilityGiveItemStatToTags tags="deconstructor" stattype="DeconstructorSpeed" value="1.5" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="fabricator" stattype="FabricationSpeed" value="1.5" />
<CharacterAbilityGiveItemStatToTags tags="deconstructor" stattype="DeconstructorSpeed" value="1.5" />
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="pumpndump">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="1,7" sheetelementsize="128,128"/>
<Description tag="talentdescription.pumpndump">
<Replace tag="[amount]" value="10" color="gui.green"/>
<Replace tag="[stattype]" value="stattypenames.maxflow" color="gui.orange"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<conditions>
<AbilityConditionItem tags="pump"/>
</conditions>
<Abilities>
<CharacterAbilityGiveItemStat stattype="PumpSpeed" value="1.1"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="ballastdenizen">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="7,6" sheetelementsize="128,128"/>
<Description tag="talentdescription.ballastdenizen">
<Replace tag="[amount]" value="50" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="HoldBreathMultiplier" value="0.5"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="engineengineer">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="2,5" sheetelementsize="128,128"/>
<Description tag="talentdescription.engineengineer">
<Replace tag="[amount]" value="2.5" color="gui.green"/>
<Replace tag="[max]" value="5" color="gui.green"/>
<Replace tag="[stattype]" value="stattypenames.maxspeed" color="gui.orange"/>
</Description>
<Description tag="talentdescription.doesnotstack" />
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="1" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.025" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="2" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.05" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="3" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.075" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="4" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.1" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="5" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.125" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="6" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.15" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel levelequals="7" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.175" />
</Abilities>
</AbilityGroupInterval>
<AbilityGroupInterval interval="60">
<Conditions>
<AbilityConditionHasLevel minlevel="8" />
</Conditions>
<Abilities>
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.2" />
</Abilities>
</AbilityGroupInterval>
</Talent>
<Talent identifier="multifunctional">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="6,1" sheetelementsize="128,128"/>
<Description tag="talentdescription.multifunctional">
<Replace tag="[powerincrease]" value="50" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnAttack">
<Conditions>
<AbilityConditionAttackData tags="wrenchitem"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyAttackData addeddamagemultiplier="0.5"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnAttack">
<Conditions>
<AbilityConditionAttackData tags="crowbaritem"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyAttackData addeddamagemultiplier="0.5"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="salvagecrew">
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="0,7" sheetelementsize="128,128"/>
<Description tag="talentdescription.bonusxponmission">
<Replace tag="[xpbonus]" value="30" color="gui.green"/>
<Replace tag="[missiontype]" value="missiontype.salvage" color="gui.orange"/>
</Description>
<Description tag="talentdescription.salvagecrew">
<Replace tag="[swimbonus]" value="50" color="gui.green"/>
<Replace tag="[resistanceamount]" value="10" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnGainMissionExperience">
<Conditions>
<AbilityConditionMission missiontype="Salvage"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyValue multiplyvalue="1.3"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupInterval interval="0.9">
<Conditions>
<AbilityConditionInSubmarine submarinetype="Wreck" />
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="This" disabledeltatime="true">
<Affliction identifier="salvagecrew" amount="1.0"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupInterval>
</Talent>
<Talent identifier="machinemaniac" trackedstat="machinemaniac_counter" trackedmax="100">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="3,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.machinemaniac">
<Replace tag="[bonus]" value="80" color="gui.green"/>
<Replace tag="[amount]" value="3" color="gui.orange"/>
</Description>
<Description tag="talentdescription.machinemaniac.30">
<Replace tag="[requirement]" value="12" color="gui.green"/>
<Replace tag="[amount]" value="10" color="gui.green"/>
<Replace tag="[skill]" value="stattypenames.mechanicalskillbonus" color="gui.orange"/>
<Replace tag="[xpamount]" value="500" color="gui.green"/>
</Description>
<Description tag="talentdescription.machinemaniac.50">
<Replace tag="[requirement]" value="20" color="gui.green"/>
<Replace tag="[level]" value="1" color="gui.green"/>
</Description>
<Description tag="talentdescription.machinemaniac.100">
<Replace tag="[requirement]" value="40" color="gui.green"/>
<Replace tag="[amount]" value="50" color="gui.green"/>
</Description>
<!-- Give the player stats that tracks if the rewards should be given -->
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_30" value="1" maxvalue="1" setvalue="true" />
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_50" value="1" maxvalue="1" setvalue="true" />
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_100" value="1" maxvalue="1" setvalue="true" />
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
</Conditions>
<Abilities>
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_counter" value="1" removeondeath="false" />
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_30" min="1"/>
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_counter" min="12"/>
</Conditions>
<Abilities>
<CharacterAbilityGiveExperience amount="2000"/>
<CharacterAbilityGivePermanentStat stattype="MechanicalSkillBonus" statidentifier="machinemaniac" value="10" setvalue="true" removeondeath="false" />
<CharacterAbilityResetPermanentStat statidentifier="machinemaniac_30" />
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_50" min="1"/>
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_counter" min="20"/>
</Conditions>
<Abilities>
<CharacterAbilityUpgradeSubmarine upgradeprefab="increasemaxpumpflow" upgradecategory="pumps" level="1" />
<CharacterAbilityResetPermanentStat statidentifier="machinemaniac_50" />
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_100" min="1"/>
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_counter" min="40"/>
</Conditions>
<Abilities>
<CharacterAbilityGivePermanentStat stattype="MechanicalRepairSpeed" statidentifier="machinemaniac" value="0.5" setvalue="true" removeondeath="false" />
<CharacterAbilityResetPermanentStat statidentifier="machinemaniac_100" />
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="tinkerer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="4,1" sheetelementsize="128,128"/>
<Description tag="talentdescription.increasemaxrepairmechanical">
<Replace tag="[percentage]" value="40" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="MaxRepairConditionMultiplierMechanical" value="0.4"/>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="modularrepairs">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,1" sheetelementsize="128,128"/>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.repairpack" color="gui.orange"/>
</Description>
<Description tag="talentdescription.freeupgrade">
<Replace tag="[level]" value="1" color="gui.green"/>
<Replace tag="[upgradename]" value="upgradename.decreaselowskillfixduration" color="gui.orange"/>
</Description>
<AddedRecipe itemidentifier="repairpack"/>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityUpgradeSubmarine upgradeprefab="decreaselowskillfixduration" upgradecategory="electricaldevices" level="1" />
<CharacterAbilityUpgradeSubmarine upgradeprefab="decreaselowskillfixduration" upgradecategory="mechanicaldevices" level="1" />
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="hullfixer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="0,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.fixfoamgrenade,entityname.handheldstatusmonitor" color="gui.orange"/>
</Description>
<Description tag="talentdescription.additionalstattype">
<Replace tag="[amount]" value="25" color="gui.green"/>
<Replace tag="[stattype]" value="stattypenames.repairtoolstructurerepairmultiplier" color="gui.orange"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="RepairToolStructureRepairMultiplier" value="0.25"/>
</Abilities>
</AbilityGroupEffect>
<AddedRecipe itemidentifier="fixfoamgrenade"/>
<AddedRecipe itemidentifier="handheldstatusmonitor"/>
</Talent>
<Talent identifier="letitdrain">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="1,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.letitdrain"/>
<Description tag="talentdescription.letitdrainreminder">
<Replace tag="[itemcount]" value="2" color="gui.green"/>
</Description>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.portablepump" color="gui.orange"/>
</Description>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGivePermanentStat statidentifier="portablepump" stattype="MaxAttachableCount" value="2" />
</Abilities>
</AbilityGroupEffect>
<AddedRecipe itemidentifier="portablepump"/>
</Talent>
<Talent identifier="quickfixer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.quickfixer">
<Replace tag="[amount]" value="20" color="gui.green"/>
<Replace tag="[duration]" value="10" color="gui.green"/>
</Description>
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="1"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
<Affliction identifier="quickfixer" amount="10.0"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="scrapsavant">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="6,3" sheetelementsize="128,128"/>
<Description tag="talentdescription.doublescrapoutput" />
<Description tag="talentdescription.findadditionalscrap">
<Replace tag="[probability]" value="20" color="gui.green"/>
</Description>
<AbilityGroupEffect abilityeffecttype="OnItemDeconstructedMaterial">
<Conditions>
<AbilityConditionItem tags="scrap"/>
</Conditions>
<Abilities>
<CharacterAbilityModifyValue multiplyvalue="2"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnOpenItemContainer">
<Conditions>
<AbilityConditionItemInSubmarine submarinetype="Wreck"/>
<AbilityConditionItem tags="container"/>
</Conditions>
<Abilities>
<CharacterAbilitySpawnItemsToContainer randomchance="0.2" oncepercontainer="true">
<StatusEffects>
<StatusEffect type="OnAbility" target="UseTarget" >
<SpawnItem identifiers="scrap" spawnposition="ThisInventory" spawnifcantbecontained="false" />
</StatusEffect>
</StatusEffects>
</CharacterAbilitySpawnItemsToContainer>
</Abilities>
</AbilityGroupEffect>
</Talent>
<Talent identifier="safetyfirst">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="4,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.unlockrecipe">
<Replace tag="[itemname]" value="entityname.safetyharness" color="gui.orange"/>
</Description>
<AddedRecipe itemidentifier="safetyharness"/>
</Talent>
</Talents>

View File

@@ -0,0 +1,27 @@
package main
import (
"modify/logger"
"time"
)
func main() {
// Initialize logger with DEBUG level
logger.Init(logger.LevelDebug)
// Test different log levels
logger.Info("This is an info message")
logger.Debug("This is a debug message")
logger.Warning("This is a warning message")
logger.Error("This is an error message")
logger.Trace("This is a trace message (not visible at DEBUG level)")
// Test with a goroutine
logger.SafeGo(func() {
time.Sleep(10 * time.Millisecond)
logger.Info("Message from goroutine")
})
// Wait for goroutine to complete
time.Sleep(20 * time.Millisecond)
}

445
logger/logger.go Normal file
View File

@@ -0,0 +1,445 @@
package logger
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
// LogLevel defines the severity of log messages
type LogLevel int
const (
// LevelError is for critical errors that should always be displayed
LevelError LogLevel = iota
// LevelWarning is for important warnings
LevelWarning
// LevelInfo is for informational messages
LevelInfo
// LevelDebug is for detailed debugging information
LevelDebug
// LevelTrace is for very detailed tracing information
LevelTrace
)
var levelNames = map[LogLevel]string{
LevelError: "ERROR",
LevelWarning: "WARNING",
LevelInfo: "INFO",
LevelDebug: "DEBUG",
LevelTrace: "TRACE",
}
var levelColors = map[LogLevel]string{
LevelError: "\033[1;31m", // Bold Red
LevelWarning: "\033[1;33m", // Bold Yellow
LevelInfo: "\033[1;32m", // Bold Green
LevelDebug: "\033[1;36m", // Bold Cyan
LevelTrace: "\033[1;35m", // Bold Magenta
}
// ResetColor is the ANSI code to reset text color
const ResetColor = "\033[0m"
// Logger is our custom logger with level support
type Logger struct {
mu sync.Mutex
out io.Writer
currentLevel LogLevel
prefix string
flag int
useColors bool
callerOffset int
defaultFields map[string]interface{}
showGoroutine bool
}
var (
// DefaultLogger is the global logger instance
DefaultLogger *Logger
// defaultLogLevel is the default log level if not specified
defaultLogLevel = LevelInfo
// Global mutex for DefaultLogger initialization
initMutex sync.Mutex
)
// ParseLevel converts a string log level to LogLevel
func ParseLevel(levelStr string) LogLevel {
switch strings.ToUpper(levelStr) {
case "ERROR":
return LevelError
case "WARNING", "WARN":
return LevelWarning
case "INFO":
return LevelInfo
case "DEBUG":
return LevelDebug
case "TRACE":
return LevelTrace
default:
return defaultLogLevel
}
}
// String returns the string representation of the log level
func (l LogLevel) String() string {
if name, ok := levelNames[l]; ok {
return name
}
return fmt.Sprintf("Level(%d)", l)
}
// New creates a new Logger instance
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{
out: out,
currentLevel: defaultLogLevel,
prefix: prefix,
flag: flag,
useColors: true,
callerOffset: 0,
defaultFields: make(map[string]interface{}),
showGoroutine: true,
}
}
// Init initializes the DefaultLogger
func Init(level LogLevel) {
initMutex.Lock()
defer initMutex.Unlock()
if DefaultLogger == nil {
DefaultLogger = New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile)
}
DefaultLogger.SetLevel(level)
}
// SetLevel sets the current log level
func (l *Logger) SetLevel(level LogLevel) {
l.mu.Lock()
defer l.mu.Unlock()
l.currentLevel = level
}
// GetLevel returns the current log level
func (l *Logger) GetLevel() LogLevel {
l.mu.Lock()
defer l.mu.Unlock()
return l.currentLevel
}
// SetCallerOffset sets the caller offset for correct file and line reporting
func (l *Logger) SetCallerOffset(offset int) {
l.mu.Lock()
defer l.mu.Unlock()
l.callerOffset = offset
}
// SetShowGoroutine sets whether to include goroutine ID in log messages
func (l *Logger) SetShowGoroutine(show bool) {
l.mu.Lock()
defer l.mu.Unlock()
l.showGoroutine = show
}
// ShowGoroutine returns whether goroutine ID is included in log messages
func (l *Logger) ShowGoroutine() bool {
l.mu.Lock()
defer l.mu.Unlock()
return l.showGoroutine
}
// WithField adds a field to the logger's context
func (l *Logger) WithField(key string, value interface{}) *Logger {
newLogger := &Logger{
out: l.out,
currentLevel: l.currentLevel,
prefix: l.prefix,
flag: l.flag,
useColors: l.useColors,
callerOffset: l.callerOffset,
defaultFields: make(map[string]interface{}),
showGoroutine: l.showGoroutine,
}
// Copy existing fields
for k, v := range l.defaultFields {
newLogger.defaultFields[k] = v
}
// Add new field
newLogger.defaultFields[key] = value
return newLogger
}
// WithFields adds multiple fields to the logger's context
func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
newLogger := &Logger{
out: l.out,
currentLevel: l.currentLevel,
prefix: l.prefix,
flag: l.flag,
useColors: l.useColors,
callerOffset: l.callerOffset,
defaultFields: make(map[string]interface{}),
showGoroutine: l.showGoroutine,
}
// Copy existing fields
for k, v := range l.defaultFields {
newLogger.defaultFields[k] = v
}
// Add new fields
for k, v := range fields {
newLogger.defaultFields[k] = v
}
return newLogger
}
// GetGoroutineID extracts the goroutine ID from the runtime stack
func GetGoroutineID() string {
buf := make([]byte, 64)
n := runtime.Stack(buf, false)
// Format of first line is "goroutine N [state]:"
// We only need the N part
buf = buf[:n]
idField := bytes.Fields(bytes.Split(buf, []byte{':'})[0])[1]
return string(idField)
}
// formatMessage formats a log message with level, time, file, and line information
func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string {
var msg string
if len(args) > 0 {
msg = fmt.Sprintf(format, args...)
} else {
msg = format
}
// Format default fields if any
var fields string
if len(l.defaultFields) > 0 {
var pairs []string
for k, v := range l.defaultFields {
pairs = append(pairs, fmt.Sprintf("%s=%v", k, v))
}
fields = " " + strings.Join(pairs, " ")
}
var levelColor, resetColor string
if l.useColors {
levelColor = levelColors[level]
resetColor = ResetColor
}
var caller string
if l.flag&log.Lshortfile != 0 || l.flag&log.Llongfile != 0 {
// Find the actual caller by scanning up the stack
// until we find a function outside the logger package
var file string
var line int
var ok bool
// Start at a reasonable depth and scan up to 10 frames
for depth := 4; depth < 15; depth++ {
_, file, line, ok = runtime.Caller(depth)
if !ok {
break
}
// If the caller is not in the logger package, we found our caller
if !strings.Contains(file, "logger/logger.go") {
break
}
}
if !ok {
file = "???"
line = 0
}
if l.flag&log.Lshortfile != 0 {
file = filepath.Base(file)
}
caller = fmt.Sprintf("%-25s ", file+":"+strconv.Itoa(line))
}
// Format the timestamp with fixed width
var timeStr string
if l.flag&(log.Ldate|log.Ltime|log.Lmicroseconds) != 0 {
t := time.Now()
if l.flag&log.Ldate != 0 {
timeStr += fmt.Sprintf("%04d/%02d/%02d ", t.Year(), t.Month(), t.Day())
}
if l.flag&(log.Ltime|log.Lmicroseconds) != 0 {
timeStr += fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second())
if l.flag&log.Lmicroseconds != 0 {
timeStr += fmt.Sprintf(".%06d", t.Nanosecond()/1000)
}
}
timeStr = fmt.Sprintf("%-15s ", timeStr)
}
// Add goroutine ID if enabled, with fixed width
var goroutineStr string
if l.showGoroutine {
goroutineID := GetGoroutineID()
goroutineStr = fmt.Sprintf("[g:%-4s] ", goroutineID)
}
// Create a colored level indicator with both brackets colored
levelStr := fmt.Sprintf("%s[%s]%s", levelColor, levelNames[level], levelColor)
// Add a space after the level and before the reset color
levelColumn := fmt.Sprintf("%s %s", levelStr, resetColor)
return fmt.Sprintf("%s%s%s%s%s%s%s\n",
l.prefix, timeStr, caller, goroutineStr, levelColumn, msg, fields)
}
// log logs a message at the specified level
func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
if level > l.currentLevel {
return
}
l.mu.Lock()
defer l.mu.Unlock()
msg := l.formatMessage(level, format, args...)
fmt.Fprint(l.out, msg)
}
// Error logs an error message
func (l *Logger) Error(format string, args ...interface{}) {
l.log(LevelError, format, args...)
}
// Warning logs a warning message
func (l *Logger) Warning(format string, args ...interface{}) {
l.log(LevelWarning, format, args...)
}
// Info logs an informational message
func (l *Logger) Info(format string, args ...interface{}) {
l.log(LevelInfo, format, args...)
}
// Debug logs a debug message
func (l *Logger) Debug(format string, args ...interface{}) {
l.log(LevelDebug, format, args...)
}
// Trace logs a trace message
func (l *Logger) Trace(format string, args ...interface{}) {
l.log(LevelTrace, format, args...)
}
// Global log functions that use DefaultLogger
// Error logs an error message using the default logger
func Error(format string, args ...interface{}) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
DefaultLogger.Error(format, args...)
}
// Warning logs a warning message using the default logger
func Warning(format string, args ...interface{}) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
DefaultLogger.Warning(format, args...)
}
// Info logs an informational message using the default logger
func Info(format string, args ...interface{}) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
DefaultLogger.Info(format, args...)
}
// Debug logs a debug message using the default logger
func Debug(format string, args ...interface{}) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
DefaultLogger.Debug(format, args...)
}
// Trace logs a trace message using the default logger
func Trace(format string, args ...interface{}) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
DefaultLogger.Trace(format, args...)
}
// LogPanic logs a panic error and its stack trace
func LogPanic(r interface{}) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
stack := make([]byte, 4096)
n := runtime.Stack(stack, false)
DefaultLogger.Error("PANIC: %v\n%s", r, stack[:n])
}
// SetLevel sets the log level for the default logger
func SetLevel(level LogLevel) {
if DefaultLogger == nil {
Init(level)
return
}
DefaultLogger.SetLevel(level)
}
// GetLevel gets the log level for the default logger
func GetLevel() LogLevel {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
return DefaultLogger.GetLevel()
}
// WithField returns a new logger with the field added to the default logger's context
func WithField(key string, value interface{}) *Logger {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
return DefaultLogger.WithField(key, value)
}
// WithFields returns a new logger with the fields added to the default logger's context
func WithFields(fields map[string]interface{}) *Logger {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
return DefaultLogger.WithFields(fields)
}
// SetShowGoroutine enables or disables goroutine ID display in the default logger
func SetShowGoroutine(show bool) {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
DefaultLogger.SetShowGoroutine(show)
}
// ShowGoroutine returns whether goroutine ID is included in default logger's messages
func ShowGoroutine() bool {
if DefaultLogger == nil {
Init(defaultLogLevel)
}
return DefaultLogger.ShowGoroutine()
}

49
logger/panic_handler.go Normal file
View File

@@ -0,0 +1,49 @@
package logger
import (
"fmt"
"runtime/debug"
)
// PanicHandler handles a panic and logs it
func PanicHandler() {
if r := recover(); r != nil {
goroutineID := GetGoroutineID()
stackTrace := debug.Stack()
Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace)
}
}
// SafeGo launches a goroutine with panic recovery
// Usage: logger.SafeGo(func() { ... your code ... })
func SafeGo(f func()) {
go func() {
defer PanicHandler()
f()
}()
}
// SafeGoWithArgs launches a goroutine with panic recovery and passes arguments
// Usage: logger.SafeGoWithArgs(func(arg1, arg2 interface{}) { ... }, "value1", 42)
func SafeGoWithArgs(f func(...interface{}), args ...interface{}) {
go func() {
defer PanicHandler()
f(args...)
}()
}
// SafeExec executes a function with panic recovery
// Useful for code that should not panic
func SafeExec(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
goroutineID := GetGoroutineID()
stackTrace := debug.Stack()
Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace)
err = fmt.Errorf("panic recovered: %v", r)
}
}()
f()
return nil
}

108
main.go
View File

@@ -13,6 +13,7 @@ import (
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
"modify/logger"
"modify/processor" "modify/processor"
) )
@@ -24,20 +25,22 @@ type GlobalStats struct {
} }
var stats GlobalStats var stats GlobalStats
var logger *log.Logger var stdLogger *log.Logger // Legacy logger for compatibility
var ( var (
jsonFlag = flag.Bool("json", false, "Process JSON files") jsonFlag = flag.Bool("json", false, "Process JSON files")
xmlFlag = flag.Bool("xml", false, "Process XML files") xmlFlag = flag.Bool("xml", false, "Process XML files")
gitFlag = flag.Bool("git", false, "Use git to manage files") gitFlag = flag.Bool("git", false, "Use git to manage files")
resetFlag = flag.Bool("reset", false, "Reset files to their original state") resetFlag = flag.Bool("reset", false, "Reset files to their original state")
logLevel = flag.String("loglevel", "INFO", "Set log level: ERROR, WARNING, INFO, DEBUG, TRACE")
repo *git.Repository repo *git.Repository
worktree *git.Worktree worktree *git.Worktree
) )
func init() { func init() {
// Keep standard logger setup for compatibility with legacy code
log.SetFlags(log.Lmicroseconds | log.Lshortfile) log.SetFlags(log.Lmicroseconds | log.Lshortfile)
logger = log.New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile) stdLogger = log.New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile)
stats = GlobalStats{} stats = GlobalStats{}
} }
@@ -66,6 +69,8 @@ func main() {
fmt.Fprintf(os.Stderr, " Use git to manage files\n") fmt.Fprintf(os.Stderr, " Use git to manage files\n")
fmt.Fprintf(os.Stderr, " -reset\n") fmt.Fprintf(os.Stderr, " -reset\n")
fmt.Fprintf(os.Stderr, " Reset files to their original state\n") fmt.Fprintf(os.Stderr, " Reset files to their original state\n")
fmt.Fprintf(os.Stderr, " -loglevel string\n")
fmt.Fprintf(os.Stderr, " Set logging level: ERROR, WARNING, INFO, DEBUG, TRACE (default \"INFO\")\n")
fmt.Fprintf(os.Stderr, " -mode string\n") fmt.Fprintf(os.Stderr, " -mode string\n")
fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n") fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, "\nExamples:\n")
@@ -86,13 +91,19 @@ func main() {
} }
flag.Parse() flag.Parse()
// Initialize logger with the specified log level
level := logger.ParseLevel(*logLevel)
logger.Init(level)
logger.Info("Initializing with log level: %s", level.String())
args := flag.Args() args := flag.Args()
if *resetFlag { if *resetFlag {
*gitFlag = true *gitFlag = true
} }
if len(args) < 3 { if len(args) < 3 {
log.Printf("At least %d arguments are required", 3) logger.Error("At least %d arguments are required", 3)
flag.Usage() flag.Usage()
return return
} }
@@ -109,37 +120,45 @@ func main() {
originalLuaExpr := luaExpr originalLuaExpr := luaExpr
luaExpr = processor.BuildLuaScript(luaExpr) luaExpr = processor.BuildLuaScript(luaExpr)
if originalLuaExpr != luaExpr { if originalLuaExpr != luaExpr {
logger.Printf("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr) logger.Debug("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr)
} }
if *gitFlag { if *gitFlag {
logger.Info("Git integration enabled, setting up git repository")
err := setupGit() err := setupGit()
if err != nil { if err != nil {
logger.Error("Failed to setup git: %v", err)
fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err) fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err)
return return
} }
} }
// Expand file patterns with glob support // Expand file patterns with glob support
logger.Debug("Expanding file patterns: %v", filePatterns)
files, err := expandFilePatterns(filePatterns) files, err := expandFilePatterns(filePatterns)
if err != nil { if err != nil {
logger.Error("Failed to expand file patterns: %v", err)
fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err) fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
return return
} }
if len(files) == 0 { if len(files) == 0 {
logger.Warning("No files found matching the specified patterns")
fmt.Fprintf(os.Stderr, "No files found matching the specified patterns\n") fmt.Fprintf(os.Stderr, "No files found matching the specified patterns\n")
return return
} }
if *gitFlag { if *gitFlag {
logger.Info("Cleaning up git files before processing")
err := cleanupGitFiles(files) err := cleanupGitFiles(files)
if err != nil { if err != nil {
logger.Error("Failed to cleanup git files: %v", err)
fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err) fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err)
return return
} }
} }
if *resetFlag { if *resetFlag {
logger.Info("Files reset to their original state, nothing more to do")
log.Printf("Files reset to their original state, nothing more to do") log.Printf("Files reset to their original state, nothing more to do")
return return
} }
@@ -149,15 +168,15 @@ func main() {
switch { switch {
case *xmlFlag: case *xmlFlag:
proc = &processor.XMLProcessor{} proc = &processor.XMLProcessor{}
logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files", logger.Info("Starting XML modifier with XPath %q, expression %q on %d files",
pattern, luaExpr, len(files)) pattern, luaExpr, len(files))
case *jsonFlag: case *jsonFlag:
proc = &processor.JSONProcessor{} proc = &processor.JSONProcessor{}
logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files", logger.Info("Starting JSON modifier with JSONPath %q, expression %q on %d files",
pattern, luaExpr, len(files)) pattern, luaExpr, len(files))
default: default:
proc = &processor.RegexProcessor{} proc = &processor.RegexProcessor{}
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files", logger.Info("Starting regex modifier with pattern %q, expression %q on %d files",
pattern, luaExpr, len(files)) pattern, luaExpr, len(files))
} }
@@ -165,29 +184,42 @@ func main() {
// Process each file // Process each file
for _, file := range files { for _, file := range files {
wg.Add(1) wg.Add(1)
go func(file string) { logger.SafeGoWithArgs(func(args ...interface{}) {
defer wg.Done() defer wg.Done()
logger.Printf("Processing file: %s", file) fileToProcess := args[0].(string)
logger.Debug("Processing file: %s", fileToProcess)
// It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now // It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now
modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr) modCount, matchCount, err := processor.Process(proc, fileToProcess, pattern, luaExpr)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err) logger.Error("Failed to process file %s: %v", fileToProcess, err)
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileToProcess, err)
stats.FailedFiles++ stats.FailedFiles++
} else { } else {
logger.Printf("Successfully processed file: %s", file) if modCount > 0 {
logger.Info("Successfully processed file %s: %d modifications from %d matches",
fileToProcess, modCount, matchCount)
} else if matchCount > 0 {
logger.Info("Found %d matches in file %s but made no modifications",
matchCount, fileToProcess)
} else {
logger.Debug("No matches found in file: %s", fileToProcess)
}
stats.ProcessedFiles++ stats.ProcessedFiles++
stats.TotalMatches += matchCount stats.TotalMatches += matchCount
stats.TotalModifications += modCount stats.TotalModifications += modCount
} }
}(file) }, file)
} }
wg.Wait() wg.Wait()
// Print summary // Print summary
if stats.TotalModifications == 0 { if stats.TotalModifications == 0 {
logger.Warning("No modifications were made in any files")
fmt.Fprintf(os.Stderr, "No modifications were made in any files\n") fmt.Fprintf(os.Stderr, "No modifications were made in any files\n")
} else { } else {
logger.Info("Operation complete! Modified %d values in %d/%d files",
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
fmt.Printf("Operation complete! Modified %d values in %d/%d files\n", fmt.Printf("Operation complete! Modified %d values in %d/%d files\n",
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles) stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
} }
@@ -198,27 +230,27 @@ func setupGit() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err) return fmt.Errorf("failed to get current working directory: %w", err)
} }
logger.Printf("Current working directory obtained: %s", cwd) logger.Debug("Current working directory obtained: %s", cwd)
logger.Printf("Attempting to open git repository at %s", cwd) logger.Debug("Attempting to open git repository at %s", cwd)
repo, err = git.PlainOpen(cwd) repo, err = git.PlainOpen(cwd)
if err != nil { if err != nil {
logger.Printf("No existing git repository found at %s, attempting to initialize a new git repository.", cwd) logger.Debug("No existing git repository found at %s, attempting to initialize a new git repository.", cwd)
repo, err = git.PlainInit(cwd, false) repo, err = git.PlainInit(cwd, false)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize a new git repository at %s: %w", cwd, err) return fmt.Errorf("failed to initialize a new git repository at %s: %w", cwd, err)
} }
logger.Printf("Successfully initialized a new git repository at %s", cwd) logger.Info("Successfully initialized a new git repository at %s", cwd)
} else { } else {
logger.Printf("Successfully opened existing git repository at %s", cwd) logger.Info("Successfully opened existing git repository at %s", cwd)
} }
logger.Printf("Attempting to obtain worktree for repository at %s", cwd) logger.Debug("Attempting to obtain worktree for repository at %s", cwd)
worktree, err = repo.Worktree() worktree, err = repo.Worktree()
if err != nil { if err != nil {
return fmt.Errorf("failed to obtain worktree for repository at %s: %w", cwd, err) return fmt.Errorf("failed to obtain worktree for repository at %s: %w", cwd, err)
} }
logger.Printf("Successfully obtained worktree for repository at %s", cwd) logger.Debug("Successfully obtained worktree for repository at %s", cwd)
return nil return nil
} }
@@ -226,39 +258,55 @@ func expandFilePatterns(patterns []string) ([]string, error) {
var files []string var files []string
filesMap := make(map[string]bool) filesMap := make(map[string]bool)
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get current working directory: %w", err)
}
logger.Debug("Expanding patterns from directory: %s", cwd)
for _, pattern := range patterns { for _, pattern := range patterns {
matches, _ := doublestar.Glob(os.DirFS("."), pattern) logger.Trace("Processing pattern: %s", pattern)
matches, _ := doublestar.Glob(os.DirFS(cwd), pattern)
logger.Debug("Found %d matches for pattern %s", len(matches), pattern)
for _, m := range matches { for _, m := range matches {
if info, err := os.Stat(m); err == nil && !info.IsDir() && !filesMap[m] { info, err := os.Stat(m)
if err != nil {
logger.Warning("Error getting file info for %s: %v", m, err)
continue
}
if !info.IsDir() && !filesMap[m] {
logger.Trace("Adding file to process list: %s", m)
filesMap[m], files = true, append(files, m) filesMap[m], files = true, append(files, m)
} }
} }
} }
if len(files) > 0 { if len(files) > 0 {
logger.Printf("Found %d files to process", len(files)) logger.Debug("Found %d files to process: %v", len(files), files)
} }
return files, nil return files, nil
} }
func cleanupGitFiles(files []string) error { func cleanupGitFiles(files []string) error {
for _, file := range files { for _, file := range files {
logger.Printf("Checking file: %s", file) logger.Debug("Checking git status for file: %s", file)
status, err := worktree.Status() status, err := worktree.Status()
if err != nil { if err != nil {
logger.Error("Error getting worktree status: %v", err)
fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err) fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err)
return fmt.Errorf("error getting worktree status: %w", err) return fmt.Errorf("error getting worktree status: %w", err)
} }
if status.IsUntracked(file) { if status.IsUntracked(file) {
logger.Printf("Detected untracked file: %s. Attempting to add it to the git index.", file) logger.Info("Detected untracked file: %s. Adding to git index.", file)
_, err = worktree.Add(file) _, err = worktree.Add(file)
if err != nil { if err != nil {
logger.Error("Error adding file to git: %v", err)
fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err) fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err)
return fmt.Errorf("error adding file to git: %w", err) return fmt.Errorf("error adding file to git: %w", err)
} }
filename := filepath.Base(file) filename := filepath.Base(file)
logger.Printf("File %s added successfully. Now committing it with message: 'Track %s'", filename, filename) logger.Info("File %s added successfully. Committing with message: 'Track %s'", filename, filename)
_, err = worktree.Commit("Track "+filename, &git.CommitOptions{ _, err = worktree.Commit("Track "+filename, &git.CommitOptions{
Author: &object.Signature{ Author: &object.Signature{
Name: "Big Chef", Name: "Big Chef",
@@ -267,22 +315,24 @@ func cleanupGitFiles(files []string) error {
}, },
}) })
if err != nil { if err != nil {
logger.Error("Error committing file: %v", err)
fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err) fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err)
return fmt.Errorf("error committing file: %w", err) return fmt.Errorf("error committing file: %w", err)
} }
logger.Printf("Successfully committed file: %s with message: 'Track %s'", filename, filename) logger.Info("Successfully committed file: %s", filename)
} else { } else {
logger.Printf("File %s is already tracked. Restoring it to the working tree.", file) logger.Info("File %s is already tracked. Restoring it to the working tree.", file)
err := worktree.Restore(&git.RestoreOptions{ err := worktree.Restore(&git.RestoreOptions{
Files: []string{file}, Files: []string{file},
Staged: true, Staged: true,
Worktree: true, Worktree: true,
}) })
if err != nil { if err != nil {
logger.Error("Error restoring file: %v", err)
fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err) fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err)
return fmt.Errorf("error restoring file: %w", err) return fmt.Errorf("error restoring file: %w", err)
} }
logger.Printf("File %s restored successfully.", file) logger.Info("File %s restored successfully", file)
} }
} }
return nil return nil

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "modify/logger"
"modify/processor/jsonpath" "modify/processor/jsonpath"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
@@ -14,80 +14,98 @@ type JSONProcessor struct{}
// ProcessContent implements the Processor interface for JSONProcessor // ProcessContent implements the Processor interface for JSONProcessor
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) { func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
logger.Debug("Processing JSON content with JSONPath: %s", pattern)
// Parse JSON document // Parse JSON document
logger.Trace("Parsing JSON document")
var jsonData interface{} var jsonData interface{}
err := json.Unmarshal([]byte(content), &jsonData) err := json.Unmarshal([]byte(content), &jsonData)
if err != nil { if err != nil {
logger.Error("Failed to parse JSON: %v", err)
return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err) return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err)
} }
// Find nodes matching the JSONPath pattern // Find nodes matching the JSONPath pattern
logger.Debug("Executing JSONPath query: %s", pattern)
nodes, err := jsonpath.Get(jsonData, pattern) nodes, err := jsonpath.Get(jsonData, pattern)
if err != nil { if err != nil {
logger.Error("Failed to execute JSONPath: %v", err)
return content, 0, 0, fmt.Errorf("error getting nodes: %v", err) return content, 0, 0, fmt.Errorf("error getting nodes: %v", err)
} }
matchCount := len(nodes) matchCount := len(nodes)
logger.Debug("Found %d nodes matching JSONPath", matchCount)
if matchCount == 0 { if matchCount == 0 {
logger.Warning("No nodes matched the JSONPath pattern: %s", pattern)
return content, 0, 0, nil return content, 0, 0, nil
} }
modCount := 0 modCount := 0
for _, node := range nodes { for i, node := range nodes {
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value) logger.Trace("Processing node #%d at path: %s with value: %v", i+1, node.Path, node.Value)
// Initialize Lua // Initialize Lua
L, err := NewLuaState() L, err := NewLuaState()
if err != nil { if err != nil {
logger.Error("Failed to create Lua state: %v", err)
return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err) return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
} }
defer L.Close() defer L.Close()
log.Println("Lua state initialized successfully.") logger.Trace("Lua state initialized successfully")
err = p.ToLua(L, node.Value) err = p.ToLua(L, node.Value)
if err != nil { if err != nil {
logger.Error("Failed to convert value to Lua: %v", err)
return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err) return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err)
} }
log.Printf("Converted node value to Lua: %v", node.Value) logger.Trace("Converted node value to Lua: %v", node.Value)
originalScript := luaExpr originalScript := luaExpr
fullScript := BuildLuaScript(luaExpr) fullScript := BuildLuaScript(luaExpr)
log.Printf("Original script: %q, Full script: %q", originalScript, fullScript) logger.Debug("Original script: %q, Full script: %q", originalScript, fullScript)
// Execute Lua script // Execute Lua script
log.Printf("Executing Lua script: %q", fullScript) logger.Trace("Executing Lua script: %q", fullScript)
if err := L.DoString(fullScript); err != nil { if err := L.DoString(fullScript); err != nil {
logger.Error("Failed to execute Lua script: %v", err)
return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err) return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err)
} }
log.Println("Lua script executed successfully.") logger.Trace("Lua script executed successfully")
// Get modified value // Get modified value
result, err := p.FromLua(L) result, err := p.FromLua(L)
if err != nil { if err != nil {
logger.Error("Failed to get result from Lua: %v", err)
return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err) return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
} }
log.Printf("Retrieved modified value from Lua: %v", result) logger.Trace("Retrieved modified value from Lua: %v", result)
modified := false modified := false
modified = L.GetGlobal("modified").String() == "true" modified = L.GetGlobal("modified").String() == "true"
if !modified { if !modified {
log.Printf("No changes made to node at path: %s", node.Path) logger.Debug("No changes made to node at path: %s", node.Path)
continue continue
} }
// Apply the modification to the JSON data // Apply the modification to the JSON data
logger.Debug("Updating JSON at path: %s with new value: %v", node.Path, result)
err = p.updateJSONValue(jsonData, node.Path, result) err = p.updateJSONValue(jsonData, node.Path, result)
if err != nil { if err != nil {
logger.Error("Failed to update JSON at path %s: %v", node.Path, err)
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err) return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
} }
log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result) logger.Debug("Updated JSON at path: %s successfully", node.Path)
modCount++ modCount++
} }
logger.Info("JSON processing complete: %d modifications from %d matches", modCount, matchCount)
// Convert the modified JSON back to a string with same formatting // Convert the modified JSON back to a string with same formatting
logger.Trace("Marshalling JSON data back to string")
var jsonBytes []byte var jsonBytes []byte
jsonBytes, err = json.MarshalIndent(jsonData, "", " ") jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
if err != nil { if err != nil {
logger.Error("Failed to marshal JSON: %v", err)
return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err) return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
} }
return string(jsonBytes), modCount, matchCount, nil return string(jsonBytes), modCount, matchCount, nil
@@ -95,8 +113,11 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
// updateJSONValue updates a value in the JSON structure based on its JSONPath // updateJSONValue updates a value in the JSON structure based on its JSONPath
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error { func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
logger.Trace("Updating JSON value at path: %s", path)
// Special handling for root node // Special handling for root node
if path == "$" { if path == "$" {
logger.Debug("Handling special case for root node update")
// For the root node, we'll copy the value to the jsonData reference // For the root node, we'll copy the value to the jsonData reference
// This is a special case since we can't directly replace the interface{} variable // This is a special case since we can't directly replace the interface{} variable
@@ -108,15 +129,18 @@ func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newVa
if !ok { if !ok {
// If the original wasn't a map, completely replace it with the new map // If the original wasn't a map, completely replace it with the new map
// This is handled by the jsonpath.Set function // This is handled by the jsonpath.Set function
logger.Debug("Root was not a map, replacing entire root")
return jsonpath.Set(jsonData, path, newValue) return jsonpath.Set(jsonData, path, newValue)
} }
// Clear the original map // Clear the original map
logger.Trace("Clearing original root map")
for k := range rootMap { for k := range rootMap {
delete(rootMap, k) delete(rootMap, k)
} }
// Copy all keys from the new map // Copy all keys from the new map
logger.Trace("Copying keys to root map")
for k, v := range rootValue { for k, v := range rootValue {
rootMap[k] = v rootMap[k] = v
} }
@@ -127,22 +151,27 @@ func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newVa
rootArray, ok := jsonData.([]interface{}) rootArray, ok := jsonData.([]interface{})
if !ok { if !ok {
// If the original wasn't an array, use jsonpath.Set // If the original wasn't an array, use jsonpath.Set
logger.Debug("Root was not an array, replacing entire root")
return jsonpath.Set(jsonData, path, newValue) return jsonpath.Set(jsonData, path, newValue)
} }
// Clear and recreate the array // Clear and recreate the array
logger.Trace("Replacing root array")
*&rootArray = rootValue *&rootArray = rootValue
return nil return nil
default: default:
// For other types, use jsonpath.Set // For other types, use jsonpath.Set
logger.Debug("Replacing root with primitive value")
return jsonpath.Set(jsonData, path, newValue) return jsonpath.Set(jsonData, path, newValue)
} }
} }
// For non-root paths, use the regular Set method // For non-root paths, use the regular Set method
logger.Trace("Using regular Set method for non-root path")
err := jsonpath.Set(jsonData, path, newValue) err := jsonpath.Set(jsonData, path, newValue)
if err != nil { if err != nil {
logger.Error("Failed to set JSON value at path %s: %v", path, err)
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err) return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)
} }
return nil return nil

View File

@@ -1,14 +1,17 @@
package processor package processor
import ( import (
"crypto/md5"
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/antchfx/xmlquery" "github.com/antchfx/xmlquery"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"modify/logger"
) )
// Processor defines the interface for all file processors // Processor defines the interface for all file processors
@@ -42,14 +45,18 @@ func NewLuaState() (*lua.LState, error) {
// defer L.Close() // defer L.Close()
// Load math library // Load math library
logger.Debug("Loading Lua math library")
L.Push(L.GetGlobal("require")) L.Push(L.GetGlobal("require"))
L.Push(lua.LString("math")) L.Push(lua.LString("math"))
if err := L.PCall(1, 1, nil); err != nil { if err := L.PCall(1, 1, nil); err != nil {
logger.Error("Failed to load Lua math library: %v", err)
return nil, fmt.Errorf("error loading Lua math library: %v", err) return nil, fmt.Errorf("error loading Lua math library: %v", err)
} }
// Initialize helper functions // Initialize helper functions
logger.Debug("Initializing Lua helper functions")
if err := InitLuaHelpers(L); err != nil { if err := InitLuaHelpers(L); err != nil {
logger.Error("Failed to initialize Lua helper functions: %v", err)
return nil, err return nil, err
} }
@@ -57,37 +64,134 @@ func NewLuaState() (*lua.LState, error) {
} }
func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) { func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) {
logger.Debug("Processing file %q with pattern %q", filename, pattern)
// Read file content // Read file content
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
logger.Error("Failed to get current working directory: %v", err)
return 0, 0, fmt.Errorf("error getting current working directory: %v", err) return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
} }
fullPath := filepath.Join(cwd, filename) fullPath := filepath.Join(cwd, filename)
logger.Trace("Reading file from: %s", fullPath)
stat, err := os.Stat(fullPath)
if err != nil {
logger.Error("Failed to stat file %s: %v", fullPath, err)
return 0, 0, fmt.Errorf("error getting file info: %v", err)
}
logger.Debug("File size: %d bytes, modified: %s", stat.Size(), stat.ModTime().Format(time.RFC3339))
content, err := os.ReadFile(fullPath) content, err := os.ReadFile(fullPath)
if err != nil { if err != nil {
logger.Error("Failed to read file %s: %v", fullPath, err)
return 0, 0, fmt.Errorf("error reading file: %v", err) return 0, 0, fmt.Errorf("error reading file: %v", err)
} }
fileContent := string(content) fileContent := string(content)
logger.Trace("File read successfully: %d bytes, hash: %x", len(content), md5sum(content))
// Detect and log file type
fileType := detectFileType(filename, fileContent)
if fileType != "" {
logger.Debug("Detected file type: %s", fileType)
}
// Process the content // Process the content
logger.Debug("Starting content processing with %s processor", getProcessorType(p))
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr) modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
if err != nil { if err != nil {
logger.Error("Processing error: %v", err)
return 0, 0, err return 0, 0, err
} }
logger.Debug("Processing results: %d matches, %d modifications", matchCount, modCount)
// If we made modifications, save the file // If we made modifications, save the file
if modCount > 0 { if modCount > 0 {
// Calculate changes summary
changePercent := float64(len(modifiedContent)) / float64(len(fileContent)) * 100
logger.Info("File size change: %d → %d bytes (%.1f%%)",
len(fileContent), len(modifiedContent), changePercent)
logger.Debug("Writing modified content to %s", fullPath)
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644) err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
if err != nil { if err != nil {
logger.Error("Failed to write to file %s: %v", fullPath, err)
return 0, 0, fmt.Errorf("error writing file: %v", err) return 0, 0, fmt.Errorf("error writing file: %v", err)
} }
logger.Debug("File written successfully, new hash: %x", md5sum([]byte(modifiedContent)))
} else if matchCount > 0 {
logger.Debug("No content modifications needed for %d matches", matchCount)
} else {
logger.Debug("No matches found in file")
} }
return modCount, matchCount, nil return modCount, matchCount, nil
} }
// Helper function to get a short MD5 hash of content for logging
func md5sum(data []byte) []byte {
h := md5.New()
h.Write(data)
return h.Sum(nil)[:4] // Just use first 4 bytes for brevity
}
// Helper function to detect basic file type from extension and content
func detectFileType(filename string, content string) string {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".xml":
return "XML"
case ".json":
return "JSON"
case ".html", ".htm":
return "HTML"
case ".txt":
return "Text"
case ".go":
return "Go"
case ".js":
return "JavaScript"
case ".py":
return "Python"
case ".java":
return "Java"
case ".c", ".cpp", ".h":
return "C/C++"
default:
// Try content-based detection for common formats
if strings.HasPrefix(strings.TrimSpace(content), "<?xml") {
return "XML"
}
if strings.HasPrefix(strings.TrimSpace(content), "{") ||
strings.HasPrefix(strings.TrimSpace(content), "[") {
return "JSON"
}
if strings.HasPrefix(strings.TrimSpace(content), "<!DOCTYPE html") ||
strings.HasPrefix(strings.TrimSpace(content), "<html") {
return "HTML"
}
return ""
}
}
// Helper function to get processor type name
func getProcessorType(p Processor) string {
switch p.(type) {
case *RegexProcessor:
return "Regex"
case *XMLProcessor:
return "XML"
case *JSONProcessor:
return "JSON"
default:
return "Unknown"
}
}
// ToLua converts a struct or map to a Lua table recursively // ToLua converts a struct or map to a Lua table recursively
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) { func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
switch v := data.(type) { switch v := data.(type) {
@@ -174,25 +278,28 @@ func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
} }
func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) { func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
logger.Trace("Checking if Lua table is an array")
L.SetGlobal("table_to_check", v) L.SetGlobal("table_to_check", v)
// Use our predefined helper function from InitLuaHelpers // Use our predefined helper function from InitLuaHelpers
err := L.DoString(`is_array = isArray(table_to_check)`) err := L.DoString(`is_array = isArray(table_to_check)`)
if err != nil { if err != nil {
logger.Error("Error determining if table is an array: %v", err)
return false, fmt.Errorf("error determining if table is array: %w", err) return false, fmt.Errorf("error determining if table is array: %w", err)
} }
// Check the result of our Lua function // Check the result of our Lua function
isArray := L.GetGlobal("is_array") isArray := L.GetGlobal("is_array")
// LVIsFalse returns true if a given LValue is a nil or false otherwise false. // LVIsFalse returns true if a given LValue is a nil or false otherwise false.
if !lua.LVIsFalse(isArray) { result := !lua.LVIsFalse(isArray)
return true, nil logger.Trace("Lua table is array: %v", result)
} return result, nil
return false, nil
} }
// InitLuaHelpers initializes common Lua helper functions // InitLuaHelpers initializes common Lua helper functions
func InitLuaHelpers(L *lua.LState) error { func InitLuaHelpers(L *lua.LState) error {
logger.Debug("Loading Lua helper functions")
helperScript := ` helperScript := `
-- Custom Lua helpers for math operations -- Custom Lua helpers for math operations
function min(a, b) return math.min(a, b) end function min(a, b) return math.min(a, b) end
@@ -205,6 +312,7 @@ function floor(x) return math.floor(x) end
function ceil(x) return math.ceil(x) end function ceil(x) return math.ceil(x) end
function upper(s) return string.upper(s) end function upper(s) return string.upper(s) end
function lower(s) return string.lower(s) end function lower(s) return string.lower(s) end
function format(s, ...) return string.format(s, ...) end
-- String to number conversion helper -- String to number conversion helper
function num(str) function num(str)
@@ -238,9 +346,11 @@ end
modified = false modified = false
` `
if err := L.DoString(helperScript); err != nil { if err := L.DoString(helperScript); err != nil {
logger.Error("Failed to load Lua helper functions: %v", err)
return fmt.Errorf("error loading helper functions: %v", err) return fmt.Errorf("error loading helper functions: %v", err)
} }
logger.Debug("Setting up Lua print function to Go")
L.SetGlobal("print", L.NewFunction(printToGo)) L.SetGlobal("print", L.NewFunction(printToGo))
return nil return nil
} }
@@ -279,6 +389,8 @@ func PrependLuaAssignment(luaExpr string) string {
// BuildLuaScript prepares a Lua expression from shorthand notation // BuildLuaScript prepares a Lua expression from shorthand notation
func BuildLuaScript(luaExpr string) string { func BuildLuaScript(luaExpr string) string {
logger.Debug("Building Lua script from expression: %s", luaExpr)
luaExpr = PrependLuaAssignment(luaExpr) luaExpr = PrependLuaAssignment(luaExpr)
// This allows the user to specify whether or not they modified a value // This allows the user to specify whether or not they modified a value
@@ -299,17 +411,14 @@ func BuildLuaScript(luaExpr string) string {
} }
func printToGo(L *lua.LState) int { func printToGo(L *lua.LState) int {
// Get the number of arguments passed to the Lua print function top := L.GetTop()
n := L.GetTop() args := make([]interface{}, top)
// Create a slice to hold the arguments for i := 1; i <= top; i++ {
args := make([]interface{}, n) args[i-1] = L.Get(i)
for i := 1; i <= n; i++ {
args[i-1] = L.Get(i) // Get the argument from Lua stack
} }
// Print the arguments to Go's stdout message := fmt.Sprint(args...)
log.Print("Lua: ") logger.Info("[Lua] %s", message)
log.Println(args...) return 0
return 0 // No return values
} }
// Max returns the maximum of two integers // Max returns the maximum of two integers

View File

@@ -2,12 +2,14 @@ package processor
import ( import (
"fmt" "fmt"
"log"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"modify/logger"
) )
// RegexProcessor implements the Processor interface using regex patterns // RegexProcessor implements the Processor interface using regex patterns
@@ -15,72 +17,351 @@ type RegexProcessor struct{}
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings) // ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error { func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error {
captures, ok := data.([]string) captureGroups, ok := data.([]*CaptureGroup)
if !ok { if !ok {
return fmt.Errorf("expected []string for captures, got %T", data) return fmt.Errorf("expected []*CaptureGroup for captures, got %T", data)
} }
// Set variables for each capture group, starting from v1/s1 for the first capture groupindex := 0
for i := 0; i < len(captures); i++ { for _, capture := range captureGroups {
// Set string version (always available as s1, s2, etc.) if capture.Name == "" {
L.SetGlobal(fmt.Sprintf("s%d", i+1), lua.LString(captures[i])) // We don't want to change the name of the capture group
// Even if it's empty
tempName := fmt.Sprintf("%d", groupindex+1)
groupindex++
// Try to convert to number and set v1, v2, etc. L.SetGlobal("s"+tempName, lua.LString(capture.Value))
if val, err := strconv.ParseFloat(captures[i], 64); err == nil {
L.SetGlobal(fmt.Sprintf("v%d", i+1), lua.LNumber(val)) val, err := strconv.ParseFloat(capture.Value, 64)
if err == nil {
L.SetGlobal("v"+tempName, lua.LNumber(val))
}
} else {
val, err := strconv.ParseFloat(capture.Value, 64)
if err == nil {
L.SetGlobal(capture.Name, lua.LNumber(val))
} else {
L.SetGlobal(capture.Name, lua.LString(capture.Value))
}
} }
} }
return nil return nil
} }
// FromLua implements the Processor interface for RegexProcessor
func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) { func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
// Get the modified values after Lua execution // Stub to satisfy interface
modifications := make(map[int]string) return nil, nil
}
// Check for modifications to v1-v12 and s1-s12 // FromLua implements the Processor interface for RegexProcessor
for i := 0; i < 12; i++ { func (p *RegexProcessor) FromLuaCustom(L *lua.LState, captureGroups []*CaptureGroup) ([]*CaptureGroup, error) {
// Check both v and s variables to see if any were modified captureIndex := 0
vVarName := fmt.Sprintf("v%d", i+1) for _, capture := range captureGroups {
sVarName := fmt.Sprintf("s%d", i+1) if capture.Name == "" {
capture.Name = fmt.Sprintf("%d", captureIndex+1)
vVarName := fmt.Sprintf("v%s", capture.Name)
sVarName := fmt.Sprintf("s%s", capture.Name)
captureIndex++
vLuaVal := L.GetGlobal(vVarName) vLuaVal := L.GetGlobal(vVarName)
sLuaVal := L.GetGlobal(sVarName) sLuaVal := L.GetGlobal(sVarName)
// If our value is a number then it's very likely we want it to be a number
// And not a string
// If we do want it to be a string we will cast it into a string in lua
// wait that wouldn't work... Casting v to a string would not load it here
if vLuaVal.Type() == lua.LTNumber {
modifications[i] = vLuaVal.String()
continue
}
if sLuaVal.Type() == lua.LTString { if sLuaVal.Type() == lua.LTString {
modifications[i] = sLuaVal.String() capture.Updated = sLuaVal.String()
continue }
// Numbers have priority
if vLuaVal.Type() == lua.LTNumber {
capture.Updated = vLuaVal.String()
}
} else {
// Easy shit
capture.Updated = L.GetGlobal(capture.Name).String()
}
} }
return captureGroups, nil
} }
return modifications, nil type CaptureGroup struct {
}
type NamedCapture struct {
Name string Name string
Value string Value string
Updated string
Range [2]int Range [2]int
} }
type ReplaceCommand struct {
From int
To int
With string
}
// ProcessContent applies regex replacement with Lua processing // ProcessContent applies regex replacement with Lua processing
func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) { func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
pattern = ResolveRegexPlaceholders(pattern)
logger.Debug("Compiling regex pattern: %s", pattern)
compiledPattern, err := regexp.Compile(pattern)
if err != nil {
logger.Error("Error compiling pattern: %v", err)
return "", 0, 0, fmt.Errorf("error compiling pattern: %v", err)
}
logger.Debug("Compiled pattern successfully: %s", pattern)
previous := luaExpr
luaExpr = BuildLuaScript(luaExpr)
logger.Debug("Transformed Lua expression: %q → %q", previous, luaExpr)
// Initialize Lua environment
modificationCount := 0
// Process all regex matches
result := content
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
logger.Debug("Found %d matches in content of length %d", len(indices), len(content))
// We walk backwards because we're replacing something with something else that might be longer
// And in the case it is longer than the original all indicces past that change will be fucked up
// By going backwards we fuck up all the indices to the end of the file that we don't care about
// Because there either aren't any (last match) or they're already modified (subsequent matches)
for i := len(indices) - 1; i >= 0; i-- {
logger.Debug("Processing match %d of %d", i+1, len(indices))
L, err := NewLuaState()
if err != nil {
logger.Error("Error creating Lua state: %v", err)
return "", 0, 0, fmt.Errorf("error creating Lua state: %v", err)
}
// Hmm... Maybe we don't want to defer this..
// Maybe we want to close them every iteration
// We'll leave it as is for now
defer L.Close()
logger.Trace("Lua state created successfully for match %d", i+1)
matchIndices := indices[i]
logger.Trace("Match indices: %v (match position %d-%d)", matchIndices, matchIndices[0], matchIndices[1])
// Why we're doing this whole song and dance of indices is to properly handle empty matches
// Plus it's a little cleaner to surgically replace our matches
// If we were to use string.replace and encountered an empty match there'd be nothing to replace
// But using indices an empty match would have its starting and ending indices be the same
// So when we're cutting open the array we say 0:7 + modified + 7:end
// As if concatenating in the middle of the array
// Plus it supports lookarounds
match := content[matchIndices[0]:matchIndices[1]]
matchPreview := match
if len(match) > 50 {
matchPreview = match[:47] + "..."
}
logger.Trace("Matched content: %q (length: %d)", matchPreview, len(match))
groups := matchIndices[2:]
if len(groups) <= 0 {
logger.Warning("No capture groups found for match %q and regex %q", matchPreview, pattern)
continue
}
if len(groups)%2 == 1 {
logger.Warning("Invalid number of group indices (%d), should be even: %v", len(groups), groups)
continue
}
// Count how many valid groups we have
validGroups := 0
for j := 0; j < len(groups); j += 2 {
if groups[j] != -1 && groups[j+1] != -1 {
validGroups++
}
}
logger.Debug("Found %d valid capture groups in match", validGroups)
for _, index := range groups {
if index == -1 {
logger.Warning("Negative index encountered in match indices %v. This may indicate an issue with the regex pattern or an empty/optional capture group.", matchIndices)
continue
}
}
// We have to use array to preserve order
// Very important for the reconstruction step
// Because we must overwrite the values in reverse order
// See comments a few dozen lines above for more details
captureGroups := make([]*CaptureGroup, 0, len(groups)/2)
groupNames := compiledPattern.SubexpNames()[1:]
for i, name := range groupNames {
start := groups[i*2]
end := groups[i*2+1]
if start == -1 || end == -1 {
continue
}
value := content[start:end]
captureGroups = append(captureGroups, &CaptureGroup{
Name: name,
Value: value,
Range: [2]int{start, end},
})
// Include name info in log if available
if name != "" {
logger.Trace("Capture group '%s': %q (pos %d-%d)", name, value, start, end)
} else {
logger.Trace("Capture group #%d: %q (pos %d-%d)", i+1, value, start, end)
}
}
captureGroups = deduplicateGroups(captureGroups)
if err := p.ToLua(L, captureGroups); err != nil {
logger.Error("Failed to set Lua variables: %v", err)
continue
}
logger.Trace("Set %d capture groups as Lua variables", len(captureGroups))
if err := L.DoString(luaExpr); err != nil {
logger.Error("Lua script execution failed: %v\nScript: %s\nCapture Groups: %+v",
err, luaExpr, captureGroups)
continue
}
logger.Trace("Lua script executed successfully")
// Get modifications from Lua
captureGroups, err = p.FromLuaCustom(L, captureGroups)
if err != nil {
logger.Error("Failed to retrieve modifications from Lua: %v", err)
continue
}
logger.Trace("Retrieved updated values from Lua")
replacement := ""
replacementVar := L.GetGlobal("replacement")
if replacementVar.Type() != lua.LTNil {
replacement = replacementVar.String()
logger.Debug("Using global replacement: %q", replacement)
}
// Check if modification flag is set
modifiedVal := L.GetGlobal("modified")
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
logger.Debug("Skipping match - no modifications made by Lua script")
continue
}
if replacement == "" {
commands := make([]ReplaceCommand, 0, len(captureGroups))
// Apply the modifications to the original match
replacement = match
// Count groups that were actually modified
modifiedGroups := 0
for _, capture := range captureGroups {
if capture.Value != capture.Updated {
modifiedGroups++
}
}
logger.Debug("%d of %d capture groups were modified", modifiedGroups, len(captureGroups))
for _, capture := range captureGroups {
if capture.Value == capture.Updated {
logger.Trace("Capture group unchanged: %s", capture.Value)
continue
}
// Log what changed with context
logger.Debug("Modifying group %s: %q → %q",
capture.Name, capture.Value, capture.Updated)
// Indices of the group are relative to content
// To relate them to match we have to subtract the match start index
// replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
commands = append(commands, ReplaceCommand{
From: capture.Range[0] - matchIndices[0],
To: capture.Range[1] - matchIndices[0],
With: capture.Updated,
})
}
// Sort commands in reverse order for safe replacements
sort.Slice(commands, func(i, j int) bool {
return commands[i].From > commands[j].From
})
logger.Trace("Applying %d replacement commands in reverse order", len(commands))
for _, command := range commands {
logger.Trace("Replace pos %d-%d with %q", command.From, command.To, command.With)
if command.To < command.From {
logger.Error("Command to is less than from: %v", command)
continue
}
if command.From > len(replacement) || command.To > len(replacement) {
logger.Error("Command from or to is greater than replacement length: %v", command)
continue
}
replacement = replacement[:command.From] + command.With + replacement[command.To:]
}
}
// Preview the replacement for logging
replacementPreview := replacement
if len(replacement) > 50 {
replacementPreview = replacement[:47] + "..."
}
logger.Debug("Replacing match %q with %q", matchPreview, replacementPreview)
modificationCount++
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
logger.Debug("Match #%d processed, running modification count: %d", i+1, modificationCount)
}
logger.Info("Regex processing complete: %d modifications from %d matches", modificationCount, len(indices))
return result, modificationCount, len(indices), nil
}
func deduplicateGroups(captureGroups []*CaptureGroup) []*CaptureGroup {
deduplicatedGroups := make([]*CaptureGroup, 0)
for _, group := range captureGroups {
overlaps := false
logger.Debug("Checking capture group: %s with range %v", group.Name, group.Range)
for _, existingGroup := range deduplicatedGroups {
logger.Debug("Comparing with existing group: %s with range %v", existingGroup.Name, existingGroup.Range)
if group.Range[0] < existingGroup.Range[1] && group.Range[1] > existingGroup.Range[0] {
overlaps = true
logger.Warning("Detected overlap between capture group '%s' and existing group '%s' in range %v-%v and %v-%v", group.Name, existingGroup.Name, group.Range[0], group.Range[1], existingGroup.Range[0], existingGroup.Range[1])
break
}
}
if overlaps {
// We CAN just continue despite this fuckup
logger.Error("Overlapping capture group: %s", group.Name)
continue
}
logger.Debug("No overlap detected for capture group: %s. Adding to deduplicated groups.", group.Name)
deduplicatedGroups = append(deduplicatedGroups, group)
}
return deduplicatedGroups
}
// The order of these replaces is important
// This one handles !num-s inside of named capture groups
// If it were not here our !num in a named capture group would
// Expand to another capture group in the capture group
// We really only want one (our named) capture group
func ResolveRegexPlaceholders(pattern string) string {
// Handle special pattern modifications // Handle special pattern modifications
if !strings.HasPrefix(pattern, "(?s)") { if !strings.HasPrefix(pattern, "(?s)") {
pattern = "(?s)" + pattern pattern = "(?s)" + pattern
log.Printf("Pattern modified to include (?s): %s", pattern) // Use fmt.Printf for test compatibility
fmt.Printf("Pattern modified to include (?s): %s\n", pattern)
} }
pattern = strings.ReplaceAll(pattern, "!num", `"?(\d*\.?\d+)"?`) namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`)
pattern = namedGroupNum.ReplaceAllStringFunc(pattern, func(match string) string {
parts := namedGroupNum.FindStringSubmatch(match)
if len(parts) != 3 {
return match
}
replacement := `-?\d*\.?\d+`
return parts[1] + replacement
})
pattern = strings.ReplaceAll(pattern, "!num", `"?(-?\d*\.?\d+)"?`)
pattern = strings.ReplaceAll(pattern, "!any", `.*?`) pattern = strings.ReplaceAll(pattern, "!any", `.*?`)
repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`) repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`)
// !rep(pattern, count) repeats the pattern n times // !rep(pattern, count) repeats the pattern n times
@@ -95,174 +376,5 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
repetitions, _ := strconv.Atoi(count) repetitions, _ := strconv.Atoi(count)
return strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern return strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern
}) })
return pattern
compiledPattern, err := regexp.Compile(pattern)
if err != nil {
log.Printf("Error compiling pattern: %v", err)
return "", 0, 0, fmt.Errorf("error compiling pattern: %v", err)
}
log.Printf("Compiled pattern successfully: %s", pattern)
previous := luaExpr
luaExpr = BuildLuaScript(luaExpr)
log.Printf("Changing Lua expression from: %s to: %s", previous, luaExpr)
// Initialize Lua environment
modificationCount := 0
// Process all regex matches
result := content
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
log.Printf("Found %d matches in the content", len(indices))
// We walk backwards because we're replacing something with something else that might be longer
// And in the case it is longer than the original all indicces past that change will be fucked up
// By going backwards we fuck up all the indices to the end of the file that we don't care about
// Because there either aren't any (last match) or they're already modified (subsequent matches)
for i := len(indices) - 1; i >= 0; i-- {
L, err := NewLuaState()
if err != nil {
log.Printf("Error creating Lua state: %v", err)
return "", 0, 0, fmt.Errorf("error creating Lua state: %v", err)
}
// Hmm... Maybe we don't want to defer this..
// Maybe we want to close them every iteration
// We'll leave it as is for now
defer L.Close()
log.Printf("Lua state created successfully")
matchIndices := indices[i]
log.Printf("Processing match indices: %v", matchIndices)
// Why we're doing this whole song and dance of indices is to properly handle empty matches
// Plus it's a little cleaner to surgically replace our matches
// If we were to use string.replace and encountered an empty match there'd be nothing to replace
// But using indices an empty match would have its starting and ending indices be the same
// So when we're cutting open the array we say 0:7 + modified + 7:end
// As if concatenating in the middle of the array
// Plus it supports lookarounds
match := content[matchIndices[0]:matchIndices[1]]
log.Printf("Matched content: %s", match)
groups := matchIndices[2:]
if len(groups) <= 0 {
log.Println("No capture groups for lua to chew on")
continue
}
if len(groups)%2 == 1 {
log.Println("Odd number of indices of groups, what the fuck?")
continue
}
for _, index := range groups {
if index == -1 {
// return "", 0, 0, fmt.Errorf("negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. Please check the regex pattern and input content.", matchIndices)
log.Printf("Negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. This is not an error but it's possibly not what you want.", matchIndices)
continue
}
}
captures := make([]string, 0, len(groups)/2)
for j := 0; j < len(groups); j += 2 {
if groups[j] == -1 || groups[j+1] == -1 {
continue
}
captures = append(captures, content[groups[j]:groups[j+1]])
}
log.Printf("Captured groups: %v", captures)
// We have to use array to preserve order
// Very important for the reconstruction step
// Because we must overwrite the values in reverse order
// See comments a few dozen lines above for more details
namedCaptures := make([]NamedCapture, 0, len(groups)/2)
groupNames := compiledPattern.SubexpNames()[1:]
for i, name := range groupNames {
if name == "" {
continue
}
if groups[i*2] == -1 || groups[i*2+1] == -1 {
continue
}
namedCaptures = append(namedCaptures, NamedCapture{
Name: name,
Value: captures[i],
Range: [2]int{groups[i*2], groups[i*2+1]},
})
}
log.Printf("Named captures: %v", namedCaptures)
if err := p.ToLua(L, captures); err != nil {
log.Printf("Error setting Lua variables: %v", err)
continue
}
log.Println("Lua variables set successfully")
for _, capture := range namedCaptures {
if capture.Name == "" {
continue
}
if val, err := strconv.ParseFloat(capture.Value, 64); err == nil {
L.SetGlobal(capture.Name, lua.LNumber(val))
} else {
L.SetGlobal(capture.Name, lua.LString(capture.Value))
}
}
if err := L.DoString(luaExpr); err != nil {
log.Printf("Error executing Lua code %s for group %s: %v", luaExpr, captures, err)
continue
}
log.Println("Lua code executed successfully")
// Get modifications from Lua
modResult, err := p.FromLua(L)
if err != nil {
log.Printf("Error getting modifications: %v", err)
continue
}
// Apply modifications to the matched text
modsMap, ok := modResult.(map[int]string)
if !ok || len(modsMap) == 0 {
log.Println("No modifications to apply")
continue
}
replacement := ""
replacementVar := L.GetGlobal("replacement")
if replacementVar.Type() != lua.LTNil {
replacement = replacementVar.String()
}
if replacement == "" {
// Apply the modifications to the original match
replacement = match
for i := len(modsMap) - 1; i >= 0; i-- {
newVal := modsMap[i]
log.Printf("Applying modification: %s", newVal)
// Indices of the group are relative to content
// To relate them to match we have to subtract the match start index
groupStart := groups[i*2] - matchIndices[0]
groupEnd := groups[i*2+1] - matchIndices[0]
replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
}
for i := len(namedCaptures) - 1; i >= 0; i-- {
capture := namedCaptures[i]
if capture.Name == "" {
continue
}
groupStart := capture.Range[0] - matchIndices[0]
groupEnd := capture.Range[1] - matchIndices[0]
luaValue := L.GetGlobal(capture.Name).String()
replacement = replacement[:groupStart] + luaValue + replacement[groupEnd:]
}
}
modificationCount++
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
log.Printf("Modification count updated: %d", modificationCount)
}
log.Printf("Process completed with %d modifications", modificationCount)
return result, modificationCount, len(indices), nil
} }

View File

@@ -581,6 +581,39 @@ func TestNamedCaptureGroups(t *testing.T) {
} }
} }
func TestNamedCaptureGroupsNum(t *testing.T) {
content := `<config>
<item>
<value>100</value>
</item>
</config>`
expected := `<config>
<item>
<value>200</value>
</item>
</config>`
p := &RegexProcessor{}
result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(?<amount>!num)</value>`, "amount = amount * 2")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
if matches != 1 {
t.Errorf("Expected 1 match, got %d", matches)
}
if mods != 1 {
t.Errorf("Expected 1 modification, got %d", mods)
}
if result != expected {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestMultipleNamedCaptureGroups(t *testing.T) { func TestMultipleNamedCaptureGroups(t *testing.T) {
content := `<product> content := `<product>
<name>Widget</name> <name>Widget</name>

26
processor/test_helper.go Normal file
View File

@@ -0,0 +1,26 @@
package processor
import (
"io"
"modify/logger"
"os"
)
func init() {
// Only modify logger in test mode
// This checks if we're running under 'go test'
if os.Getenv("GO_TESTING") == "1" || os.Getenv("TESTING") == "1" {
// Initialize logger with ERROR level for tests
// to minimize noise in test output
logger.Init(logger.LevelError)
// Optionally redirect logger output to discard
// This prevents logger output from interfering with test output
disableTestLogs := os.Getenv("ENABLE_TEST_LOGS") != "1"
if disableTestLogs {
// Create a new logger that writes to nowhere
silentLogger := logger.New(io.Discard, "", 0)
logger.DefaultLogger = silentLogger
}
}
}

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"fmt" "fmt"
"log" "modify/logger"
"modify/processor/xpath" "modify/processor/xpath"
"strings" "strings"
@@ -15,69 +15,91 @@ type XMLProcessor struct{}
// ProcessContent implements the Processor interface for XMLProcessor // ProcessContent implements the Processor interface for XMLProcessor
func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr string) (string, int, int, error) { func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr string) (string, int, int, error) {
logger.Debug("Processing XML content with XPath: %s", path)
// Parse XML document // Parse XML document
// We can't really use encoding/xml here because it requires a pre defined struct // We can't really use encoding/xml here because it requires a pre defined struct
// And we HAVE TO parse dynamic unknown XML // And we HAVE TO parse dynamic unknown XML
logger.Trace("Parsing XML document")
doc, err := xmlquery.Parse(strings.NewReader(content)) doc, err := xmlquery.Parse(strings.NewReader(content))
if err != nil { if err != nil {
logger.Error("Failed to parse XML: %v", err)
return content, 0, 0, fmt.Errorf("error parsing XML: %v", err) return content, 0, 0, fmt.Errorf("error parsing XML: %v", err)
} }
// Find nodes matching the XPath pattern // Find nodes matching the XPath pattern
logger.Debug("Executing XPath query: %s", path)
nodes, err := xpath.Get(doc, path) nodes, err := xpath.Get(doc, path)
if err != nil { if err != nil {
logger.Error("Failed to execute XPath: %v", err)
return content, 0, 0, fmt.Errorf("error executing XPath: %v", err) return content, 0, 0, fmt.Errorf("error executing XPath: %v", err)
} }
matchCount := len(nodes) matchCount := len(nodes)
logger.Debug("Found %d nodes matching XPath", matchCount)
if matchCount == 0 { if matchCount == 0 {
logger.Warning("No nodes matched the XPath pattern: %s", path)
return content, 0, 0, nil return content, 0, 0, nil
} }
// Apply modifications to each node // Apply modifications to each node
modCount := 0 modCount := 0
for _, node := range nodes { for i, node := range nodes {
logger.Trace("Processing node #%d: %s", i+1, node.Data)
L, err := NewLuaState() L, err := NewLuaState()
if err != nil { if err != nil {
logger.Error("Failed to create Lua state: %v", err)
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err) return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err)
} }
defer L.Close() defer L.Close()
logger.Trace("Converting XML node to Lua")
err = p.ToLua(L, node) err = p.ToLua(L, node)
if err != nil { if err != nil {
logger.Error("Failed to convert XML node to Lua: %v", err)
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err) return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
} }
err = L.DoString(BuildLuaScript(luaExpr)) luaScript := BuildLuaScript(luaExpr)
logger.Trace("Executing Lua script: %s", luaScript)
err = L.DoString(luaScript)
if err != nil { if err != nil {
logger.Error("Failed to execute Lua script: %v", err)
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err) return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
} }
result, err := p.FromLua(L) result, err := p.FromLua(L)
if err != nil { if err != nil {
logger.Error("Failed to get result from Lua: %v", err)
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err) return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
} }
log.Printf("%#v", result) logger.Trace("Lua returned result: %#v", result)
modified := false modified := false
modified = L.GetGlobal("modified").String() == "true" modified = L.GetGlobal("modified").String() == "true"
if !modified { if !modified {
log.Printf("No changes made to node at path: %s", node.Data) logger.Debug("No changes made to node at path: %s", node.Data)
continue continue
} }
// Apply modification based on the result // Apply modification based on the result
if updatedValue, ok := result.(string); ok { if updatedValue, ok := result.(string); ok {
// If the result is a simple string, update the node value directly // If the result is a simple string, update the node value directly
logger.Debug("Updating node with string value: %s", updatedValue)
xpath.Set(doc, path, updatedValue) xpath.Set(doc, path, updatedValue)
} else if nodeData, ok := result.(map[string]interface{}); ok { } else if nodeData, ok := result.(map[string]interface{}); ok {
// If the result is a map, apply more complex updates // If the result is a map, apply more complex updates
logger.Debug("Updating node with complex data structure")
updateNodeFromMap(node, nodeData) updateNodeFromMap(node, nodeData)
} }
modCount++ modCount++
logger.Debug("Successfully modified node #%d", i+1)
} }
logger.Info("XML processing complete: %d modifications from %d matches", modCount, matchCount)
// Serialize the modified XML document to string // Serialize the modified XML document to string
if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode { if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode {
// If we have an XML declaration, start with it // If we have an XML declaration, start with it

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
package regression
import (
"modify/processor"
"os"
"path/filepath"
"testing"
)
func TestTalentsMechanicOutOfRange(t *testing.T) {
given := `<Talent identifier="quickfixer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.quickfixer">
<Replace tag="[amount]" value="20" color="gui.green"/>
<Replace tag="[duration]" value="10" color="gui.green"/>
</Description>
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="1"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
<Affliction identifier="quickfixer" amount="10.0"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupEffect>
</Talent>`
actual := `<Talent identifier="quickfixer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.quickfixer">
<Replace tag="[amount]" value="30" color="gui.green"/>
<Replace tag="[duration]" value="20" color="gui.green"/>
</Description>
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="2"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
<Affliction identifier="quickfixer" amount="20"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupEffect>
</Talent>`
p := &processor.RegexProcessor{}
result, mods, matches, err := p.ProcessContent(given, `<Talent identifier="quickfixer">!anyvalue="(?<movementspeed>!num)"!anyvalue="(?<duration>!num)"!anyvalue="(?<repairspeed>!num)"!anyamount="(?<durationv>!num)"`, "movementspeed=round(movementspeed*1.5, 2) duration=round(duration*2, 2) repairspeed=round(repairspeed*2, 2) durationv=duration")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
if matches != 1 {
t.Errorf("Expected 1 match, got %d", matches)
}
if mods != 1 {
t.Errorf("Expected 1 modification, got %d", mods)
}
if result != actual {
t.Errorf("expected %s, got %s", actual, result)
}
}
func TestIndexExplosions_ShouldNotPanic(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Error getting current working directory: %v", err)
}
given, err := os.ReadFile(filepath.Join(cwd, "..", "testfiles", "OutpostItems.xml"))
if err != nil {
t.Fatalf("Error reading file: %v", err)
}
expected, err := os.ReadFile(filepath.Join(cwd, "..", "testfiles", "OutpostItemsExpected.xml"))
if err != nil {
t.Fatalf("Error reading file: %v", err)
}
p := &processor.RegexProcessor{}
result, mods, matches, err := p.ProcessContent(string(given), `(?-s)LightComponent!anyrange="(!num)"`, "*4")
if err != nil {
t.Fatalf("Error processing content: %v", err)
}
if matches != 45 {
t.Errorf("Expected 45 match, got %d", matches)
}
if mods != 45 {
t.Errorf("Expected 45 modification, got %d", mods)
}
if string(result) != string(expected) {
t.Errorf("expected %s, got %s", expected, result)
}
}

View File

@@ -1 +0,0 @@
<config><item><value>100</value></item></config>

View File

@@ -1,12 +0,0 @@
<config>
<item>
<value>75</value>
<multiplier>2</multiplier>
<divider>4</divider>
</item>
<item>
<value>150</value>
<multiplier>3</multiplier>
<divider>2</divider>
</item>
</config>

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<testdata>
<!-- Numeric values -->
<item>
<id>1</id>
<value>200</value>
<price>24.99</price>
<quantity>5</quantity>
</item>
<!-- Text values -->
<item>
<id>2</id>
<name>Test Product</name>
<description>This is a test product description</description>
<category>Test</category>
</item>
<!-- Mixed content -->
<item>
<id>3</id>
<name>Mixed Product</name>
<price>19.99</price>
<code>PRD-123</code>
<tags>sale,discount,new</tags>
</item>
<!-- Empty and special values -->
<item>
<id>4</id>
<value></value>
<specialChars>Hello &amp; World &lt; &gt; &quot; &apos;</specialChars>
<multiline>Line 1
Line 2
Line 3</multiline>
</item>
</testdata>

1252
testfiles/OutpostItems.xml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
<config><item><value>100</value></item></config>