diff --git a/FF1Lib/FF1Lib.csproj b/FF1Lib/FF1Lib.csproj index 4eee7f15e..3aafeedca 100644 --- a/FF1Lib/FF1Lib.csproj +++ b/FF1Lib/FF1Lib.csproj @@ -55,7 +55,7 @@ - + diff --git a/FF1Lib/FF1Rom.cs b/FF1Lib/FF1Rom.cs index 8771b0f1c..7b1f9f4bf 100644 --- a/FF1Lib/FF1Rom.cs +++ b/FF1Lib/FF1Rom.cs @@ -15,7 +15,7 @@ namespace FF1Lib public partial class FF1Rom : NesRom { public const int RngOffset = 0x7F100; - public const int BattleRngOffset = 0x7FCF1; + public const int BattleRngLutOffset = 0x7FCF1; public const int RngSize = 256; public const int LevelRequirementsOffset = 0x6CC81; @@ -106,6 +106,7 @@ public void Randomize(Blob seed, Flags flags, Preferences preferences) FixWarpBug(); // The warp bug must be fixed for magic level shuffle and spellcrafter SeparateUnrunnables(); UpdateDialogs(); + ReplaceBattleRNG(rng); flags = Flags.ConvertAllTriState(flags, rng); @@ -1074,10 +1075,10 @@ public void FixMissingBattleRngEntry() { // of the 256 entries in the battle RNG table, the 98th entry (index 97) is a duplicate '00' where '95' hex / 149 int is absent. // you could arbitrarily choose the other '00', the 111th entry (index 110), to replace instead - var battleRng = Get(BattleRngOffset, RngSize).Chunk(1).ToList(); + var battleRng = Get(BattleRngLutOffset, RngSize).Chunk(1).ToList(); battleRng[97] = Blob.FromHex("95"); - Put(BattleRngOffset, battleRng.SelectMany(blob => blob.ToBytes()).ToArray()); + Put(BattleRngLutOffset, battleRng.SelectMany(blob => blob.ToBytes()).ToArray()); } public void ShuffleRng(MT19337 rng) @@ -1087,10 +1088,10 @@ public void ShuffleRng(MT19337 rng) Put(RngOffset, rngTable.SelectMany(blob => blob.ToBytes()).ToArray()); - var battleRng = Get(BattleRngOffset, RngSize).Chunk(1).ToList(); + var battleRng = Get(BattleRngLutOffset, RngSize).Chunk(1).ToList(); battleRng.Shuffle(rng); - Put(BattleRngOffset, battleRng.SelectMany(blob => blob.ToBytes()).ToArray()); + Put(BattleRngLutOffset, battleRng.SelectMany(blob => blob.ToBytes()).ToArray()); } } diff --git a/FF1Lib/Hacks.cs b/FF1Lib/Hacks.cs index 025feee68..d2e7a6685 100644 --- a/FF1Lib/Hacks.cs +++ b/FF1Lib/Hacks.cs @@ -42,6 +42,61 @@ public partial class FF1Rom : NesRom public const string BattleBoxUndrawFrames = "04"; // 2/3 normal (Must divide 12) public const string BattleBoxUndrawRows = "03"; + public const int SmokeSpriteReplaceStart = 0x317E9; + public const int SmokeSpriteReplaceEnd = 0x31A2C; + public const int Bank0BRandAXReplaceStart = 0x2DF1D; + public const int Bank0BRandAXReplaceEnd = 0x2DF3B; + public const int Bank0CRandAXReplaceStart = 0x32E5D; + public const int Bank0CRandAXReplaceEnd = 0x32E7B; + public const int CombatBoxReplaceStart = 0x320A2; + public const int CombatBoxReplaceEnd = 0x320CD; + + public const int BattleRngCodeOffset = 0x7FCE7; + + public void ReplaceBattleRNG(MT19337 rng) + { + // This moves some temporary memory locations used to draw the smoke effect sprites + // in battle to the same locations used to store attacker stats. These can overwrite + // each other without issue, and it frees up some space for a few bytes of RNG state. + var smokeSpriteCode = Get(SmokeSpriteReplaceStart, SmokeSpriteReplaceEnd - SmokeSpriteReplaceStart); + + // We only need 4 bytes, and moving the others seems to mess some stuff up. + smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68AF }), Blob.FromUShorts(new ushort[] { 0x686C })); + smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B0 }), Blob.FromUShorts(new ushort[] { 0x686D })); + smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B1 }), Blob.FromUShorts(new ushort[] { 0x686E })); + smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B2 }), Blob.FromUShorts(new ushort[] { 0x686F })); + //smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B3 }), Blob.FromUShorts(new ushort[] { 0x6870 })); + //smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B4 }), Blob.FromUShorts(new ushort[] { 0x6871 })); + //smokeSpriteCode.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B5 }), Blob.FromUShorts(new ushort[] { 0x6872 })); + + Put(SmokeSpriteReplaceStart, smokeSpriteCode); + + // RandAX uses these locations, too. + var randAX = Get(Bank0BRandAXReplaceStart, Bank0BRandAXReplaceEnd - Bank0BRandAXReplaceStart); + randAX.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68AF }), Blob.FromUShorts(new ushort[] { 0x686C })); + randAX.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B0 }), Blob.FromUShorts(new ushort[] { 0x686D })); + Put(Bank0BRandAXReplaceStart, randAX); + + // There are two copies of RandAX in different banks, so we have to do this again. + randAX = Get(Bank0CRandAXReplaceStart, Bank0CRandAXReplaceEnd - Bank0CRandAXReplaceStart); + randAX.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68AF }), Blob.FromUShorts(new ushort[] { 0x686C })); + randAX.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B0 }), Blob.FromUShorts(new ushort[] { 0x686D })); + Put(Bank0CRandAXReplaceStart, randAX); + + // One more usage of this space. + var combatBox = Get(CombatBoxReplaceStart, CombatBoxReplaceEnd - CombatBoxReplaceStart); + combatBox.ReplaceInPlace(Blob.FromUShorts(new ushort[] { 0x68B1 }), Blob.FromUShorts(new ushort[] { 0x686E })); + Put(CombatBoxReplaceStart, combatBox); + + // Now the good stuff. Write LCG.asm in place of BattleRNG. + Put(BattleRngCodeOffset, Blob.FromHex("8A48ADAF68AE51FD2059FD186D55FD9002E8188DAF688610ADB068AE52FD2059FD186D56FD9002E81865109002E8188DB0688610ADB168AE53FD2059FD186D57FD9002E81865109002E8188DB1688610ADB268AE54FD2059FD186D58FD186510188DB26868AAADB26860054B56AC0000000085118612A208A9008513461190031865126A6613CAD0F3AAA51360")); + + // Choose a random odd number for c in the LCG. + uint c = rng.Next(); + c |= 0x00000001; + Put(BattleRngCodeOffset + 0x6E, Blob.FromUInts(new[] { c })); + } + // Required for npc quest item randomizing public void PermanentCaravan() { diff --git a/FF1Lib/asm/LCG.asm b/FF1Lib/asm/LCG.asm new file mode 100644 index 000000000..8aa6485d3 --- /dev/null +++ b/FF1Lib/asm/LCG.asm @@ -0,0 +1,122 @@ +; Linear Congruential Generator (better random number generator) +; https://en.wikipedia.org/wiki/Linear_congruential_generator +; This is a simple but effective RNG with a much longer period than FF1's. + +; A research paper on selecting good parameters for the multiplier: +; MATHEMATICS OF COMPUTATION +; Volume 68, Number 225, January 1999, Pages 249–260 +; S 0025-5718(99)00996-5 +; https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf +; We'll use a = 2891336453, or 0xAC564B05 from Table 4. +; Any odd integer will do for c, so we'll take a random value. + + + +* = $FCE7 +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Bank 0F, $FCE7 (BattleRNG) ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +battle_rng_state = $68AF +tmp = $10 + +BattleRNG: + TXA + PHA ; Push X onto the stack, because we'll clobber it + + LDA battle_rng_state ; Get the first byte of state + LDX battle_rng_a ; Get the first byte of m + JSR MultiplyXA ; Multiply state by m + CLC + ADC battle_rng_c ; Add c + BCC :+ + INX ; Increment high bits if necessary + CLC + : + STA battle_rng_state ; Store the low bits back to state + STX tmp ; Save the high bits for the next step + + LDA battle_rng_state + 1 ; Now do it again for the next byte of state + LDX battle_rng_a + 1 + JSR MultiplyXA + CLC + ADC battle_rng_c + 1 + BCC :+ + INX + CLC + : + ADC tmp ; Add the high bits from the previous step + BCC :+ + INX + CLC + : + STA battle_rng_state + 1 + STX tmp + + LDA battle_rng_state + 2 + LDX battle_rng_a + 2 + JSR MultiplyXA + CLC + ADC battle_rng_c + 2 + BCC :+ + INX + CLC + : + ADC tmp + BCC :+ + INX + CLC + : + STA battle_rng_state + 2 + STX tmp + + LDA battle_rng_state + 3 ; Last byte + LDX battle_rng_a + 3 + JSR MultiplyXA + CLC + ADC battle_rng_c + 3 + CLC ; No need to save the high bits, so just CLC + ADC tmp + CLC ; Just in case + STA battle_rng_state + 3 + + PLA + TAX ; Restore X from the stack + + LDA battle_rng_state + 3 ; We want to return the highest byte of state. + RTS + +battle_rng_a: + .BYTE $05, $4B, $56, $AC +battle_rng_c: + .BYTE $00, $00, $00, $00 ; (this will be replaced by the randomizer) + + + +; MultiplyXA copied from bank 0B +btltmp_multA = $11 +btltmp_multB = $12 +btltmp_multC = $13 + +MultiplyXA: + STA btltmp_multA ; store the values we'll be multiplying + STX btltmp_multB + LDX #$08 ; Use x as a loop counter. X=8 for 8 bits + + LDA #$00 ; A will be the high byte of the product + STA btltmp_multC ; multC will be the low byte + + ; For each bit in multA + @Loop: + LSR btltmp_multA ; shift out the low bit + BCC :+ + CLC ; if it was set, add multB to our product + ADC btltmp_multB + : ROR A ; then rotate down our product + ROR btltmp_multC + DEX + BNE @Loop + + TAX ; put high bits of product in X + LDA btltmp_multC ; put low bits in A + RTS diff --git a/FF1R/FF1R.csproj b/FF1R/FF1R.csproj index 252252f62..9e779d9ec 100644 --- a/FF1R/FF1R.csproj +++ b/FF1R/FF1R.csproj @@ -13,7 +13,7 @@ - + diff --git a/FFR.Common/FFR.Common.csproj b/FFR.Common/FFR.Common.csproj index c25273d28..ea9394a95 100644 --- a/FFR.Common/FFR.Common.csproj +++ b/FFR.Common/FFR.Common.csproj @@ -2,7 +2,7 @@ - +