Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
da93770334 | |||
d9f54a8354 | |||
dc8da8ab63 | |||
24262a7dca | |||
d77b13c363 | |||
a9c60a3698 | |||
66bcf21d79 | |||
e847e5c3ce | |||
9a70c9696e | |||
9cea103042 | |||
81d8259dfc | |||
5c5fbac63f | |||
3e818e61c7 | |||
001470ffe4 | |||
d88a76c4e2 | |||
d3a1f1bd96 | |||
07a5f3f1a4 | |||
e2257e082a |
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -10,11 +10,11 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Barotrauma",
|
||||
"args": [
|
||||
"-mode=json",
|
||||
"$..name",
|
||||
"v='pero'",
|
||||
"test.json"
|
||||
"LightComponent!anyrange=\"(!num)\"",
|
||||
"*4",
|
||||
"**/*.xml"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -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>
|
27
cmd/log_format_test/main.go
Normal file
27
cmd/log_format_test/main.go
Normal 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
445
logger/logger.go
Normal 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
49
logger/panic_handler.go
Normal 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
108
main.go
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
|
||||
"modify/logger"
|
||||
"modify/processor"
|
||||
)
|
||||
|
||||
@@ -24,20 +25,22 @@ type GlobalStats struct {
|
||||
}
|
||||
|
||||
var stats GlobalStats
|
||||
var logger *log.Logger
|
||||
var stdLogger *log.Logger // Legacy logger for compatibility
|
||||
|
||||
var (
|
||||
jsonFlag = flag.Bool("json", false, "Process JSON files")
|
||||
xmlFlag = flag.Bool("xml", false, "Process XML files")
|
||||
gitFlag = flag.Bool("git", false, "Use git to manage files")
|
||||
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
|
||||
worktree *git.Worktree
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Keep standard logger setup for compatibility with legacy code
|
||||
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{}
|
||||
}
|
||||
@@ -66,6 +69,8 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, " Use git to manage files\n")
|
||||
fmt.Fprintf(os.Stderr, " -reset\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, " Processing mode: regex, xml, json (default \"regex\")\n")
|
||||
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
||||
@@ -86,13 +91,19 @@ func main() {
|
||||
}
|
||||
|
||||
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()
|
||||
if *resetFlag {
|
||||
*gitFlag = true
|
||||
}
|
||||
|
||||
if len(args) < 3 {
|
||||
log.Printf("At least %d arguments are required", 3)
|
||||
logger.Error("At least %d arguments are required", 3)
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
@@ -109,37 +120,45 @@ func main() {
|
||||
originalLuaExpr := luaExpr
|
||||
luaExpr = processor.BuildLuaScript(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 {
|
||||
logger.Info("Git integration enabled, setting up git repository")
|
||||
err := setupGit()
|
||||
if err != nil {
|
||||
logger.Error("Failed to setup git: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Expand file patterns with glob support
|
||||
logger.Debug("Expanding file patterns: %v", filePatterns)
|
||||
files, err := expandFilePatterns(filePatterns)
|
||||
if err != nil {
|
||||
logger.Error("Failed to expand file patterns: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
if *gitFlag {
|
||||
logger.Info("Cleaning up git files before processing")
|
||||
err := cleanupGitFiles(files)
|
||||
if err != nil {
|
||||
logger.Error("Failed to cleanup git files: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
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")
|
||||
return
|
||||
}
|
||||
@@ -149,15 +168,15 @@ func main() {
|
||||
switch {
|
||||
case *xmlFlag:
|
||||
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))
|
||||
case *jsonFlag:
|
||||
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))
|
||||
default:
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -165,29 +184,42 @@ func main() {
|
||||
// Process each file
|
||||
for _, file := range files {
|
||||
wg.Add(1)
|
||||
go func(file string) {
|
||||
logger.SafeGoWithArgs(func(args ...interface{}) {
|
||||
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
|
||||
modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr)
|
||||
modCount, matchCount, err := processor.Process(proc, fileToProcess, pattern, luaExpr)
|
||||
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++
|
||||
} 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.TotalMatches += matchCount
|
||||
stats.TotalModifications += modCount
|
||||
}
|
||||
}(file)
|
||||
}, file)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Print summary
|
||||
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")
|
||||
} 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",
|
||||
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
|
||||
}
|
||||
@@ -198,27 +230,27 @@ func setupGit() error {
|
||||
if err != nil {
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -226,39 +258,55 @@ func expandFilePatterns(patterns []string) ([]string, error) {
|
||||
var files []string
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func cleanupGitFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
logger.Printf("Checking file: %s", file)
|
||||
logger.Debug("Checking git status for file: %s", file)
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
logger.Error("Error getting worktree status: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err)
|
||||
return fmt.Errorf("error getting worktree status: %w", err)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Error("Error adding file to git: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err)
|
||||
return fmt.Errorf("error adding file to git: %w", err)
|
||||
}
|
||||
|
||||
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{
|
||||
Author: &object.Signature{
|
||||
Name: "Big Chef",
|
||||
@@ -267,22 +315,24 @@ func cleanupGitFiles(files []string) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Error committing file: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error committing file: %v\n", 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 {
|
||||
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{
|
||||
Files: []string{file},
|
||||
Staged: true,
|
||||
Worktree: true,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Error restoring file: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", 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
|
||||
|
@@ -3,7 +3,7 @@ package processor
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"modify/logger"
|
||||
"modify/processor/jsonpath"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
@@ -14,80 +14,98 @@ type JSONProcessor struct{}
|
||||
|
||||
// ProcessContent implements the Processor interface for JSONProcessor
|
||||
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
|
||||
logger.Trace("Parsing JSON document")
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(content), &jsonData)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse JSON: %v", err)
|
||||
return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err)
|
||||
}
|
||||
|
||||
// Find nodes matching the JSONPath pattern
|
||||
logger.Debug("Executing JSONPath query: %s", pattern)
|
||||
nodes, err := jsonpath.Get(jsonData, pattern)
|
||||
if err != nil {
|
||||
logger.Error("Failed to execute JSONPath: %v", err)
|
||||
return content, 0, 0, fmt.Errorf("error getting nodes: %v", err)
|
||||
}
|
||||
|
||||
matchCount := len(nodes)
|
||||
logger.Debug("Found %d nodes matching JSONPath", matchCount)
|
||||
if matchCount == 0 {
|
||||
logger.Warning("No nodes matched the JSONPath pattern: %s", pattern)
|
||||
return content, 0, 0, nil
|
||||
}
|
||||
|
||||
modCount := 0
|
||||
for _, node := range nodes {
|
||||
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
|
||||
for i, node := range nodes {
|
||||
logger.Trace("Processing node #%d at path: %s with value: %v", i+1, node.Path, node.Value)
|
||||
|
||||
// Initialize Lua
|
||||
L, err := NewLuaState()
|
||||
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)
|
||||
}
|
||||
defer L.Close()
|
||||
log.Println("Lua state initialized successfully.")
|
||||
logger.Trace("Lua state initialized successfully")
|
||||
|
||||
err = p.ToLua(L, node.Value)
|
||||
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)
|
||||
}
|
||||
log.Printf("Converted node value to Lua: %v", node.Value)
|
||||
logger.Trace("Converted node value to Lua: %v", node.Value)
|
||||
|
||||
originalScript := 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
|
||||
log.Printf("Executing Lua script: %q", fullScript)
|
||||
logger.Trace("Executing Lua script: %q", fullScript)
|
||||
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)
|
||||
}
|
||||
log.Println("Lua script executed successfully.")
|
||||
logger.Trace("Lua script executed successfully")
|
||||
|
||||
// Get modified value
|
||||
result, err := p.FromLua(L)
|
||||
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)
|
||||
}
|
||||
log.Printf("Retrieved modified value from Lua: %v", result)
|
||||
logger.Trace("Retrieved modified value from Lua: %v", result)
|
||||
|
||||
modified := false
|
||||
modified = L.GetGlobal("modified").String() == "true"
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
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++
|
||||
}
|
||||
|
||||
logger.Info("JSON processing complete: %d modifications from %d matches", modCount, matchCount)
|
||||
|
||||
// Convert the modified JSON back to a string with same formatting
|
||||
logger.Trace("Marshalling JSON data back to string")
|
||||
var jsonBytes []byte
|
||||
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
||||
if err != nil {
|
||||
logger.Error("Failed to marshal JSON: %v", err)
|
||||
return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
|
||||
}
|
||||
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
|
||||
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
|
||||
if path == "$" {
|
||||
logger.Debug("Handling special case for root node update")
|
||||
// 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
|
||||
|
||||
@@ -108,15 +129,18 @@ func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newVa
|
||||
if !ok {
|
||||
// If the original wasn't a map, completely replace it with the new map
|
||||
// This is handled by the jsonpath.Set function
|
||||
logger.Debug("Root was not a map, replacing entire root")
|
||||
return jsonpath.Set(jsonData, path, newValue)
|
||||
}
|
||||
|
||||
// Clear the original map
|
||||
logger.Trace("Clearing original root map")
|
||||
for k := range rootMap {
|
||||
delete(rootMap, k)
|
||||
}
|
||||
|
||||
// Copy all keys from the new map
|
||||
logger.Trace("Copying keys to root map")
|
||||
for k, v := range rootValue {
|
||||
rootMap[k] = v
|
||||
}
|
||||
@@ -127,22 +151,27 @@ func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newVa
|
||||
rootArray, ok := jsonData.([]interface{})
|
||||
if !ok {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Clear and recreate the array
|
||||
logger.Trace("Replacing root array")
|
||||
*&rootArray = rootValue
|
||||
return nil
|
||||
|
||||
default:
|
||||
// For other types, use jsonpath.Set
|
||||
logger.Debug("Replacing root with primitive value")
|
||||
return jsonpath.Set(jsonData, path, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 nil
|
||||
|
@@ -1,14 +1,17 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/antchfx/xmlquery"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
|
||||
"modify/logger"
|
||||
)
|
||||
|
||||
// Processor defines the interface for all file processors
|
||||
@@ -42,14 +45,18 @@ func NewLuaState() (*lua.LState, error) {
|
||||
// defer L.Close()
|
||||
|
||||
// Load math library
|
||||
logger.Debug("Loading Lua math library")
|
||||
L.Push(L.GetGlobal("require"))
|
||||
L.Push(lua.LString("math"))
|
||||
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)
|
||||
}
|
||||
|
||||
// Initialize helper functions
|
||||
logger.Debug("Initializing Lua helper functions")
|
||||
if err := InitLuaHelpers(L); err != nil {
|
||||
logger.Error("Failed to initialize Lua helper functions: %v", 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) {
|
||||
logger.Debug("Processing file %q with pattern %q", filename, pattern)
|
||||
|
||||
// Read file content
|
||||
cwd, err := os.Getwd()
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read file %s: %v", fullPath, err)
|
||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||
}
|
||||
|
||||
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
|
||||
logger.Debug("Starting content processing with %s processor", getProcessorType(p))
|
||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||
if err != nil {
|
||||
logger.Error("Processing error: %v", err)
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
logger.Debug("Processing results: %d matches, %d modifications", matchCount, modCount)
|
||||
|
||||
// If we made modifications, save the file
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Error("Failed to write to file %s: %v", fullPath, 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
|
||||
}
|
||||
|
||||
// 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
|
||||
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
||||
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) {
|
||||
logger.Trace("Checking if Lua table is an array")
|
||||
L.SetGlobal("table_to_check", v)
|
||||
|
||||
// Use our predefined helper function from InitLuaHelpers
|
||||
err := L.DoString(`is_array = isArray(table_to_check)`)
|
||||
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)
|
||||
}
|
||||
|
||||
// Check the result of our Lua function
|
||||
isArray := L.GetGlobal("is_array")
|
||||
// LVIsFalse returns true if a given LValue is a nil or false otherwise false.
|
||||
if !lua.LVIsFalse(isArray) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
result := !lua.LVIsFalse(isArray)
|
||||
logger.Trace("Lua table is array: %v", result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// InitLuaHelpers initializes common Lua helper functions
|
||||
func InitLuaHelpers(L *lua.LState) error {
|
||||
logger.Debug("Loading Lua helper functions")
|
||||
|
||||
helperScript := `
|
||||
-- Custom Lua helpers for math operations
|
||||
function min(a, b) return math.min(a, b) end
|
||||
@@ -205,6 +312,19 @@ function floor(x) return math.floor(x) end
|
||||
function ceil(x) return math.ceil(x) end
|
||||
function upper(s) return string.upper(s) end
|
||||
function lower(s) return string.lower(s) end
|
||||
function format(s, ...) return string.format(s, ...) end
|
||||
|
||||
-- String split helper
|
||||
function strsplit(inputstr, sep)
|
||||
if sep == nil then
|
||||
sep = "%s"
|
||||
end
|
||||
local t = {}
|
||||
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- String to number conversion helper
|
||||
function num(str)
|
||||
@@ -238,9 +358,11 @@ end
|
||||
modified = false
|
||||
`
|
||||
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)
|
||||
}
|
||||
|
||||
logger.Debug("Setting up Lua print function to Go")
|
||||
L.SetGlobal("print", L.NewFunction(printToGo))
|
||||
return nil
|
||||
}
|
||||
@@ -279,6 +401,8 @@ func PrependLuaAssignment(luaExpr string) string {
|
||||
|
||||
// BuildLuaScript prepares a Lua expression from shorthand notation
|
||||
func BuildLuaScript(luaExpr string) string {
|
||||
logger.Debug("Building Lua script from expression: %s", luaExpr)
|
||||
|
||||
luaExpr = PrependLuaAssignment(luaExpr)
|
||||
|
||||
// This allows the user to specify whether or not they modified a value
|
||||
@@ -299,17 +423,14 @@ func BuildLuaScript(luaExpr string) string {
|
||||
}
|
||||
|
||||
func printToGo(L *lua.LState) int {
|
||||
// Get the number of arguments passed to the Lua print function
|
||||
n := L.GetTop()
|
||||
// Create a slice to hold the arguments
|
||||
args := make([]interface{}, n)
|
||||
for i := 1; i <= n; i++ {
|
||||
args[i-1] = L.Get(i) // Get the argument from Lua stack
|
||||
top := L.GetTop()
|
||||
args := make([]interface{}, top)
|
||||
for i := 1; i <= top; i++ {
|
||||
args[i-1] = L.Get(i)
|
||||
}
|
||||
// Print the arguments to Go's stdout
|
||||
log.Print("Lua: ")
|
||||
log.Println(args...)
|
||||
return 0 // No return values
|
||||
message := fmt.Sprint(args...)
|
||||
logger.Info("[Lua] %s", message)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Max returns the maximum of two integers
|
||||
|
@@ -2,12 +2,14 @@ package processor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
|
||||
"modify/logger"
|
||||
)
|
||||
|
||||
// RegexProcessor implements the Processor interface using regex patterns
|
||||
@@ -15,69 +17,326 @@ type RegexProcessor struct{}
|
||||
|
||||
// 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 {
|
||||
captures, ok := data.([]string)
|
||||
captureGroups, ok := data.([]*CaptureGroup)
|
||||
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
|
||||
for i := 0; i < len(captures); i++ {
|
||||
// Set string version (always available as s1, s2, etc.)
|
||||
L.SetGlobal(fmt.Sprintf("s%d", i+1), lua.LString(captures[i]))
|
||||
groupindex := 0
|
||||
for _, capture := range captureGroups {
|
||||
if capture.Name == "" {
|
||||
// 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.
|
||||
if val, err := strconv.ParseFloat(captures[i], 64); err == nil {
|
||||
L.SetGlobal(fmt.Sprintf("v%d", i+1), lua.LNumber(val))
|
||||
L.SetGlobal("s"+tempName, lua.LString(capture.Value))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// FromLua implements the Processor interface for RegexProcessor
|
||||
func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||
// Get the modified values after Lua execution
|
||||
modifications := make(map[int]string)
|
||||
// Stub to satisfy interface
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check for modifications to v1-v12 and s1-s12
|
||||
for i := 0; i < 12; i++ {
|
||||
// Check both v and s variables to see if any were modified
|
||||
vVarName := fmt.Sprintf("v%d", i+1)
|
||||
sVarName := fmt.Sprintf("s%d", i+1)
|
||||
// FromLua implements the Processor interface for RegexProcessor
|
||||
func (p *RegexProcessor) FromLuaCustom(L *lua.LState, captureGroups []*CaptureGroup) ([]*CaptureGroup, error) {
|
||||
captureIndex := 0
|
||||
for _, capture := range captureGroups {
|
||||
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)
|
||||
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 {
|
||||
modifications[i] = sLuaVal.String()
|
||||
continue
|
||||
capture.Updated = sLuaVal.String()
|
||||
}
|
||||
// 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 NamedCapture struct {
|
||||
type CaptureGroup struct {
|
||||
Name string
|
||||
Value string
|
||||
Updated string
|
||||
Range [2]int
|
||||
}
|
||||
type ReplaceCommand struct {
|
||||
From int
|
||||
To int
|
||||
With string
|
||||
}
|
||||
|
||||
// ProcessContent applies regex replacement with Lua processing
|
||||
func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||
// Handle special pattern modifications
|
||||
if !strings.HasPrefix(pattern, "(?s)") {
|
||||
pattern = "(?s)" + pattern
|
||||
log.Printf("Pattern modified to include (?s): %s", pattern)
|
||||
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
|
||||
@@ -85,6 +344,14 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
||||
// 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
|
||||
if !strings.HasPrefix(pattern, "(?s)") {
|
||||
pattern = "(?s)" + pattern
|
||||
// Use fmt.Printf for test compatibility
|
||||
fmt.Printf("Pattern modified to include (?s): %s\n", pattern)
|
||||
}
|
||||
|
||||
namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`)
|
||||
pattern = namedGroupNum.ReplaceAllStringFunc(pattern, func(match string) string {
|
||||
parts := namedGroupNum.FindStringSubmatch(match)
|
||||
@@ -109,174 +376,5 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
||||
repetitions, _ := strconv.Atoi(count)
|
||||
return strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern
|
||||
})
|
||||
|
||||
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
|
||||
return pattern
|
||||
}
|
||||
|
26
processor/test_helper.go
Normal file
26
processor/test_helper.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ package processor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"modify/logger"
|
||||
"modify/processor/xpath"
|
||||
"strings"
|
||||
|
||||
@@ -15,69 +15,91 @@ type XMLProcessor struct{}
|
||||
|
||||
// ProcessContent implements the Processor interface for XMLProcessor
|
||||
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
|
||||
// We can't really use encoding/xml here because it requires a pre defined struct
|
||||
// And we HAVE TO parse dynamic unknown XML
|
||||
logger.Trace("Parsing XML document")
|
||||
doc, err := xmlquery.Parse(strings.NewReader(content))
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse XML: %v", err)
|
||||
return content, 0, 0, fmt.Errorf("error parsing XML: %v", err)
|
||||
}
|
||||
|
||||
// Find nodes matching the XPath pattern
|
||||
logger.Debug("Executing XPath query: %s", path)
|
||||
nodes, err := xpath.Get(doc, path)
|
||||
if err != nil {
|
||||
logger.Error("Failed to execute XPath: %v", err)
|
||||
return content, 0, 0, fmt.Errorf("error executing XPath: %v", err)
|
||||
}
|
||||
|
||||
matchCount := len(nodes)
|
||||
logger.Debug("Found %d nodes matching XPath", matchCount)
|
||||
if matchCount == 0 {
|
||||
logger.Warning("No nodes matched the XPath pattern: %s", path)
|
||||
return content, 0, 0, nil
|
||||
}
|
||||
|
||||
// Apply modifications to each node
|
||||
modCount := 0
|
||||
for _, node := range nodes {
|
||||
for i, node := range nodes {
|
||||
logger.Trace("Processing node #%d: %s", i+1, node.Data)
|
||||
|
||||
L, err := NewLuaState()
|
||||
if err != nil {
|
||||
logger.Error("Failed to create Lua state: %v", err)
|
||||
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||
}
|
||||
defer L.Close()
|
||||
|
||||
logger.Trace("Converting XML node to Lua")
|
||||
err = p.ToLua(L, node)
|
||||
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)
|
||||
}
|
||||
|
||||
err = L.DoString(BuildLuaScript(luaExpr))
|
||||
luaScript := BuildLuaScript(luaExpr)
|
||||
logger.Trace("Executing Lua script: %s", luaScript)
|
||||
err = L.DoString(luaScript)
|
||||
if err != nil {
|
||||
logger.Error("Failed to execute Lua script: %v", err)
|
||||
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
|
||||
}
|
||||
|
||||
result, err := p.FromLua(L)
|
||||
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)
|
||||
}
|
||||
log.Printf("%#v", result)
|
||||
logger.Trace("Lua returned result: %#v", result)
|
||||
|
||||
modified := false
|
||||
modified = L.GetGlobal("modified").String() == "true"
|
||||
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
|
||||
}
|
||||
|
||||
// Apply modification based on the result
|
||||
if updatedValue, ok := result.(string); ok {
|
||||
// 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)
|
||||
} else if nodeData, ok := result.(map[string]interface{}); ok {
|
||||
// If the result is a map, apply more complex updates
|
||||
logger.Debug("Updating node with complex data structure")
|
||||
updateNodeFromMap(node, nodeData)
|
||||
}
|
||||
|
||||
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
|
||||
if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode {
|
||||
// If we have an XML declaration, start with it
|
||||
|
File diff suppressed because it is too large
Load Diff
121
regression/regression_test.go
Normal file
121
regression/regression_test.go
Normal 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)
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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 & World < > " '</specialChars>
|
||||
<multiline>Line 1
|
||||
Line 2
|
||||
Line 3</multiline>
|
||||
</item>
|
||||
</testdata>
|
1252
testfiles/OutpostItems.xml
Normal file
1252
testfiles/OutpostItems.xml
Normal file
File diff suppressed because it is too large
Load Diff
1252
testfiles/OutpostItemsExpected.xml
Normal file
1252
testfiles/OutpostItemsExpected.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
<config><item><value>100</value></item></config>
|
Reference in New Issue
Block a user