wghost81 Posted July 10, 2015 Share Posted July 10, 2015 (edited) So, I decided to join an army of people already looking into this matter. :smile: That's what I found so far. Base hit and crit chances are calculated by native functions. No calls to these are present anywhere in the unrealscript code, so we can only assume that all the values are calculated automatically on Init() call, which also happens to be a native function. Luckily, hit chance is not accessed directly, but with GetHitChance() call, which effectively does nothing but returns m_iHitChance value in vanilla. LW adds its own hit chance modifiers by temporarily increasing shooter's offence and then calling CalcHitChance() to recalculate hit chance. All this happens inside GetHitChance(). I've put a lot of log output into this function and it shows that before this additional CalcHitChance() call all the values are correct (except for vanilla bugs, of course), but after this call hit chance just gets crazy. This getter function is used in many places and accessed at least three times for one shot and each time with each new CalcHitChance() call hit chance changes. Although it seems to return consistent values on normal shots, it gets completely crazy on OW shots. This is probably the reason for why it went unnoticed for so long. To make my experiment clean I switched to vanilla and put all those debug messages into vanilla GetHitChance() function. Here are some results. All the bugs I've seen so far seem to be related to flanking. Flanking unit is not considered being in cover, although m_iCurrentCoverValue and m_iBaseCoverValue are not recalculated and still hold non-zero cover values. And units which are flanked in smoke get their defense decreased by full m_iCurrentCoverValue, when it should only be decreased by m_iBaseCoverValue. m_iBaseCoverValue accounts for cover, provided by objects, and for flying bonus. It does not account for innate defense bonus. m_iCurrentCoverValue holds total cover value with all the bonuses, like from smoke and flying. Units fleeing from OW fire are not considered being in cover and never considered being flanked. m_bHasFlank and all the flanking related test are always false for them. They are considered exposed instead (m_bHasOpenTarget == true), but if they flee from flanked cover m_iBaseCoverValue and m_iCurrentCoverValue still hold that cover bonuses. Let's consider battlefield situation. Suppose, we have Rookie Alice (acc = 65), flanking Sectoid in low cover under smoke. For normal shot this Sectoid is considered being flanked, so all cover bonuses, including smoke, are subtracted, resulting in 65% chance to hit. 50% crit bonus is also added. If our crazy Rookie Alice chooses to OW this Sectoid instead and Sectoid then chooses to flee, he will no longer be flanked and will get all his cover bonuses back, including bonus from cover, which will result in 0.7*(65-20-20)=17% chance to hit. Now suppose that our Rookie Alice decides to just fall asleep in her flanking position, but her teammate, Rookie Bob, can only make it that far and can't flank nasty Sectoid. His chance to hit is 65-20-20=25%, which is awfully bad. But Rookie Bob is wise and he knows that flanked Sectoid will always run, so instead of shooting he choses to OW his target. While Sectoid flees from sleeping Rookie Alice, he is considered not being in cover and m_iBaseCoverValue and m_iCurrentCoverValue are properly decreased. I've found out that if OWing soldier is not flanking the target (i.e. it is not flanked at all, or is flanked by someone else), target's cover values are properly cleared, but if OWing soldier is also the one who is flanking the target, these values are not cleared. So, after all this crazy math our clever Rookie Bob gets 65-20=45%, which is better than his initial chance. He still misses, but hey, at least he tried! Unfortunately, stupid Sectoid moves to flank our Rookie Alice, who is apparently dreaming about her Wonderland, and fires a killing shot without even bothering to think about all the math. End of the story. :smile: As fun as this all is, so far I see at least two bugs:1. Flanking a unit in smoke results in all the defense bonuses, including smoke bonuses, being negated. This can be solved by checking for m_bHasFlank and subtracting 20 for smoke and additional 20 for dense smoke from m_iHitChance. This can only be done in GetHitChance() and must be done only once.2. OWing flanked unit by the flanker (crazy, but still) results in cover bonus being applied on OW. This can be fixed by checking for if the target is moving and not flying and has non-zero m_iBaseCoverValue. I made a little patch for vanilla which does this and outputs lot (and I mean LOT) of values to launch.log file: http://pastebin.com/W55ZrqC9 Apply with PatcherGUI 7.3. If you use this patch with LW, you'll loose all LW specific aim bonuses/penalties. After you witness a shot, open the log and search for something like this (keywords are "DevOnline: XGAbility_Targeted"): [0022.61] DevOnline: XGAbility_Targeted: m_kUnit = XGUnit_0 [0022.61] DevOnline: XGAbility_Targeted: m_kUnit.SafeGetCharacterName() = Rk. Johnson [0022.61] DevOnline: XGAbility_Targeted: m_bReactionFire = True [0022.61] DevOnline: XGAbility_Targeted: m_bHasFlank = False [0022.61] DevOnline: XGAbility_Targeted: m_bHasOpenTarget = True [0022.61] DevOnline: XGAbility_Targeted: m_bHasHeightAdvantage = False [0022.61] DevOnline: XGAbility_Targeted: m_fDistanceToTarget = 7.8169 [0022.61] DevOnline: XGAbility_Targeted: m_iHitChance = 35 [0022.61] DevOnline: XGAbility_Targeted: m_iCriticalChance = 0 [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget() = XGUnit_3 [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().SafeGetCharacterName() = Sectoid [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_bInSmokeBomb = True [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_bInDenseSmoke = True [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_iCurrentCoverValue = 40 [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_iBaseCoverValue = 0 [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().IsFlankedBy(m_kUnit) = False [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().IsMoving() = True [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_bCachedIsInCover = False [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().IsInCover() = False [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().LastCoverState = eCS_None [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_eCoverState = eCS_None [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().LastCoverTypeUsed = eECTTU_Zero [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_bDashing = False [0022.61] DevOnline: XGAbility_Targeted: GetPrimaryTarget().m_bSprinting = False [0022.61] DevOnline: XGAbility_Targeted: m_iHitChance (adjusted) = 35 [0022.61] DevOnline: XGAbility_Targeted: m_iCriticalChance (adjusted) = 0I also used this mod to observe final to hit values in game. Handy little mod it is. :smile: Since I can't possibly remember and test all the cases, I suggest anyone interested to joint the hunt for OW (and non-OW) bugs. Hundreds of guinea sectoids and sleeping rookies await you, Commander! :smile: UPD3: final version for LW: http://pastebin.com/MHJA8Zad Edited July 13, 2015 by wghost81 Link to comment Share on other sites More sharing options...
wghost81 Posted July 10, 2015 Author Share Posted July 10, 2015 And BTW, one good way to test it is to use dev console and tactical skirmish. Make yourself any squad, spawn any enemy and have fun. :) Link to comment Share on other sites More sharing options...
Amineri Posted July 10, 2015 Share Posted July 10, 2015 (edited) This can be solved by checking for m_bHasFlank and subtracting 20 for smoke and additional 20 for dense smoke from m_iHitChance. This can only be done in GetHitChance() and must be done only once. Unfortunately, this turns out to not be a very good solution. There are two problematic use-cases here : 1) GetHitChance is called multiple times, for both displaying UI hit chances as well as actually rolling for hit, so making sure the value is subtracted only once is a challenge. 2) Even if it could be subtracted once, clamping issues could make the resultant value incorrect. Here's an example : Sniper with 97 aim and height advantage (+20) and SCOPE (+8 ) shooting at exposed unit in regular smoke volume (-20). The proper to-hit should be 97 + 20 + 8 - 20 = 105, clamped to 100. However, because the native hit chance code clamps to 100, the native code would compute the 97 + 20 + 8 = 125 chance to hit, then clamp the value to 100. Then our extra -20 subtracting for smoke would happen, resulting in a to-hit chance of 80%. This clamping issue is the reason that we ended up temporarily adjusting the soldier aim stat and then calling the native GetHitChance and CalcHitModFromPerks functions to re-compute the hit-chance. ------------ From my earlier (non reaction) experiments, GetHitChance factors in almost everything (including perks), and clamps the result to be non-negative, but does not clamp on the upper end. CalcHitModFromPerks appears to only apply the range modifiers, after which the result is clamped to the (1, 100) range. This is the source of the vanilla bug when shooting at very high-defense targets from close range. For example, in vanilla, a rookie (65 aim) with close range bonus of +15 aim, shooting at a unit hunkered in full cover (-80) should have the minimum 1% to-hit chance. However, the call sequence appears to go :1) CalcHitChance uses 65 -80 = -15, clamped to 0, returns 02) CalcHitModFromPerks ingests the 0, adds +15 range modifier, clamps results to (1, 100), returns 15. In practice, I've found that the situation above does indeed return a 15% chance to hit. Edited July 10, 2015 by Amineri Link to comment Share on other sites More sharing options...
wghost81 Posted July 10, 2015 Author Share Posted July 10, 2015 (edited) If you look at the example script, you'll see that I solved multiple calls problem. :wink: m_bSave fits our need here perfectly. Clamping is another issue, though. I feel like we might end up recreating all the native code to finally fix all the bugs. :smile: But since additional call is affecting OW only (as far as I can see) and nothing is subtracted for OW shots (only added) clamping should not be the problem. And for regular shots we can still use that additional call. Edited July 10, 2015 by wghost81 Link to comment Share on other sites More sharing options...
wghost81 Posted July 10, 2015 Author Share Posted July 10, 2015 Here's LW version with lots of debug info and cover bonus removed for OW and suppression shots: http://pastebin.com/m686AF86 At least I believe it is. :) Link to comment Share on other sites More sharing options...
wghost81 Posted July 10, 2015 Author Share Posted July 10, 2015 And some more info on how thinks work. CalcHitChance was safe to use after all. :) I'm still getting slight variations in output values, but they are small. Turns out, I wasn't accounting for dashing units as in vanilla aliens never dash, but in LW they do it a lot. The way I see it, hit chance is calculated when OW conditions are met, i.e. when target moves inside LOS of OWing unit. So for targets that already are inside LOS this will be approximately a tile adjacent to starting position in the direction in which the target is moving and for targets that are entering LOS this will be the same tile but for position, when target has entered LOS. Moving units are not considered being in cover (!) nor they considered being flanked. But for some reason (probably because of how code optimization works) cover bonuses are not updated properly. If target moves away from cover into the open, those bonuses get cleared properly. But in some other cases, involving moving along the cover and from cover into the open (or from the open into cover) those bonuses can still hold some previously calculated values, resulting in hit chance miscalculation. In my opinion those values are too inconsistent and can't be considered as intentional cover bonuses. But that's just me. :) Link to comment Share on other sites More sharing options...
wghost81 Posted July 11, 2015 Author Share Posted July 11, 2015 (edited) Final mod version for LW: http://pastebin.com/nPnuLfiM Auto-opportunist for Gunners is removed, because it feels inconsistent now. Rules just became simpler. :smile: Bugs with smoke and flying units defense bonuses are also fixed. Edited July 11, 2015 by wghost81 Link to comment Share on other sites More sharing options...
Amineri Posted July 12, 2015 Share Posted July 12, 2015 (edited) The way I see it, hit chance is calculated when OW conditions are met, i.e. when target moves inside LOS of OWing unit. So for targets that already are inside LOS this will be approximately a tile adjacent to starting position in the direction in which the target is moving and for targets that are entering LOS this will be the same tile but for position, when target has entered LOS. What happens is that the path arclength is logically segmented into segments that are 96.0 unreal units long (1 tile). This all happens in XGAction_Move_Direct.state'Executing'.ReactionProcessing. iCurrentInterval = int(m_kPawn.m_fDistanceMovedAlongPath / 96.0); iIntervalToProcess = iCurrentInterval - m_kPawn.m_iLastInterval_MoveReactionProcessing; if(iIntervalToProcess > 0) This is fairly safe even on slower machines, because there is only a problem if the machine is so slow that the unit moves more than 2 tiles worth of movement during a single processing loop of XGAction_Move_Direct. Each such interval calls XGUnit.ApplyReactionCost, which will trigger reaction fire from any enemy overwatching unit that has LOS and is in range (as defined by the weapon's iReactionRange stat). There is also one additional exception that can prevent reaction fire : if(!m_kUnit.SafeFromReactionFireMinDistToDest()) This prevents ApplyReactionCost from triggering if the unit is within 96.0 units (1 tile) of it's final destination, as measured along the path arclength. Note that it's not the FIRST 1 tile of movement that is free, but the LAST 1 tile of movement that is free. Of course, if a unit only moves 1 tile, this is identical. Here's a demonstration showing that it's the LAST 1 tile of movement that is free : http://i.imgur.com/TJclAiw.png?2 The unit moving was able to move 1 unit beyond the full cover corner into low cover because the last tile's worth of movement didn't trigger reaction fire from the OWing MEC (and I used the dev console to arrange this situation...). Edited July 12, 2015 by Amineri Link to comment Share on other sites More sharing options...
Kevlin0069 Posted July 13, 2015 Share Posted July 13, 2015 After applying this patch, I noticed something strange that someone should take a look at, namely the interaction between the changes in this patch and the Arc Thrower's Stun ability in Long War B15e. I had a Medic with Packmaster parked next to a normal Sectoid, ready to stun it. When I clicked on the Stun ability to bring up the stun chance, it showed 57% on the tooltip, which is normal with advanced Arc Thrower project completed, but it showed like 34% next to the Sectoid's head. When I hit ESC to back out of the stun, then went to use it again, the new tooltip showed the previous value next to the alien, while the new value next to the alien was much much lower than before. After going in and out of the stun ability a few times, eventually, both the tooltip and the number next to the alien was 0%. After switching to another soldier and back, the numbers had reset to their original values, and going in and out of the stun ability made them go down again, eventually back to 0% on both. I'm not sure if this is relevant, but after switching soldiers again, I went back to the Medic and stunned the little guy on the first try. Link to comment Share on other sites More sharing options...
wghost81 Posted July 13, 2015 Author Share Posted July 13, 2015 Try this version: http://pastebin.com/MHJA8Zad Link to comment Share on other sites More sharing options...
Recommended Posts