Most of the games need at some point to introduce some randomness in their world. In Ultima III's case, this is implemented as a routine (found at offset 0x514B in EXODUS.COM) that I named u3_rand. Strangely, the character's creation process doesn't need it. As a matter of fact ULTIMA.COM and BOOTUP.COM only uses u3_rand for sound effects.
Which leads us to EXODUS.COM. Here is a list, I hope is exhaustive, of the different calls to u3_rand:
- position of the random explosions in Castle Exodus
- when steal action fails, determine if this will alert the guards
- starting position of the whirlpool in Ambrosia
- creatures spawning (lots of calls)
- dragons and pirates shoot on map
- move errand creatures
- Dexterity test (called by several other routines)
- Player's ship cannon shoot
- magical attack on all enemies (called by Repond, Noxum, Dag Mentar, Zxkuqyb, the unnamed spell, Pontori)
- set random position in dungeon
- spells: Repond, Mittar, Dag Acron, Pontori, Appar Unem, Sanctu, Sanctu Mani, Surmandum
- Move whirlpool
- Determination of a chest's content
- Determine if chest is trapped, the trap's type, and the damage inflicted by it.
- All party damage (called by other routines)
- Random encounter in dungeon and enemy's type
- In combat, determine the number of enemies, their characteristics
- Player's hit resolution in combat
- Dragon's fireball during combat
- Enemy moves during combat
- Enemy's magical attack]
- Player's parade during combat
- Player's damage when hit
- Poison attack
- Pilfer's determination
- Gremlin's event in dungeon
And I thinks that's all.
So how does it work ? Well, basically, there is a 128 bit-long, or 16 byte-long, seed which is updated on each u3_rnd call. It is stored in big-endian. Here is my conversion in C language of the algorithm (seed_len is 16):
void update_seed(unsigned char seed[], int seed_len) { //-- -- int carry = 0; for(int i = seed_len - 1; i > 0; i --) { unsigned a = seed[i] + seed[i - 1] + carry; seed[i - 1] = a & 0xff; carry = (a & 0x100) >> 8; }//end for //-- seed_128 += 1 -- for(int i = seed_len - 1; i >= 0; i --) { seed[i] ++; if(seed[i]) break; }//end for }
After the seed is updated, u3_rnd applies a modulo to its first byte, seed[0], and returns the result in register DL. The modulo parameter is passed in register DH.
Funny fact Ultima IV uses the same algorithm but does not do the modulo. This way, the caller can have access to 0~0xff values while in Ultima III's case, the returned value cannot exceed 0xfe. This is due to the fact that the maximum modulo parameter is 0xff.
I forgot to say that the seed is "randomized" with the return values from get_date and get_time system calls. For test purpose, you can initialize it 0s, it still works.
That's all for today.
No comments:
Post a Comment