azxeus Posted June 10, 2015 Share Posted June 10, 2015 (edited) I've come across a bug that doesn't allow any solider that has 'light em up' to use 'Hit and Run'.i've tested it to make sure it is down to these two together and not something else.Long War dev team will not support this bug because I changed the config file to enable the Hit and Run in training roulette when they had intentionally left it out.so I decided to have a look at the code myself in UE Explorer to see if I could see if anything was wrong.I found the functions BuildAbilities and BuildAbilie in XcomGame.XGAbilityTree which had the code to generate a bunch of perks but wasn't able to find any of them listed as 'Hit and Run' or even ReinforcedArmor. (EDIT: I read the nexus modding wiki page about perks and figured out that ablities and perks arent the same thing.. so i found where the perk gets it's name but not where it gets it's effects) Does anyone know where else I could be looking for code responsible for Hit and Run? Edited June 10, 2015 by azxeus Link to comment Share on other sites More sharing options...
tracktwo Posted June 10, 2015 Share Posted June 10, 2015 (edited) Try looking in the .int localization file for "Hit and Run" and from there you should be able to find the corresponding perk/ability enum value. Enum constants are resolved by the uscript compiler and replaced with byte constants, so in general you won't find references to those in the upk, you need to search for the actual value (which also gives lots of false positives). If you are lucky, the name will appear in functions though, and that can help point you in the right direction. If LW has reused a perk/ability for completely different uses than the original that still may not help much. For hit and run, I'd suggest looking for the places in the code where the effects are seen (eg taking a shot and not ending a turn) and working back from there. But without actually checking my first guess is that these two perks are actually "overlaid", actually sharing the same internal resources. Thats a pretty common technique due to the limited modding support. If that's the case there will just be a fundamental incompatibility here and you'd basically need to rewrite one of them to overlay some other perk. Since both of these do something similar under slightly different conditions - allowing a shot to not end the turn - this may be the case. But I have no actual evidence to back that up, you'll need to dig further to see. Good luck! Edited June 10, 2015 by tracktwo Link to comment Share on other sites More sharing options...
azxeus Posted June 10, 2015 Author Share Posted June 10, 2015 It thought this too but I was hoping it was might have been just a problem with priority.Thanks to your help (and my relentlessness) I managed to find the code responsible for costing turn actions and it looks like to me that my hope was right! function ApplyCost() { local XComUIBroadcastWorldMessage kBroadcastWorldMessage; // End:0x11 if(HasProperty(8)) { return; } // End:0x1C3 if(HasProperty(7)) { // End:0x8D if(m_kUnit.m_bDashing && !m_kUnit.RunAndGunPerkActive()) { m_kUnit.m_iMovesActionsPerformed += 2; } // End:0xAD else { ++ m_kUnit.m_iMovesActionsPerformed; } // End:0x1A0 if(m_kUnit.RunAndGunPerkActive()) { // End:0x13B if((m_kUnit.m_iMovesActionsPerformed == 1) && m_kUnit.m_bDashing) { m_kUnit.m_bRunAndGunUsedMove = true; } // End:0x1A0 else { // End:0x1A0 if(m_kUnit.m_iMovesActionsPerformed > 1) { m_kUnit.m_bRunAndGunUsedMove = true; m_kUnit.m_iMovesActionsPerformed = 1; } } } m_kUnit.BuildAbilities(true); } // End:0xDD7 else { // End:0x1D3 if(m_bReactionFire) { } // End:0xDD7 else { // End:0x218 if(m_kUnit.m_bFreeFireActionTaken) { ++ m_kUnit.m_iFireActionsPerformed; } // End:0xDD7 else { // End:0x35C if((((m_kUnit.GetCharacter().HasUpgrade(34) && (GetType()) == 7) && !m_kUnit.m_bRunAndGunActivated) && !m_kUnit.m_bCloseAndPersonalTaken) && VSize(m_kUnit.Location - XGAbility_Targeted(self).GetPrimaryTarget().Location) <= float(6 * 64)) { m_kUnit.m_bFreeFireActionTaken = true; m_kUnit.m_bCloseAndPersonalTaken = true; } // End:0xDD7 else { // End:0x415 if((m_kUnit.GetCharacter().HasUpgrade(126) || m_kUnit.GetCharType() == 21) && (GetType()) == 7) { m_kUnit.m_bFreeFireActionTaken ++ m_kUnit.m_iMovesActionsPerformed; } // End:0xDD7 else { // End:0x6D2 if((((m_kUnit.m_iMovesActionsPerformed == 0) && m_kUnit.GetCharacter().HasUpgrade(13)) && !XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.m_kAbilities.IsAbilityCoolingDown(m_kUnit, 47)) && XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.m_kAbilities.IsDoubleTapAllowedAbility(byte(GetType())) || (GetType()) == 12) { ++ m_kUnit.m_iMovesActionsPerformed; m_kUnit.m_aAbilitiesOnCooldown[m_kUnit.m_iNumAbilitiesOnCooldown].iCooldown = 3; m_kUnit.m_aAbilitiesOnCooldown[m_kUnit.m_iNumAbilitiesOnCooldown].iType = 47; ++ m_kUnit.m_iNumAbilitiesOnCooldown; m_kUnit.m_bDoubleTapActivated = true; m_kUnit.m_bBuildAbilityDataDirty = true; } // End:0xDD7 else { // End:0x763 if(m_kUnit.GetCharacter().HasUpgrade(121) && (GetType()) == 81) { m_kUnit.m_bFreeFireActionTaken = true; ++ m_kUnit.m_iMovesActionsPerformed; } // End:0xDD7 else { // End:0x82D if(((m_kUnit.m_kCharacter.m_kChar.aUpgrades[44] & 1) > 0) && (GetType()) == 17) { m_kUnit.m_bFreeFireActionTaken = true; ++ m_kUnit.m_iMovesActionsPerformed; m_kUnit.m_bBuildAbilityDataDirty = true; } // End:0xDD7 else { // End:0x8E5 if(m_kUnit.GetCharacter().HasUpgrade(162) && (((GetType()) == 29) || (GetType()) == 25) || (GetType()) == 26) { m_kUnit.m_bFreeFireActionTaken = true; m_kUnit.m_bBuildAbilityDataDirty = true; } // End:0xDD7 else { // End:0x9FF if((((XGAbility_Targeted(self) != none) && !XGAbility_Targeted(self).m_kWeapon.HasProperty(2)) && (m_kUnit.m_kCharacter.m_kChar.aUpgrades[58] & 1) > 0) && (GetType()) == 33) { m_kUnit.m_bFreeFireActionTaken = true; ++ m_kUnit.m_iMovesActionsPerformed; m_kUnit.m_bBuildAbilityDataDirty = true; } // End:0xDD7 else { // End:0xDB7 if(m_kUnit.GetCharacter().HasUpgrade(167)) { // End:0xDB7 if((GetType()) == 7) { // End:0xDB7 if(!m_kUnit.m_bRunAndGunActivated) { // End:0xDB7 if(!m_kUnit.m_bFreeFireActionTaken) { // End:0xDB7 if(!m_kUnit.m_bCloseAndPersonalTaken) { // End:0xDB7 if(!XGAbility_Targeted(self).GetPrimaryTarget().IsFlying()) { // End:0xDB7 if((!XGAbility_Targeted(self).GetPrimaryTarget().IsInCover() || XGAbility_Targeted(self).GetPrimaryTarget().IsFlankedByLoc(m_kUnit.Location)) || XGAbility_Targeted(self).GetPrimaryTarget().IsFlankedBy(m_kUnit)) { kBroadcastWorldMessage = XComPresentationLayer(XComPlayerController(WorldInfo.GetALocalPlayerController()).m_Pres).GetWorldMessenger().Message(XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.GetUnexpandedLocalizedMessageString(0), m_kUnit.Location, 3, m_kUnit.m_eTeamVisibilityFlags, class'XComUIBroadcastWorldMessage_UnexpandedLocalizedString'); // End:0xD72 if(kBroadcastWorldMessage != none) { XComUIBroadcastWorldMessage_UnexpandedLocalizedString(kBroadcastWorldMessage).Init_UnexpandedLocalizedString(0, m_kUnit.Location, 3, m_kUnit.m_eTeamVisibilityFlags); } m_kUnit.m_bFreeFireActionTaken = true; m_kUnit.m_bCloseAndPersonalTaken = true; } // End:0xDD7 else { } } } } } } ++ m_kUnit.m_iFireActionsPerformed; } } } } } } } } } } } // End:0xFE8 if(m_kUnit.IsAffectedByAbility(92)) { // End:0xFE8 if(m_kUnit.SetStealthCharges(m_kUnit.GetStealthCharges() - 1)) { kBroadcastWorldMessage = XComPresentationLayer(XComPlayerController(WorldInfo.GetALocalPlayerController()).m_Pres).GetWorldMessenger().Message(XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.GetUnexpandedLocalizedMessageString(31), m_kUnit.Location, 3, m_kUnit.m_eTeamVisibilityFlags, class'XComUIBroadcastWorldMessage_UnexpandedLocalizedString'); // End:0xFE8 if(kBroadcastWorldMessage != none) { XComUIBroadcastWorldMessage_UnexpandedLocalizedString(kBroadcastWorldMessage).Init_UnexpandedLocalizedString(31, m_kUnit.Location, 3, m_kUnit.m_eTeamVisibilityFlags); } } } // End:0x1078 if((((((GetType()) == 22) || (GetType()) == 27) || (GetType()) == 70) || (GetType()) == 33) || (GetType()) == 72) { m_kUnit.SetUsedAbility(GetType()); EndTurnCheck(); } // End:0x10FF if((((GetType()) != 70) && (GetType()) != 27) && (GetType()) != 1) { // End:0x10FF if(!m_bReactionFire) { // End:0x10FF if(m_kUnit.IsPoisoned()) { m_kUnit.TakePoisonDamage(); } } } return; } (sorry about the horrible white lines, no idea how to fix that x.x)at line 72 it checks for upgrade 126 which is light em upthen at line 132 it checks for 167 which is hit and run.it seems to me that if it finds light em up it ends the if statement and doesn't check for hit and run.I haven't exactly figure out how to fix it yet though.. ideas would be appreciated Link to comment Share on other sites More sharing options...
tracktwo Posted June 11, 2015 Share Posted June 11, 2015 (edited) OK cool, I see. Yeah so it looks like it just checks for LeU first, and if it matches that it doesn't enter the logic for H&R at all, which is in an else branch. You can probably move the H&R logic up above the check for LeU and that might get what you're looking for, but the interactions might be not exactly as you expect and there might be other side-effects to this. It's a complex function. H&R applies the "free shot" while LeU is basically implemented as a move action. I am not entirely familiar with how the game computes remaining actions, I suppose by looking at the totals of m_iFireActionsPerformed, m_iMovesActionsPerformed, and the free shot flag. Note this is a very complex function with a lot of control flow. You definitely do not want to be manually patching up jump offsets in writing a patch for this by hand, so I suggest starting with WGhost's hex2pseudocode utility to get a starting patch file and then move the logic around from there. Edited June 11, 2015 by tracktwo Link to comment Share on other sites More sharing options...
azxeus Posted June 11, 2015 Author Share Posted June 11, 2015 okay so it seems i don't get this hex programming marlarky I tried just replacing the code for the else's which are all 06D70D i just substituted them with 0B0B0B which means literally nothing so now everything in that function is a if statement on equal terms and i haven't moved any bytes around or anything.i checked to make sure all the code looks fine in UE Explorer which it does, loaded up the game to test it out and now everything costs 2 moves..i'm really stumped Link to comment Share on other sites More sharing options...
azxeus Posted June 11, 2015 Author Share Posted June 11, 2015 (edited) okay so it seems those aren't else branches at all which would explain why some of them have nothing in them, swapping the order does nothing LeU still gets priority somehow..i'm guessing that 06D70D is some kind of flag and/or a pointer to ++ m_kUnit.m_iFireActionsPerformed;but i honestly don't know where to go from here (EDIT: that doesn't explain why even a single move costs 2 actions when that code isn't present though.. it's not even at the movement part.. man this is confusing) (EDIT2: nevermind i was being silly and swapped it with the wrong one)AND IT WORKS!though i wonder why just swapping them around doesn't make H&R stop LeU from working but i'm not complaining at all Edited June 11, 2015 by azxeus Link to comment Share on other sites More sharing options...
azxeus Posted June 11, 2015 Author Share Posted June 11, 2015 (edited) okay, now what happens (quite weirdly) is that if i take my H&R first, i can't use LeU that turn but if i use LeU first I can H&R on my 2nd shot and then move or go for a 3rd.. any ideas why?i would have assumed it would be the other way around if anything(EDIT: I think i know why.. I'm guessing that it's seeing i have H&R first and then exiting it early because m_bFreeFireActionTaken is set to true i don't think there's a good way around that) Edited June 11, 2015 by azxeus Link to comment Share on other sites More sharing options...
tracktwo Posted June 11, 2015 Share Posted June 11, 2015 (edited) Didn't work the first few times, and you sat there looking at the screen scratching your head and swearing at the code, and then just when you're about to give up you figure it out and got it mostly working, just enough to keep going? Welcome to XCom modding, Commander :smile: More seriously, congrats. I think I see why it isn't working as you expect, though: there is a check before the large else that contains the LeU code that says: if (m_kUnit.m_bFreeFireActionTaken) ++m_kUnit.m_iFireActionsPerformed; This basically says once you've taken your free shot all other shots are real turn-enders, regardless of perks. You'd need to adjust this, but the simple change to just remove it will likely break all sorts of other stuff giving extra shots to other unrelated perks. You might try changing it to check that the free fire action was taken and either you don't have LeU or you've already taken a move (m_iMovesActionsPerformed > 0). About the 06D70D thing - that's basically JMP 0D67, an unconditional jump to offset 0xD67. UScript doesn't have "elses", bascially the only control flow it has are conditional (07) and unconditional (06) jumps. If/else is implemented by an 07 <offset> <condition>, which is "jump if not". If the condition is true, it'll keep executing the code below, otherwise it'll jump. To implement an "else" branch, you basically do this: 07 <offset> <condition> <my then block> 06 <first instruction after else> <my else block> <code after else is here> In other words, the "if" part jumps to the else if the test is false, and the end of the then block unconditionally jumps over the else code. Hopefully that makes sense. All control flow in UScript is built sort of like this, e.g. while loops are just ifs that jump back to the top at the bottom of the loop. UE can detect the common patterns and reconstruct the high-level control flow back into ifs and whiles. Hand written patches can have wacky control flow full of gotos (which is all 06 really is) that will confuse UE and it'll look strange. Also remember another oddity about uscript is all offsets are relative to the start of the function, not the current program counter. If you're familiar with most modern computing architectures, that's a bit weird, and makes moving code around a huge pain. Edited June 11, 2015 by tracktwo Link to comment Share on other sites More sharing options...
azxeus Posted June 11, 2015 Author Share Posted June 11, 2015 thanks, you're a big help, even though I figured out most of it myself, the fact I had someone replying here kept me going also that's exactly how I expect it to behave butit isn't.. if you use a LeU shot first you can take a H&R after but if you H&R first then the next shot ends the turn Link to comment Share on other sites More sharing options...
tracktwo Posted June 11, 2015 Share Posted June 11, 2015 (edited) Glad to help! Again I think that if is the one that is causing the trouble. Since the free shot flag is true after h&r it will just increment the shots taken counter and not enter any other logic, and that will leave no actions remaining even though they had LeU. You probably just need to change that if to also test if you have the leu perk, whether or not you've taken a move won't actually matter. Logic operations are tricky. I forget the opcode for && but it looks like this: AND <expression1> 18 <offset> <expression 2> 16 I think it's 18, but might be misremembering. The offset is the number of bytes to skip over expression 2 and is used for short circuiting the condition (if the first one is false, the result is always false and it doesn't even need to evaluate the 2nd one). You can write this by wrapping the 2nd expression in parentheses and using the special anonymous label syntax in upkutils to compute the correct offset for you. I'm on my phone and can't look up the exact syntax here :) Edited June 11, 2015 by tracktwo Link to comment Share on other sites More sharing options...
Recommended Posts