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 @@
-
+