parent
							
								
									7b0b243607
								
							
						
					
					
						commit
						6566dde8d0
					
				| 
						 | 
				
			
			@ -0,0 +1,255 @@
 | 
			
		|||
import asyncio
 | 
			
		||||
import json
 | 
			
		||||
import time
 | 
			
		||||
from asyncio import StreamReader, StreamWriter
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import Utils
 | 
			
		||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
 | 
			
		||||
    get_base_parser
 | 
			
		||||
 | 
			
		||||
SYSTEM_MESSAGE_ID = 0
 | 
			
		||||
 | 
			
		||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator then restart ff1_connector.lua"
 | 
			
		||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator make sure ff1_connector.lua is running"
 | 
			
		||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator then restart ff1_connector.lua"
 | 
			
		||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
 | 
			
		||||
CONNECTION_CONNECTED_STATUS = "Connected"
 | 
			
		||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FF1CommandProcessor(ClientCommandProcessor):
 | 
			
		||||
    def __init__(self, ctx: CommonContext):
 | 
			
		||||
        super().__init__(ctx)
 | 
			
		||||
 | 
			
		||||
    def _cmd_nes(self):
 | 
			
		||||
        """Check NES Connection State"""
 | 
			
		||||
        if isinstance(self.ctx, FF1Context):
 | 
			
		||||
            logger.info(f"NES Status: {self.ctx.nes_status}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FF1Context(CommonContext):
 | 
			
		||||
    def __init__(self, server_address, password):
 | 
			
		||||
        super().__init__(server_address, password)
 | 
			
		||||
        self.nes_streams: (StreamReader, StreamWriter) = None
 | 
			
		||||
        self.nes_sync_task = None
 | 
			
		||||
        self.messages = {}
 | 
			
		||||
        self.locations_array = None
 | 
			
		||||
        self.nes_status = CONNECTION_INITIAL_STATUS
 | 
			
		||||
        self.game = 'Final Fantasy'
 | 
			
		||||
        self.awaiting_rom = False
 | 
			
		||||
 | 
			
		||||
    command_processor = FF1CommandProcessor
 | 
			
		||||
 | 
			
		||||
    async def server_auth(self, password_requested: bool = False):
 | 
			
		||||
        if password_requested and not self.password:
 | 
			
		||||
            await super(FF1Context, self).server_auth(password_requested)
 | 
			
		||||
        if not self.auth:
 | 
			
		||||
            self.awaiting_rom = True
 | 
			
		||||
            logger.info('Awaiting connection to NES to get Player information')
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        await self.send_connect()
 | 
			
		||||
 | 
			
		||||
    def _set_message(self, msg: str, msg_id: int):
 | 
			
		||||
        self.messages[(time.time(), msg_id)] = msg
 | 
			
		||||
 | 
			
		||||
    def on_package(self, cmd: str, args: dict):
 | 
			
		||||
        if cmd == 'Connected':
 | 
			
		||||
            self.game = self.games.get(self.slot, None)
 | 
			
		||||
            asyncio.create_task(parse_locations(self.locations_array, self, True))
 | 
			
		||||
        elif cmd == 'Print':
 | 
			
		||||
            msg = args['text']
 | 
			
		||||
            if ': !' not in msg:
 | 
			
		||||
                self._set_message(msg, SYSTEM_MESSAGE_ID)
 | 
			
		||||
        elif cmd == "ReceivedItems":
 | 
			
		||||
            msg = f"Received {', '.join([self.item_name_getter(item.item) for item in args['items']])}"
 | 
			
		||||
            self._set_message(msg, SYSTEM_MESSAGE_ID)
 | 
			
		||||
        elif cmd == 'PrintJSON':
 | 
			
		||||
            print_type = args['type']
 | 
			
		||||
            item = args['item']
 | 
			
		||||
            receiving_player_id = args['receiving']
 | 
			
		||||
            receiving_player_name = self.player_names[receiving_player_id]
 | 
			
		||||
            sending_player_id = item.player
 | 
			
		||||
            sending_player_name = self.player_names[item.player]
 | 
			
		||||
            if print_type == 'Hint':
 | 
			
		||||
                msg = f"Hint: Your {self.item_name_getter(item.item)} is at" \
 | 
			
		||||
                      f" {self.player_names[item.player]}'s {self.location_name_getter(item.location)}"
 | 
			
		||||
                self._set_message(msg, item.item)
 | 
			
		||||
            elif print_type == 'ItemSend' and receiving_player_id != self.slot:
 | 
			
		||||
                if sending_player_id == self.slot:
 | 
			
		||||
                    if receiving_player_id == self.slot:
 | 
			
		||||
                        msg = f"You found your own {self.item_name_getter(item.item)}"
 | 
			
		||||
                    else:
 | 
			
		||||
                        msg = f"You sent {self.item_name_getter(item.item)} to {receiving_player_name}"
 | 
			
		||||
                else:
 | 
			
		||||
                    if receiving_player_id == sending_player_id:
 | 
			
		||||
                        msg = f"{sending_player_name} found their {self.item_name_getter(item.item)}"
 | 
			
		||||
                    else:
 | 
			
		||||
                        msg = f"{sending_player_name} sent {self.item_name_getter(item.item)} to " \
 | 
			
		||||
                              f"{receiving_player_name}"
 | 
			
		||||
                self._set_message(msg, item.item)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_payload(ctx: FF1Context):
 | 
			
		||||
    current_time = time.time()
 | 
			
		||||
    return json.dumps(
 | 
			
		||||
        {
 | 
			
		||||
            "items": [item.item for item in ctx.items_received],
 | 
			
		||||
            "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
 | 
			
		||||
                         if key[0] > current_time - 10}
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def parse_locations(locations_array: List[int], ctx: FF1Context, force: bool):
 | 
			
		||||
    if locations_array == ctx.locations_array and not force:
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        # print("New values")
 | 
			
		||||
        ctx.locations_array = locations_array
 | 
			
		||||
        locations_checked = []
 | 
			
		||||
        if len(locations_array) > 0xFE and locations_array[0xFE] & 0x02 != 0 and not ctx.finished_game:
 | 
			
		||||
            await ctx.send_msgs([
 | 
			
		||||
                {"cmd": "StatusUpdate",
 | 
			
		||||
                 "status": 30}
 | 
			
		||||
            ])
 | 
			
		||||
            ctx.finished_game = True
 | 
			
		||||
        for location in ctx.missing_locations:
 | 
			
		||||
            # index will be - 0x100 or 0x200
 | 
			
		||||
            index = location
 | 
			
		||||
            if location < 0x200:
 | 
			
		||||
                # Location is a chest
 | 
			
		||||
                index -= 0x100
 | 
			
		||||
                flag = 0x04
 | 
			
		||||
            else:
 | 
			
		||||
                # Location is an NPC
 | 
			
		||||
                index -= 0x200
 | 
			
		||||
                flag = 0x02
 | 
			
		||||
 | 
			
		||||
            # print(f"Location: {ctx.location_name_getter(location)}")
 | 
			
		||||
            # print(f"Index: {str(hex(index))}")
 | 
			
		||||
            # print(f"value: {locations_array[index] & flag != 0}")
 | 
			
		||||
            if locations_array[index] & flag != 0:
 | 
			
		||||
                locations_checked.append(location)
 | 
			
		||||
        if locations_checked:
 | 
			
		||||
            # print([ctx.location_name_getter(location) for location in locations_checked])
 | 
			
		||||
            await ctx.send_msgs([
 | 
			
		||||
                {"cmd": "LocationChecks",
 | 
			
		||||
                 "locations": locations_checked}
 | 
			
		||||
            ])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def nes_sync_task(ctx: FF1Context):
 | 
			
		||||
    logger.info("Starting nes connector. Use /nes for status information")
 | 
			
		||||
    while not ctx.exit_event.is_set():
 | 
			
		||||
        error_status = None
 | 
			
		||||
        if ctx.nes_streams:
 | 
			
		||||
            (reader, writer) = ctx.nes_streams
 | 
			
		||||
            msg = get_payload(ctx).encode()
 | 
			
		||||
            writer.write(msg)
 | 
			
		||||
            writer.write(b'\n')
 | 
			
		||||
            try:
 | 
			
		||||
                await asyncio.wait_for(writer.drain(), timeout=1.5)
 | 
			
		||||
                try:
 | 
			
		||||
                    # Data will return a dict with up to two fields:
 | 
			
		||||
                    # 1. A keepalive response of the Players Name (always)
 | 
			
		||||
                    # 2. An array representing the memory values of the locations area (if in game)
 | 
			
		||||
                    data = await asyncio.wait_for(reader.readline(), timeout=5)
 | 
			
		||||
                    data_decoded = json.loads(data.decode())
 | 
			
		||||
                    # print(data_decoded)
 | 
			
		||||
                    if ctx.game is not None and 'locations' in data_decoded:
 | 
			
		||||
                        # Not just a keep alive ping, parse
 | 
			
		||||
                        asyncio.create_task(parse_locations(data_decoded['locations'], ctx, False))
 | 
			
		||||
                    if not ctx.auth:
 | 
			
		||||
                        ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
 | 
			
		||||
                        if ctx.awaiting_rom:
 | 
			
		||||
                            await ctx.server_auth(False)
 | 
			
		||||
                except asyncio.TimeoutError:
 | 
			
		||||
                    logger.debug("Read Timed Out, Reconnecting")
 | 
			
		||||
                    error_status = CONNECTION_TIMING_OUT_STATUS
 | 
			
		||||
                    writer.close()
 | 
			
		||||
                    ctx.nes_streams = None
 | 
			
		||||
                except ConnectionResetError as e:
 | 
			
		||||
                    logger.debug("Read failed due to Connection Lost, Reconnecting")
 | 
			
		||||
                    error_status = CONNECTION_RESET_STATUS
 | 
			
		||||
                    writer.close()
 | 
			
		||||
                    ctx.nes_streams = None
 | 
			
		||||
            except TimeoutError:
 | 
			
		||||
                logger.debug("Connection Timed Out, Reconnecting")
 | 
			
		||||
                error_status = CONNECTION_TIMING_OUT_STATUS
 | 
			
		||||
                writer.close()
 | 
			
		||||
                ctx.nes_streams = None
 | 
			
		||||
            except ConnectionResetError:
 | 
			
		||||
                logger.debug("Connection Lost, Reconnecting")
 | 
			
		||||
                error_status = CONNECTION_RESET_STATUS
 | 
			
		||||
                writer.close()
 | 
			
		||||
                ctx.nes_streams = None
 | 
			
		||||
            if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
 | 
			
		||||
                if not error_status:
 | 
			
		||||
                    logger.info("Successfully Connected to NES")
 | 
			
		||||
                    ctx.nes_status = CONNECTION_CONNECTED_STATUS
 | 
			
		||||
                else:
 | 
			
		||||
                    ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
 | 
			
		||||
            elif error_status:
 | 
			
		||||
                ctx.nes_status = error_status
 | 
			
		||||
                logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
 | 
			
		||||
        else:
 | 
			
		||||
            try:
 | 
			
		||||
                logger.debug("Attempting to connect to NES")
 | 
			
		||||
                ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
 | 
			
		||||
                ctx.nes_status = CONNECTION_TENTATIVE_STATUS
 | 
			
		||||
            except TimeoutError:
 | 
			
		||||
                logger.debug("Connection Timed Out, Trying Again")
 | 
			
		||||
                ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
 | 
			
		||||
                continue
 | 
			
		||||
            except ConnectionRefusedError:
 | 
			
		||||
                logger.debug("Connection Refused, Trying Again")
 | 
			
		||||
                ctx.nes_status = CONNECTION_REFUSED_STATUS
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    # Text Mode to use !hint and such with games that have no text entry
 | 
			
		||||
    Utils.init_logging("FF1Client")
 | 
			
		||||
 | 
			
		||||
    async def main(args):
 | 
			
		||||
        ctx = FF1Context(args.connect, args.password)
 | 
			
		||||
        ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
 | 
			
		||||
        if gui_enabled:
 | 
			
		||||
            input_task = None
 | 
			
		||||
            from kvui import TextManager
 | 
			
		||||
            ctx.ui = TextManager(ctx)
 | 
			
		||||
            ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
 | 
			
		||||
        else:
 | 
			
		||||
            input_task = asyncio.create_task(console_loop(ctx), name="Input")
 | 
			
		||||
            ui_task = None
 | 
			
		||||
 | 
			
		||||
        ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
 | 
			
		||||
 | 
			
		||||
        await ctx.exit_event.wait()
 | 
			
		||||
        ctx.server_address = None
 | 
			
		||||
 | 
			
		||||
        await ctx.shutdown()
 | 
			
		||||
 | 
			
		||||
        if ctx.nes_sync_task:
 | 
			
		||||
            await ctx.nes_sync_task
 | 
			
		||||
 | 
			
		||||
        if ui_task:
 | 
			
		||||
            await ui_task
 | 
			
		||||
 | 
			
		||||
        if input_task:
 | 
			
		||||
            input_task.cancel()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    import colorama
 | 
			
		||||
 | 
			
		||||
    parser = get_base_parser()
 | 
			
		||||
    args, rest = parser.parse_known_args()
 | 
			
		||||
    colorama.init()
 | 
			
		||||
 | 
			
		||||
    loop = asyncio.get_event_loop()
 | 
			
		||||
    loop.run_until_complete(main(args))
 | 
			
		||||
    loop.close()
 | 
			
		||||
    colorama.deinit()
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
# Final Fantasy 1 (NES)
 | 
			
		||||
 | 
			
		||||
## Where is the settings page?
 | 
			
		||||
Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled entirely by the original randomzier.
 | 
			
		||||
You can find an exhaustive list of documented settings [here](https://finalfantasyrandomizer.com/)
 | 
			
		||||
 | 
			
		||||
## What does randomization do to this game?
 | 
			
		||||
A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory
 | 
			
		||||
and boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle
 | 
			
		||||
progression items and non-progression items into separate pools and then redistribute them to their respective 
 | 
			
		||||
locations. So ,for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal 
 | 
			
		||||
Pot or some armor. There are plenty of other things that can be randomized on our 
 | 
			
		||||
[main randomizer site](https://finalfantasyrandomizer.com/)
 | 
			
		||||
 | 
			
		||||
Some features are not currently supported by AP. A non-exhaustive list includes:
 | 
			
		||||
- Shard Hunt
 | 
			
		||||
- Deep Dungeon
 | 
			
		||||
 | 
			
		||||
## What Final Fantasy items can appear in other players' worlds?
 | 
			
		||||
Currently, only progression items can appear in other players' worlds. Armor, Weapons and Consumable Items can not.
 | 
			
		||||
 | 
			
		||||
## What does another world's item look like in Final Fantasy
 | 
			
		||||
All local and remote items appear the same. It will say that you received an item and then BOTH the client log and
 | 
			
		||||
the emulator will display what was found external to the in-game text box.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,114 @@
 | 
			
		|||
# Final Fantasy 1 (NES) Multiworld Setup Guide
 | 
			
		||||
 | 
			
		||||
## Required Software
 | 
			
		||||
- The FF1Client which is bundled with [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
 | 
			
		||||
- The [BizHawk](http://tasvideos.org/BizHawk.html) emulator. Versions 2.3.1 and higher are supported. 
 | 
			
		||||
  Version 2.7 is recommended
 | 
			
		||||
- Your Final Fantasy (USA Edition) ROM file, probably named `Final Fantasy (USA).nes`. Neither Archipelago.gg nor the 
 | 
			
		||||
  Final Fantasy Randomizer Community can supply you with this.
 | 
			
		||||
 | 
			
		||||
## Installation Procedures
 | 
			
		||||
1. Download and install the latest version of [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
 | 
			
		||||
   1. On Windows, download Setup.Archipelago.<HighestVersion>.exe and run it
 | 
			
		||||
2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files.
 | 
			
		||||
    1. Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional
 | 
			
		||||
       steps for loading ROMs more conveniently
 | 
			
		||||
       1. Right-click on a ROM file and select **Open with...**
 | 
			
		||||
       2. Check the box next to **Always use this app to open .nes files**
 | 
			
		||||
       3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
 | 
			
		||||
       4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**.
 | 
			
		||||
 | 
			
		||||
## Playing a Multiworld
 | 
			
		||||
Playing a multiworld on Archipelago.gg has 3 key components:
 | 
			
		||||
1. The Server which is hosting a game for all players.
 | 
			
		||||
2. The Client Program. For Final Fantasy 1, it is a standalone program but other randomizers may build it in.
 | 
			
		||||
3. The Game itself, in this case running on Bizhawk, which then connects to the Client running on your computer.
 | 
			
		||||
 | 
			
		||||
To set this up the following steps are required:
 | 
			
		||||
1. (Each Player) Generate your own yaml file and randomized ROM
 | 
			
		||||
2. (Host Only) Generate a randomized game with you and 0 or more players using Archipelago
 | 
			
		||||
3. (Host Only) Run the Archipelago Server
 | 
			
		||||
4. (Each Player) Run your client program and connect it to the Server
 | 
			
		||||
5. (Each Player) Run your game and connect it to your client program
 | 
			
		||||
6. (Each Player) Play the game and have fun!
 | 
			
		||||
 | 
			
		||||
### Obtaining your Archipelago yaml file and randomized ROM
 | 
			
		||||
Unlike most other Archipelago.gg games Final Fantasy 1 is randomized by the 
 | 
			
		||||
[main randomizer](https://finalfantasyrandomizer.com/). Generate a game by going to the site and performing the
 | 
			
		||||
following steps:
 | 
			
		||||
1. Select the randomization options (also known as `Flags` in the community) of your choice. If you do not know what 
 | 
			
		||||
you prefer, or it is your first time playing select the "Archipelago" preset on the main page.
 | 
			
		||||
2. Go to the `Beta` tab and ensure `Archipelago` is enabled. Set your player name to any name that represents you.
 | 
			
		||||
3. Upload you `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
 | 
			
		||||
4. Press the `NEW` button beside `Seed` a few times
 | 
			
		||||
5. Click `GENERATE ROM`
 | 
			
		||||
 | 
			
		||||
It should download two files. One is the `*.nes` file which your emulator will run and the other is the yaml file
 | 
			
		||||
required by Archipelago.gg
 | 
			
		||||
 | 
			
		||||
### Generating the Multiworld and Starting the Server
 | 
			
		||||
The game can be generated locally or by Archipelago.gg.
 | 
			
		||||
 | 
			
		||||
#### Generating on Archipelago.gg (Recommended)
 | 
			
		||||
1. Gather all yaml files
 | 
			
		||||
2. Create a zip file containing all of the yaml files. Make sure it is a `*.zip` not a `*.7z` or a `*.rar`
 | 
			
		||||
3. Navigate to the [Generate Page](https://archipelago.gg/generate) and click `Upload File`
 | 
			
		||||
   1. For your first game keep `Forfeit Permission` as `Automatic on goal completion`. Forfeiting actually means
 | 
			
		||||
      giving out all of the items remaining in your game in this case so you do not block anyone else.
 | 
			
		||||
   2. For your first game keep `Hint Cost` at 10%
 | 
			
		||||
4. Select your zip file
 | 
			
		||||
 | 
			
		||||
#### Generating Locally
 | 
			
		||||
1. Navigate to your Archipelago install directory
 | 
			
		||||
2. Empty the `Players` directory then fill it with one yaml per player including your own which you got from the 
 | 
			
		||||
   finalfantasyrandomizer website above
 | 
			
		||||
3. Run `ArchipelagoGenerate.exe` (double-click it in File Explorer)
 | 
			
		||||
4. You will find your generated game in the `output` directory
 | 
			
		||||
 | 
			
		||||
#### Starting the server
 | 
			
		||||
If you generated on Archipelago.gg click `Create New Room` on the results page to start your server
 | 
			
		||||
If you generated locally simply navigate to the [Host Game Page](https://archipelago.gg/uploads) and upload the file
 | 
			
		||||
in the `output` directory
 | 
			
		||||
 | 
			
		||||
### Running the Client Program and Connecting to the Server
 | 
			
		||||
1. Navigate to your Archipelago install folder and run `ArchipelagoFF1Client.exe`
 | 
			
		||||
2. Notice the `/connect command` on the server hosting page (It should look like `/connect archipelago.gg:*****` where 
 | 
			
		||||
   ***** are numbers)
 | 
			
		||||
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
 | 
			
		||||
   already say `archipelago.gg`) and click `connect`
 | 
			
		||||
 | 
			
		||||
#### Running Your Game and Connecting to the Client Program
 | 
			
		||||
1. Open Bizhawk 2.3.1 or higher and load your ROM OR 
 | 
			
		||||
   click your ROM file if it is already associated with the extension `*.nes`
 | 
			
		||||
2. Click on the Tools menu and click on **Lua Console**
 | 
			
		||||
3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**)
 | 
			
		||||
4. Navigate to the location you installed Archipelago to. Open data/lua/FF1/ff1_connector.lua
 | 
			
		||||
   1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
 | 
			
		||||
   close your emulator entirely, restart it and re-run these steps
 | 
			
		||||
   2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking 
 | 
			
		||||
   **Help** -> **About**
 | 
			
		||||
 | 
			
		||||
### Play the game
 | 
			
		||||
When the client shows both NES and server are connected you are good to go. You can check the connection status of the
 | 
			
		||||
NES at any time by running `/nes`
 | 
			
		||||
 | 
			
		||||
### Helpful Commands
 | 
			
		||||
Commands are broken into two types: `/` and `!` commands. The difference is that `/commands` are local to your machine
 | 
			
		||||
and game whereas `!` commands ask the server. Most of the time you can use local commands.
 | 
			
		||||
 | 
			
		||||
#### Local Commands
 | 
			
		||||
- `/connect <address with port number>` connect to the multiworld server
 | 
			
		||||
- `/disconnect` if you accidentally connected to the wrong port run this to disconnect and then reconnect using
 | 
			
		||||
- `/nes` Shows the current status of the NES connection
 | 
			
		||||
- `/received` Displays all the items you have found or been sent
 | 
			
		||||
- `/missing` Displays all the locations along with their current status (checked/missing)
 | 
			
		||||
- Just typing anything will broadcast a message to all players
 | 
			
		||||
 | 
			
		||||
#### Remote Commands
 | 
			
		||||
- `!hint <item name>` Tells you at which location in whose game your Item is. Note you need to have checked some locations
 | 
			
		||||
to earn a hint. You can check how many you have by just running `!hint`
 | 
			
		||||
- `!forfeit` If you didn't turn on auto-forfeit or you allowed forfeiting prior to goal completion. Remember that
 | 
			
		||||
"forfeiting" actually means sending out your remaining items in your world
 | 
			
		||||
 | 
			
		||||
#### Host only (on Archipelago.gg)
 | 
			
		||||
`/forfeit <Player Name>` Forfeits someone regardless of settings and game completion status
 | 
			
		||||
| 
						 | 
				
			
			@ -323,5 +323,24 @@
 | 
			
		|||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "gameTitle": "Final Fantasy",
 | 
			
		||||
    "tutorials": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "Multiworld Setup Guide",
 | 
			
		||||
        "description": "A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
 | 
			
		||||
        "files": [
 | 
			
		||||
          {
 | 
			
		||||
            "language": "English",
 | 
			
		||||
            "filename": "ff1/multiworld_en.md",
 | 
			
		||||
            "link": "ff1/multiworld/en",
 | 
			
		||||
            "authors": [
 | 
			
		||||
              "jat2980"
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,542 @@
 | 
			
		|||
local socket = require("socket")
 | 
			
		||||
local json = require('json')
 | 
			
		||||
local math = require('math')
 | 
			
		||||
 | 
			
		||||
local STATE_OK = "Ok"
 | 
			
		||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
 | 
			
		||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
 | 
			
		||||
local STATE_UNINITIALIZED = "Uninitialized"
 | 
			
		||||
 | 
			
		||||
local ITEM_INDEX = 0x03
 | 
			
		||||
local WEAPON_INDEX = 0x07
 | 
			
		||||
local ARMOR_INDEX = 0x0B
 | 
			
		||||
 | 
			
		||||
local goldLookup = {
 | 
			
		||||
    [0x16C] = 10,
 | 
			
		||||
    [0x16D] = 20,
 | 
			
		||||
    [0x16E] = 25,
 | 
			
		||||
    [0x16F] = 30,
 | 
			
		||||
    [0x170] = 55,
 | 
			
		||||
    [0x171] = 70,
 | 
			
		||||
    [0x172] = 85,
 | 
			
		||||
    [0x173] = 110,
 | 
			
		||||
    [0x174] = 135,
 | 
			
		||||
    [0x175] = 155,
 | 
			
		||||
    [0x176] = 160,
 | 
			
		||||
    [0x177] = 180,
 | 
			
		||||
    [0x178] = 240,
 | 
			
		||||
    [0x179] = 255,
 | 
			
		||||
    [0x17A] = 260,
 | 
			
		||||
    [0x17B] = 295,
 | 
			
		||||
    [0x17C] = 300,
 | 
			
		||||
    [0x17D] = 315,
 | 
			
		||||
    [0x17E] = 330,
 | 
			
		||||
    [0x17F] = 350,
 | 
			
		||||
    [0x180] = 385,
 | 
			
		||||
    [0x181] = 400,
 | 
			
		||||
    [0x182] = 450,
 | 
			
		||||
    [0x183] = 500,
 | 
			
		||||
    [0x184] = 530,
 | 
			
		||||
    [0x185] = 575,
 | 
			
		||||
    [0x186] = 620,
 | 
			
		||||
    [0x187] = 680,
 | 
			
		||||
    [0x188] = 750,
 | 
			
		||||
    [0x189] = 795,
 | 
			
		||||
    [0x18A] = 880,
 | 
			
		||||
    [0x18B] = 1020,
 | 
			
		||||
    [0x18C] = 1250,
 | 
			
		||||
    [0x18D] = 1455,
 | 
			
		||||
    [0x18E] = 1520,
 | 
			
		||||
    [0x18F] = 1760,
 | 
			
		||||
    [0x190] = 1975,
 | 
			
		||||
    [0x191] = 2000,
 | 
			
		||||
    [0x192] = 2750,
 | 
			
		||||
    [0x193] = 3400,
 | 
			
		||||
    [0x194] = 4150,
 | 
			
		||||
    [0x195] = 5000,
 | 
			
		||||
    [0x196] = 5450,
 | 
			
		||||
    [0x197] = 6400,
 | 
			
		||||
    [0x198] = 6720,
 | 
			
		||||
    [0x199] = 7340,
 | 
			
		||||
    [0x19A] = 7690,
 | 
			
		||||
    [0x19B] = 7900,
 | 
			
		||||
    [0x19C] = 8135,
 | 
			
		||||
    [0x19D] = 9000,
 | 
			
		||||
    [0x19E] = 9300,
 | 
			
		||||
    [0x19F] = 9500,
 | 
			
		||||
    [0x1A0] = 9900,
 | 
			
		||||
    [0x1A1] = 10000,
 | 
			
		||||
    [0x1A2] = 12350,
 | 
			
		||||
    [0x1A3] = 13000,
 | 
			
		||||
    [0x1A4] = 13450,
 | 
			
		||||
    [0x1A5] = 14050,
 | 
			
		||||
    [0x1A6] = 14720,
 | 
			
		||||
    [0x1A7] = 15000,
 | 
			
		||||
    [0x1A8] = 17490,
 | 
			
		||||
    [0x1A9] = 18010,
 | 
			
		||||
    [0x1AA] = 19990,
 | 
			
		||||
    [0x1AB] = 20000,
 | 
			
		||||
    [0x1AC] = 20010,
 | 
			
		||||
    [0x1AD] = 26000,
 | 
			
		||||
    [0x1AE] = 45000,
 | 
			
		||||
    [0x1AF] = 65000
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
local extensionConsumableLookup = {
 | 
			
		||||
    [432] = 0x3C,
 | 
			
		||||
    [436] = 0x3C,
 | 
			
		||||
    [440] = 0x3C,
 | 
			
		||||
    [433] = 0x3D,
 | 
			
		||||
    [437] = 0x3D,
 | 
			
		||||
    [441] = 0x3D,
 | 
			
		||||
    [434] = 0x3E,
 | 
			
		||||
    [438] = 0x3E,
 | 
			
		||||
    [442] = 0x3E,
 | 
			
		||||
    [435] = 0x3F,
 | 
			
		||||
    [439] = 0x3F,
 | 
			
		||||
    [443] = 0x3F
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
local itemMessages = {}
 | 
			
		||||
local consumableStacks = nil
 | 
			
		||||
local prevstate = ""
 | 
			
		||||
local curstate =  STATE_UNINITIALIZED
 | 
			
		||||
local ff1Socket = nil
 | 
			
		||||
local frame = 0
 | 
			
		||||
 | 
			
		||||
local u8 = nil
 | 
			
		||||
local wU8 = nil
 | 
			
		||||
local isNesHawk = false
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
 | 
			
		||||
local function defineMemoryFunctions()
 | 
			
		||||
	local memDomain = {}
 | 
			
		||||
	local domains = memory.getmemorydomainlist()
 | 
			
		||||
	if domains[1] == "System Bus" then
 | 
			
		||||
		--NesHawk
 | 
			
		||||
		isNesHawk = true
 | 
			
		||||
		memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
 | 
			
		||||
		memDomain["saveram"]   = function() memory.usememorydomain("Battery RAM") end
 | 
			
		||||
		memDomain["rom"]       = function() memory.usememorydomain("PRG ROM") end
 | 
			
		||||
	elseif domains[1] == "WRAM" then
 | 
			
		||||
		--QuickNES
 | 
			
		||||
		memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
 | 
			
		||||
		memDomain["saveram"]   = function() memory.usememorydomain("WRAM") end
 | 
			
		||||
		memDomain["rom"]       = function() memory.usememorydomain("PRG ROM") end
 | 
			
		||||
	end
 | 
			
		||||
	return memDomain
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local memDomain = defineMemoryFunctions()
 | 
			
		||||
u8 = memory.read_u8
 | 
			
		||||
wU8 = memory.write_u8
 | 
			
		||||
uRange = memory.readbyterange
 | 
			
		||||
 | 
			
		||||
local function StateOKForMainLoop()
 | 
			
		||||
    memDomain.saveram()
 | 
			
		||||
    local A = u8(0x102) -- Party Made
 | 
			
		||||
    local B = u8(0x0FC)
 | 
			
		||||
    local C = u8(0x0A3)
 | 
			
		||||
    return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function table.empty (self)
 | 
			
		||||
    for _, _ in pairs(self) do
 | 
			
		||||
        return false
 | 
			
		||||
    end
 | 
			
		||||
    return true
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function slice (tbl, s, e)
 | 
			
		||||
    local pos, new = 1, {}
 | 
			
		||||
    for i = s + 1, e do
 | 
			
		||||
        new[pos] = tbl[i]
 | 
			
		||||
        pos = pos + 1
 | 
			
		||||
    end
 | 
			
		||||
    return new
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local bizhawk_version = client.getversion()
 | 
			
		||||
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
 | 
			
		||||
local is26To27 =  (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7")
 | 
			
		||||
 | 
			
		||||
local function getMaxMessageLength()
 | 
			
		||||
    if is23Or24Or25 then
 | 
			
		||||
        return client.screenwidth()/11
 | 
			
		||||
    elseif is26To27 then
 | 
			
		||||
        return client.screenwidth()/12
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function drawText(x, y, message, color)
 | 
			
		||||
    if is23Or24Or25 then
 | 
			
		||||
        gui.addmessage(message)
 | 
			
		||||
    elseif is26To27 then
 | 
			
		||||
        gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client")
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function clearScreen()
 | 
			
		||||
    if is23Or24Or25 then
 | 
			
		||||
        return
 | 
			
		||||
    elseif is26To27 then
 | 
			
		||||
        drawText(0, 0, "", "black")
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function drawMessages()
 | 
			
		||||
    if table.empty(itemMessages) then
 | 
			
		||||
        clearScreen()
 | 
			
		||||
        return
 | 
			
		||||
    end
 | 
			
		||||
    local y = 10
 | 
			
		||||
    found = false
 | 
			
		||||
    maxMessageLength = getMaxMessageLength()
 | 
			
		||||
    for k, v in pairs(itemMessages) do
 | 
			
		||||
        if v["TTL"] > 0 then
 | 
			
		||||
            message = v["message"]
 | 
			
		||||
            while true do
 | 
			
		||||
                drawText(5, y, message:sub(1, maxMessageLength), v["color"])
 | 
			
		||||
                y = y + 16
 | 
			
		||||
 | 
			
		||||
                message = message:sub(maxMessageLength + 1, message:len())
 | 
			
		||||
                if message:len() == 0 then
 | 
			
		||||
                    break
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
            newTTL = 0
 | 
			
		||||
            if is26To27 then
 | 
			
		||||
                newTTL = itemMessages[k]["TTL"] - 1
 | 
			
		||||
            end
 | 
			
		||||
            itemMessages[k]["TTL"] = newTTL
 | 
			
		||||
            found = true
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    if found == false then
 | 
			
		||||
        clearScreen()
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function generateLocationChecked()
 | 
			
		||||
    memDomain.saveram()
 | 
			
		||||
    data = uRange(0x01FF, 0x101)
 | 
			
		||||
    data[0] = nil
 | 
			
		||||
    return data
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function setConsumableStacks()
 | 
			
		||||
    memDomain.rom()
 | 
			
		||||
    consumableStacks = {}
 | 
			
		||||
    -- In order shards, tent, cabin, house, heal, pure, soft, ext1, ext2, ext3, ex4
 | 
			
		||||
    consumableStacks[0x35] = 1
 | 
			
		||||
    consumableStacks[0x36] = u8(0x47400) + 1
 | 
			
		||||
    consumableStacks[0x37] = u8(0x47401) + 1
 | 
			
		||||
    consumableStacks[0x38] = u8(0x47402) + 1
 | 
			
		||||
    consumableStacks[0x39] = u8(0x47403) + 1
 | 
			
		||||
    consumableStacks[0x3A] = u8(0x47404) + 1
 | 
			
		||||
    consumableStacks[0x3B] = u8(0x47405) + 1
 | 
			
		||||
    consumableStacks[0x3C] = u8(0x47406) + 1
 | 
			
		||||
    consumableStacks[0x3D] = u8(0x47407) + 1
 | 
			
		||||
    consumableStacks[0x3E] = u8(0x47408) + 1
 | 
			
		||||
    consumableStacks[0x3F] = u8(0x47409) + 1
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function getEmptyWeaponSlots()
 | 
			
		||||
    memDomain.saveram()
 | 
			
		||||
    ret = {}
 | 
			
		||||
    count = 1
 | 
			
		||||
    slot1 = uRange(0x118, 0x4)
 | 
			
		||||
    slot2 = uRange(0x158, 0x4)
 | 
			
		||||
    slot3 = uRange(0x198, 0x4)
 | 
			
		||||
    slot4 = uRange(0x1D8, 0x4)
 | 
			
		||||
    for i,v in pairs(slot1) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x118 + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    for i,v in pairs(slot2) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x158 + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    for i,v in pairs(slot3) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x198 + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    for i,v in pairs(slot4) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x1D8 + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    return ret
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function getEmptyArmorSlots()
 | 
			
		||||
    memDomain.saveram()
 | 
			
		||||
    ret = {}
 | 
			
		||||
    count = 1
 | 
			
		||||
    slot1 = uRange(0x11C, 0x4)
 | 
			
		||||
    slot2 = uRange(0x15C, 0x4)
 | 
			
		||||
    slot3 = uRange(0x19C, 0x4)
 | 
			
		||||
    slot4 = uRange(0x1DC, 0x4)
 | 
			
		||||
    for i,v in pairs(slot1) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x11C + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    for i,v in pairs(slot2) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x15C + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    for i,v in pairs(slot3) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x19C + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    for i,v in pairs(slot4) do
 | 
			
		||||
        if v == 0 then
 | 
			
		||||
            ret[count] = 0x1DC + i
 | 
			
		||||
            count = count + 1
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    return ret
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function processBlock(block)
 | 
			
		||||
    local msgBlock = block['messages']
 | 
			
		||||
    if msgBlock ~= nil then
 | 
			
		||||
        for i, v in pairs(msgBlock) do
 | 
			
		||||
            if itemMessages[i] == nil then
 | 
			
		||||
                local msg = {TTL=450, message=v, color=0xFFFF0000}
 | 
			
		||||
                itemMessages[i] = msg
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
    local itemsBlock = block["items"]
 | 
			
		||||
    memDomain.saveram()
 | 
			
		||||
    isInGame = u8(0x102)
 | 
			
		||||
    if itemsBlock ~= nil and isInGame ~= 0x00 then
 | 
			
		||||
        if consumableStacks == nil then
 | 
			
		||||
            setConsumableStacks()
 | 
			
		||||
        end
 | 
			
		||||
        memDomain.saveram()
 | 
			
		||||
--         print('ITEMBLOCK: ')
 | 
			
		||||
--         print(itemsBlock)
 | 
			
		||||
        itemIndex = u8(ITEM_INDEX)
 | 
			
		||||
--         print('ITEMINDEX: '..itemIndex)
 | 
			
		||||
        for i, v in pairs(slice(itemsBlock, itemIndex, #itemsBlock)) do
 | 
			
		||||
            -- Minus the offset and add to the correct domain
 | 
			
		||||
            local memoryLocation = v
 | 
			
		||||
            if v >= 0x100 and v <= 0x114 then
 | 
			
		||||
                -- This is a key item
 | 
			
		||||
                memoryLocation = memoryLocation - 0x0E0
 | 
			
		||||
                wU8(memoryLocation, 0x01)
 | 
			
		||||
            elseif v >= 0x1E0 then
 | 
			
		||||
                -- This is a movement item
 | 
			
		||||
                -- Minus Offset (0x100) - movement offset (0xE0)
 | 
			
		||||
                memoryLocation = memoryLocation - 0x1E0
 | 
			
		||||
                -- Canal is a flipped bit
 | 
			
		||||
                if memoryLocation == 0x0C then
 | 
			
		||||
                    wU8(memoryLocation, 0x00)
 | 
			
		||||
                else
 | 
			
		||||
                    wU8(memoryLocation, 0x01)
 | 
			
		||||
                end
 | 
			
		||||
 | 
			
		||||
            elseif v >= 0x16C and v <= 0x1AF then
 | 
			
		||||
                -- This is a gold item
 | 
			
		||||
                amountToAdd = goldLookup[v]
 | 
			
		||||
                biggest = u8(0x01E)
 | 
			
		||||
                medium = u8(0x01D)
 | 
			
		||||
                smallest = u8(0x01C)
 | 
			
		||||
                currentValue = 0x10000 * biggest + 0x100 * medium + smallest
 | 
			
		||||
                newValue = currentValue + amountToAdd
 | 
			
		||||
                newBiggest = math.floor(newValue / 0x10000)
 | 
			
		||||
                newMedium = math.floor(math.fmod(newValue, 0x10000) / 0x100)
 | 
			
		||||
                newSmallest = math.floor(math.fmod(newValue, 0x100))
 | 
			
		||||
                wU8(0x01E, newBiggest)
 | 
			
		||||
                wU8(0x01D, newMedium)
 | 
			
		||||
                wU8(0x01C, newSmallest)
 | 
			
		||||
            elseif v >= 0x115 and v <= 0x11B then
 | 
			
		||||
                -- This is a regular consumable OR a shard
 | 
			
		||||
                -- Minus Offset (0x100) + item offset (0x20)
 | 
			
		||||
                memoryLocation = memoryLocation - 0x0E0
 | 
			
		||||
                currentValue = u8(memoryLocation)
 | 
			
		||||
                amountToAdd = consumableStacks[memoryLocation]
 | 
			
		||||
                if currentValue < 99 then
 | 
			
		||||
                    wU8(memoryLocation, currentValue + amountToAdd)
 | 
			
		||||
                end
 | 
			
		||||
            elseif v >= 0x1B0  and v <= 0x1BB then
 | 
			
		||||
                -- This is an extension consumable
 | 
			
		||||
                memoryLocation = extensionConsumableLookup[v]
 | 
			
		||||
                currentValue = u8(memoryLocation)
 | 
			
		||||
                amountToAdd = consumableStacks[memoryLocation]
 | 
			
		||||
                if currentValue < 99 then
 | 
			
		||||
                    value = currentValue + amountToAdd
 | 
			
		||||
                    if value > 99 then
 | 
			
		||||
                        value = 99
 | 
			
		||||
                    end
 | 
			
		||||
                    wU8(memoryLocation, value)
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        if #itemsBlock ~= itemIndex then
 | 
			
		||||
            wU8(ITEM_INDEX, #itemsBlock)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        memDomain.saveram()
 | 
			
		||||
        weaponIndex = u8(WEAPON_INDEX)
 | 
			
		||||
        emptyWeaponSlots = getEmptyWeaponSlots()
 | 
			
		||||
        lastUsedWeaponIndex = weaponIndex
 | 
			
		||||
--         print('WEAPON_INDEX: '.. weaponIndex)
 | 
			
		||||
        memDomain.saveram()
 | 
			
		||||
        for i, v in pairs(slice(itemsBlock, weaponIndex, #itemsBlock)) do
 | 
			
		||||
            if v >= 0x11C and v <= 0x143 then
 | 
			
		||||
                -- Minus the offset and add to the correct domain
 | 
			
		||||
                local itemValue = v - 0x11B
 | 
			
		||||
                if #emptyWeaponSlots > 0 then
 | 
			
		||||
                    slot = table.remove(emptyWeaponSlots, 1)
 | 
			
		||||
                    wU8(slot, itemValue)
 | 
			
		||||
                    lastUsedWeaponIndex = weaponIndex + i
 | 
			
		||||
                else
 | 
			
		||||
                    break
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        if lastUsedWeaponIndex ~= weaponIndex then
 | 
			
		||||
            wU8(WEAPON_INDEX, lastUsedWeaponIndex)
 | 
			
		||||
        end
 | 
			
		||||
        memDomain.saveram()
 | 
			
		||||
        armorIndex = u8(ARMOR_INDEX)
 | 
			
		||||
        emptyArmorSlots = getEmptyArmorSlots()
 | 
			
		||||
        lastUsedArmorIndex = armorIndex
 | 
			
		||||
--         print('ARMOR_INDEX: '.. armorIndex)
 | 
			
		||||
        memDomain.saveram()
 | 
			
		||||
        for i, v in pairs(slice(itemsBlock, armorIndex, #itemsBlock)) do
 | 
			
		||||
            if v >= 0x144 and v <= 0x16B then
 | 
			
		||||
                -- Minus the offset and add to the correct domain
 | 
			
		||||
                local itemValue = v - 0x143
 | 
			
		||||
                if #emptyArmorSlots > 0 then
 | 
			
		||||
                    slot = table.remove(emptyArmorSlots, 1)
 | 
			
		||||
                    wU8(slot, itemValue)
 | 
			
		||||
                    lastUsedArmorIndex = armorIndex + i
 | 
			
		||||
                else
 | 
			
		||||
                    break
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        if lastUsedArmorIndex ~= armorIndex then
 | 
			
		||||
            wU8(ARMOR_INDEX, lastUsedArmorIndex)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function difference(a, b)
 | 
			
		||||
    local aa = {}
 | 
			
		||||
    for k,v in pairs(a) do aa[v]=true end
 | 
			
		||||
    for k,v in pairs(b) do aa[v]=nil end
 | 
			
		||||
    local ret = {}
 | 
			
		||||
    local n = 0
 | 
			
		||||
    for k,v in pairs(a) do
 | 
			
		||||
        if aa[v] then n=n+1 ret[n]=v end
 | 
			
		||||
    end
 | 
			
		||||
    return ret
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function receive()
 | 
			
		||||
    l, e = ff1Socket:receive()
 | 
			
		||||
    if e == 'closed' then
 | 
			
		||||
        if curstate == STATE_OK then
 | 
			
		||||
            print("Connection closed")
 | 
			
		||||
        end
 | 
			
		||||
        curstate = STATE_UNINITIALIZED
 | 
			
		||||
        return
 | 
			
		||||
    elseif e == 'timeout' then
 | 
			
		||||
        print("timeout")
 | 
			
		||||
        return
 | 
			
		||||
    elseif e ~= nil then
 | 
			
		||||
        print(e)
 | 
			
		||||
        curstate = STATE_UNINITIALIZED
 | 
			
		||||
        return
 | 
			
		||||
    end
 | 
			
		||||
    processBlock(json.decode(l))
 | 
			
		||||
 | 
			
		||||
    -- Determine Message to send back
 | 
			
		||||
    memDomain.rom()
 | 
			
		||||
    local playerName = uRange(0x7BCBF, 0x41)
 | 
			
		||||
    playerName[0] = nil
 | 
			
		||||
    local retTable = {}
 | 
			
		||||
    retTable["playerName"] = playerName
 | 
			
		||||
    if StateOKForMainLoop() then
 | 
			
		||||
        retTable["locations"] = generateLocationChecked()
 | 
			
		||||
    end
 | 
			
		||||
    msg = json.encode(retTable).."\n"
 | 
			
		||||
    local ret, error = ff1Socket:send(msg)
 | 
			
		||||
    if ret == nil then
 | 
			
		||||
        print(error)
 | 
			
		||||
    elseif curstate == STATE_INITIAL_CONNECTION_MADE then
 | 
			
		||||
        curstate = STATE_TENTATIVELY_CONNECTED
 | 
			
		||||
    elseif curstate == STATE_TENTATIVELY_CONNECTED then
 | 
			
		||||
        print("Connected!")
 | 
			
		||||
        itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
 | 
			
		||||
        curstate = STATE_OK
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function main()
 | 
			
		||||
    if (is23Or24Or25 or is26To27) == false then
 | 
			
		||||
        print("Must use a version of bizhawk 2.3.1 or higher")
 | 
			
		||||
        return
 | 
			
		||||
    end
 | 
			
		||||
    server, error = socket.bind('localhost', 52980)
 | 
			
		||||
 | 
			
		||||
    while true do
 | 
			
		||||
        gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
 | 
			
		||||
        frame = frame + 1
 | 
			
		||||
        drawMessages()
 | 
			
		||||
        if not (curstate == prevstate) then
 | 
			
		||||
            -- console.log("Current state: "..curstate)
 | 
			
		||||
            prevstate = curstate
 | 
			
		||||
        end
 | 
			
		||||
        if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
 | 
			
		||||
            if (frame % 60 == 0) then
 | 
			
		||||
                gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
 | 
			
		||||
                receive()
 | 
			
		||||
            else
 | 
			
		||||
                gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
 | 
			
		||||
            end
 | 
			
		||||
        elseif (curstate == STATE_UNINITIALIZED) then
 | 
			
		||||
            gui.drawEllipse(248, 9, 6, 6, "Black", "White")
 | 
			
		||||
            if  (frame % 60 == 0) then
 | 
			
		||||
                gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
 | 
			
		||||
 | 
			
		||||
                drawText(5, 8, "Waiting for client", 0xFFFF0000)
 | 
			
		||||
                drawText(5, 32, "Please start FF1Client.exe", 0xFFFF0000)
 | 
			
		||||
 | 
			
		||||
                -- Advance so the messages are drawn
 | 
			
		||||
                emu.frameadvance()
 | 
			
		||||
                server:settimeout(2)
 | 
			
		||||
                print("Attempting to connect")
 | 
			
		||||
                local client, timeout = server:accept()
 | 
			
		||||
                if timeout == nil then
 | 
			
		||||
                    -- print('Initial Connection Made')
 | 
			
		||||
                    curstate = STATE_INITIAL_CONNECTION_MADE
 | 
			
		||||
                    ff1Socket = client
 | 
			
		||||
                    ff1Socket:settimeout(0)
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        emu.frameadvance()
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
main()
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,380 @@
 | 
			
		|||
--
 | 
			
		||||
-- json.lua
 | 
			
		||||
--
 | 
			
		||||
-- Copyright (c) 2015 rxi
 | 
			
		||||
--
 | 
			
		||||
-- This library is free software; you can redistribute it and/or modify it
 | 
			
		||||
-- under the terms of the MIT license. See LICENSE for details.
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
local json = { _version = "0.1.0" }
 | 
			
		||||
 | 
			
		||||
-------------------------------------------------------------------------------
 | 
			
		||||
-- Encode
 | 
			
		||||
-------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
local encode
 | 
			
		||||
 | 
			
		||||
local escape_char_map = {
 | 
			
		||||
  [ "\\" ] = "\\\\",
 | 
			
		||||
  [ "\"" ] = "\\\"",
 | 
			
		||||
  [ "\b" ] = "\\b",
 | 
			
		||||
  [ "\f" ] = "\\f",
 | 
			
		||||
  [ "\n" ] = "\\n",
 | 
			
		||||
  [ "\r" ] = "\\r",
 | 
			
		||||
  [ "\t" ] = "\\t",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
 | 
			
		||||
for k, v in pairs(escape_char_map) do
 | 
			
		||||
  escape_char_map_inv[v] = k
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function escape_char(c)
 | 
			
		||||
  return escape_char_map[c] or string.format("\\u%04x", c:byte())
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function encode_nil(val)
 | 
			
		||||
  return "null"
 | 
			
		||||
end 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function encode_table(val, stack)
 | 
			
		||||
  local res = {}
 | 
			
		||||
  stack = stack or {}
 | 
			
		||||
 | 
			
		||||
  -- Circular reference?
 | 
			
		||||
  if stack[val] then error("circular reference") end
 | 
			
		||||
 | 
			
		||||
  stack[val] = true
 | 
			
		||||
 | 
			
		||||
  if val[1] ~= nil or next(val) == nil then
 | 
			
		||||
    -- Treat as array -- check keys are valid and it is not sparse
 | 
			
		||||
    local n = 0
 | 
			
		||||
    for k in pairs(val) do
 | 
			
		||||
      if type(k) ~= "number" then
 | 
			
		||||
        error("invalid table: mixed or invalid key types")
 | 
			
		||||
      end
 | 
			
		||||
      n = n + 1
 | 
			
		||||
    end
 | 
			
		||||
    if n ~= #val then
 | 
			
		||||
      error("invalid table: sparse array")
 | 
			
		||||
    end
 | 
			
		||||
    -- Encode
 | 
			
		||||
    for i, v in ipairs(val) do
 | 
			
		||||
      table.insert(res, encode(v, stack))
 | 
			
		||||
    end
 | 
			
		||||
    stack[val] = nil
 | 
			
		||||
    return "[" .. table.concat(res, ",") .. "]"
 | 
			
		||||
 | 
			
		||||
  else
 | 
			
		||||
    -- Treat as an object
 | 
			
		||||
    for k, v in pairs(val) do
 | 
			
		||||
      if type(k) ~= "string" then
 | 
			
		||||
        error("invalid table: mixed or invalid key types")
 | 
			
		||||
      end
 | 
			
		||||
      table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
 | 
			
		||||
    end
 | 
			
		||||
    stack[val] = nil
 | 
			
		||||
    return "{" .. table.concat(res, ",") .. "}"
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function encode_string(val)
 | 
			
		||||
  return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function encode_number(val)
 | 
			
		||||
  -- Check for NaN, -inf and inf
 | 
			
		||||
  if val ~= val or val <= -math.huge or val >= math.huge then
 | 
			
		||||
    error("unexpected number value '" .. tostring(val) .. "'")
 | 
			
		||||
  end
 | 
			
		||||
  return string.format("%.14g", val)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local type_func_map = {
 | 
			
		||||
  [ "nil"     ] = encode_nil,
 | 
			
		||||
  [ "table"   ] = encode_table,
 | 
			
		||||
  [ "string"  ] = encode_string,
 | 
			
		||||
  [ "number"  ] = encode_number,
 | 
			
		||||
  [ "boolean" ] = tostring,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
encode = function(val, stack)
 | 
			
		||||
  local t = type(val)
 | 
			
		||||
  local f = type_func_map[t]
 | 
			
		||||
  if f then
 | 
			
		||||
    return f(val, stack)
 | 
			
		||||
  end
 | 
			
		||||
  error("unexpected type '" .. t .. "'")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function json.encode(val)
 | 
			
		||||
  return ( encode(val) )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
-------------------------------------------------------------------------------
 | 
			
		||||
-- Decode
 | 
			
		||||
-------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
local parse
 | 
			
		||||
 | 
			
		||||
local function create_set(...) 
 | 
			
		||||
  local res = {}
 | 
			
		||||
  for i = 1, select("#", ...) do
 | 
			
		||||
    res[ select(i, ...) ] = true
 | 
			
		||||
  end
 | 
			
		||||
  return res
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local space_chars   = create_set(" ", "\t", "\r", "\n")
 | 
			
		||||
local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
 | 
			
		||||
local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
 | 
			
		||||
local literals      = create_set("true", "false", "null")
 | 
			
		||||
 | 
			
		||||
local literal_map = {
 | 
			
		||||
  [ "true"  ] = true,
 | 
			
		||||
  [ "false" ] = false,
 | 
			
		||||
  [ "null"  ] = nil,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function next_char(str, idx, set, negate)
 | 
			
		||||
  for i = idx, #str do
 | 
			
		||||
    if set[str:sub(i, i)] ~= negate then
 | 
			
		||||
      return i
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
  return #str + 1
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function decode_error(str, idx, msg)
 | 
			
		||||
  --local line_count = 1
 | 
			
		||||
  --local col_count = 1
 | 
			
		||||
  --for i = 1, idx - 1 do
 | 
			
		||||
  --  col_count = col_count + 1
 | 
			
		||||
  --  if str:sub(i, i) == "\n" then
 | 
			
		||||
  --   line_count = line_count + 1
 | 
			
		||||
  --    col_count = 1
 | 
			
		||||
  --  end
 | 
			
		||||
  -- end
 | 
			
		||||
  -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function codepoint_to_utf8(n)
 | 
			
		||||
  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
 | 
			
		||||
  local f = math.floor
 | 
			
		||||
  if n <= 0x7f then
 | 
			
		||||
    return string.char(n)
 | 
			
		||||
  elseif n <= 0x7ff then
 | 
			
		||||
    return string.char(f(n / 64) + 192, n % 64 + 128)
 | 
			
		||||
  elseif n <= 0xffff then
 | 
			
		||||
    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
 | 
			
		||||
  elseif n <= 0x10ffff then
 | 
			
		||||
    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
 | 
			
		||||
                       f(n % 4096 / 64) + 128, n % 64 + 128)
 | 
			
		||||
  end
 | 
			
		||||
  error( string.format("invalid unicode codepoint '%x'", n) )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function parse_unicode_escape(s)
 | 
			
		||||
  local n1 = tonumber( s:sub(3, 6),  16 )
 | 
			
		||||
  local n2 = tonumber( s:sub(9, 12), 16 )
 | 
			
		||||
  -- Surrogate pair?
 | 
			
		||||
  if n2 then
 | 
			
		||||
    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
 | 
			
		||||
  else
 | 
			
		||||
    return codepoint_to_utf8(n1)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function parse_string(str, i)
 | 
			
		||||
  local has_unicode_escape = false
 | 
			
		||||
  local has_surrogate_escape = false
 | 
			
		||||
  local has_escape = false
 | 
			
		||||
  local last
 | 
			
		||||
  for j = i + 1, #str do
 | 
			
		||||
    local x = str:byte(j)
 | 
			
		||||
 | 
			
		||||
    if x < 32 then
 | 
			
		||||
      decode_error(str, j, "control character in string")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if last == 92 then -- "\\" (escape char)
 | 
			
		||||
      if x == 117 then -- "u" (unicode escape sequence)
 | 
			
		||||
        local hex = str:sub(j + 1, j + 5)
 | 
			
		||||
        if not hex:find("%x%x%x%x") then
 | 
			
		||||
          decode_error(str, j, "invalid unicode escape in string")
 | 
			
		||||
        end
 | 
			
		||||
        if hex:find("^[dD][89aAbB]") then
 | 
			
		||||
          has_surrogate_escape = true
 | 
			
		||||
        else
 | 
			
		||||
          has_unicode_escape = true
 | 
			
		||||
        end
 | 
			
		||||
      else
 | 
			
		||||
        local c = string.char(x)
 | 
			
		||||
        if not escape_chars[c] then
 | 
			
		||||
          decode_error(str, j, "invalid escape char '" .. c .. "' in string")
 | 
			
		||||
        end
 | 
			
		||||
        has_escape = true
 | 
			
		||||
      end
 | 
			
		||||
      last = nil
 | 
			
		||||
 | 
			
		||||
    elseif x == 34 then -- '"' (end of string)
 | 
			
		||||
      local s = str:sub(i + 1, j - 1)
 | 
			
		||||
      if has_surrogate_escape then 
 | 
			
		||||
        s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
 | 
			
		||||
      end
 | 
			
		||||
      if has_unicode_escape then 
 | 
			
		||||
        s = s:gsub("\\u....", parse_unicode_escape)
 | 
			
		||||
      end
 | 
			
		||||
      if has_escape then
 | 
			
		||||
        s = s:gsub("\\.", escape_char_map_inv)
 | 
			
		||||
      end
 | 
			
		||||
      return s, j + 1
 | 
			
		||||
    
 | 
			
		||||
    else
 | 
			
		||||
      last = x
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
  decode_error(str, i, "expected closing quote for string")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function parse_number(str, i)
 | 
			
		||||
  local x = next_char(str, i, delim_chars)
 | 
			
		||||
  local s = str:sub(i, x - 1)
 | 
			
		||||
  local n = tonumber(s)
 | 
			
		||||
  if not n then
 | 
			
		||||
    decode_error(str, i, "invalid number '" .. s .. "'")
 | 
			
		||||
  end
 | 
			
		||||
  return n, x
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function parse_literal(str, i)
 | 
			
		||||
  local x = next_char(str, i, delim_chars)
 | 
			
		||||
  local word = str:sub(i, x - 1)
 | 
			
		||||
  if not literals[word] then
 | 
			
		||||
    decode_error(str, i, "invalid literal '" .. word .. "'")
 | 
			
		||||
  end
 | 
			
		||||
  return literal_map[word], x
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function parse_array(str, i)
 | 
			
		||||
  local res = {}
 | 
			
		||||
  local n = 1
 | 
			
		||||
  i = i + 1
 | 
			
		||||
  while 1 do
 | 
			
		||||
    local x
 | 
			
		||||
    i = next_char(str, i, space_chars, true)
 | 
			
		||||
    -- Empty / end of array?
 | 
			
		||||
    if str:sub(i, i) == "]" then 
 | 
			
		||||
      i = i + 1
 | 
			
		||||
      break
 | 
			
		||||
    end
 | 
			
		||||
    -- Read token
 | 
			
		||||
    x, i = parse(str, i)
 | 
			
		||||
    res[n] = x
 | 
			
		||||
    n = n + 1
 | 
			
		||||
    -- Next token 
 | 
			
		||||
    i = next_char(str, i, space_chars, true)
 | 
			
		||||
    local chr = str:sub(i, i)
 | 
			
		||||
    i = i + 1
 | 
			
		||||
    if chr == "]" then break end
 | 
			
		||||
    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
 | 
			
		||||
  end
 | 
			
		||||
  return res, i
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local function parse_object(str, i)
 | 
			
		||||
  local res = {}
 | 
			
		||||
  i = i + 1
 | 
			
		||||
  while 1 do
 | 
			
		||||
    local key, val
 | 
			
		||||
    i = next_char(str, i, space_chars, true)
 | 
			
		||||
    -- Empty / end of object?
 | 
			
		||||
    if str:sub(i, i) == "}" then 
 | 
			
		||||
      i = i + 1
 | 
			
		||||
      break
 | 
			
		||||
    end
 | 
			
		||||
    -- Read key
 | 
			
		||||
    if str:sub(i, i) ~= '"' then
 | 
			
		||||
      decode_error(str, i, "expected string for key")
 | 
			
		||||
    end
 | 
			
		||||
    key, i = parse(str, i)
 | 
			
		||||
    -- Read ':' delimiter
 | 
			
		||||
    i = next_char(str, i, space_chars, true)
 | 
			
		||||
    if str:sub(i, i) ~= ":" then
 | 
			
		||||
      decode_error(str, i, "expected ':' after key")
 | 
			
		||||
    end
 | 
			
		||||
    i = next_char(str, i + 1, space_chars, true)
 | 
			
		||||
    -- Read value
 | 
			
		||||
    val, i = parse(str, i)
 | 
			
		||||
    -- Set
 | 
			
		||||
    res[key] = val
 | 
			
		||||
    -- Next token
 | 
			
		||||
    i = next_char(str, i, space_chars, true)
 | 
			
		||||
    local chr = str:sub(i, i)
 | 
			
		||||
    i = i + 1
 | 
			
		||||
    if chr == "}" then break end
 | 
			
		||||
    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
 | 
			
		||||
  end
 | 
			
		||||
  return res, i
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
local char_func_map = {
 | 
			
		||||
  [ '"' ] = parse_string,
 | 
			
		||||
  [ "0" ] = parse_number,
 | 
			
		||||
  [ "1" ] = parse_number,
 | 
			
		||||
  [ "2" ] = parse_number,
 | 
			
		||||
  [ "3" ] = parse_number,
 | 
			
		||||
  [ "4" ] = parse_number,
 | 
			
		||||
  [ "5" ] = parse_number,
 | 
			
		||||
  [ "6" ] = parse_number,
 | 
			
		||||
  [ "7" ] = parse_number,
 | 
			
		||||
  [ "8" ] = parse_number,
 | 
			
		||||
  [ "9" ] = parse_number,
 | 
			
		||||
  [ "-" ] = parse_number,
 | 
			
		||||
  [ "t" ] = parse_literal,
 | 
			
		||||
  [ "f" ] = parse_literal,
 | 
			
		||||
  [ "n" ] = parse_literal,
 | 
			
		||||
  [ "[" ] = parse_array,
 | 
			
		||||
  [ "{" ] = parse_object,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
parse = function(str, idx)
 | 
			
		||||
  local chr = str:sub(idx, idx)
 | 
			
		||||
  local f = char_func_map[chr]
 | 
			
		||||
  if f then
 | 
			
		||||
    return f(str, idx)
 | 
			
		||||
  end
 | 
			
		||||
  decode_error(str, idx, "unexpected character '" .. chr .. "'")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function json.decode(str)
 | 
			
		||||
  if type(str) ~= "string" then
 | 
			
		||||
    error("expected argument of type string, got " .. type(str))
 | 
			
		||||
  end
 | 
			
		||||
  return ( parse(str, next_char(str, 1, space_chars, true)) )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
return json
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,132 @@
 | 
			
		|||
-----------------------------------------------------------------------------
 | 
			
		||||
-- LuaSocket helper module
 | 
			
		||||
-- Author: Diego Nehab
 | 
			
		||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
-- Declare module and import dependencies
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
local base = _G
 | 
			
		||||
local string = require("string")
 | 
			
		||||
local math = require("math")
 | 
			
		||||
local socket = require("socket.core")
 | 
			
		||||
module("socket")
 | 
			
		||||
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
-- Exported auxiliar functions
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
function connect(address, port, laddress, lport)
 | 
			
		||||
    local sock, err = socket.tcp()
 | 
			
		||||
    if not sock then return nil, err end
 | 
			
		||||
    if laddress then
 | 
			
		||||
        local res, err = sock:bind(laddress, lport, -1)
 | 
			
		||||
        if not res then return nil, err end
 | 
			
		||||
    end
 | 
			
		||||
    local res, err = sock:connect(address, port)
 | 
			
		||||
    if not res then return nil, err end
 | 
			
		||||
    return sock
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function bind(host, port, backlog)
 | 
			
		||||
    local sock, err = socket.tcp()
 | 
			
		||||
    if not sock then return nil, err end
 | 
			
		||||
    sock:setoption("reuseaddr", true)
 | 
			
		||||
    local res, err = sock:bind(host, port)
 | 
			
		||||
    if not res then return nil, err end
 | 
			
		||||
    res, err = sock:listen(backlog)
 | 
			
		||||
    if not res then return nil, err end
 | 
			
		||||
    return sock
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
try = newtry()
 | 
			
		||||
 | 
			
		||||
function choose(table)
 | 
			
		||||
    return function(name, opt1, opt2)
 | 
			
		||||
        if base.type(name) ~= "string" then
 | 
			
		||||
            name, opt1, opt2 = "default", name, opt1
 | 
			
		||||
        end
 | 
			
		||||
        local f = table[name or "nil"]
 | 
			
		||||
        if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
 | 
			
		||||
        else return f(opt1, opt2) end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
-- Socket sources and sinks, conforming to LTN12
 | 
			
		||||
-----------------------------------------------------------------------------
 | 
			
		||||
-- create namespaces inside LuaSocket namespace
 | 
			
		||||
sourcet = {}
 | 
			
		||||
sinkt = {}
 | 
			
		||||
 | 
			
		||||
BLOCKSIZE = 2048
 | 
			
		||||
 | 
			
		||||
sinkt["close-when-done"] = function(sock)
 | 
			
		||||
    return base.setmetatable({
 | 
			
		||||
        getfd = function() return sock:getfd() end,
 | 
			
		||||
        dirty = function() return sock:dirty() end
 | 
			
		||||
    }, {
 | 
			
		||||
        __call = function(self, chunk, err)
 | 
			
		||||
            if not chunk then
 | 
			
		||||
                sock:close()
 | 
			
		||||
                return 1
 | 
			
		||||
            else return sock:send(chunk) end
 | 
			
		||||
        end
 | 
			
		||||
    })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
sinkt["keep-open"] = function(sock)
 | 
			
		||||
    return base.setmetatable({
 | 
			
		||||
        getfd = function() return sock:getfd() end,
 | 
			
		||||
        dirty = function() return sock:dirty() end
 | 
			
		||||
    }, {
 | 
			
		||||
        __call = function(self, chunk, err)
 | 
			
		||||
            if chunk then return sock:send(chunk)
 | 
			
		||||
            else return 1 end
 | 
			
		||||
        end
 | 
			
		||||
    })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
sinkt["default"] = sinkt["keep-open"]
 | 
			
		||||
 | 
			
		||||
sink = choose(sinkt)
 | 
			
		||||
 | 
			
		||||
sourcet["by-length"] = function(sock, length)
 | 
			
		||||
    return base.setmetatable({
 | 
			
		||||
        getfd = function() return sock:getfd() end,
 | 
			
		||||
        dirty = function() return sock:dirty() end
 | 
			
		||||
    }, {
 | 
			
		||||
        __call = function()
 | 
			
		||||
            if length <= 0 then return nil end
 | 
			
		||||
            local size = math.min(socket.BLOCKSIZE, length)
 | 
			
		||||
            local chunk, err = sock:receive(size)
 | 
			
		||||
            if err then return nil, err end
 | 
			
		||||
            length = length - string.len(chunk)
 | 
			
		||||
            return chunk
 | 
			
		||||
        end
 | 
			
		||||
    })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
sourcet["until-closed"] = function(sock)
 | 
			
		||||
    local done
 | 
			
		||||
    return base.setmetatable({
 | 
			
		||||
        getfd = function() return sock:getfd() end,
 | 
			
		||||
        dirty = function() return sock:dirty() end
 | 
			
		||||
    }, {
 | 
			
		||||
        __call = function()
 | 
			
		||||
            if done then return nil end
 | 
			
		||||
            local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
 | 
			
		||||
            if not err then return chunk
 | 
			
		||||
            elseif err == "closed" then
 | 
			
		||||
                sock:close()
 | 
			
		||||
                done = 1
 | 
			
		||||
                return partial
 | 
			
		||||
            else return nil, err end
 | 
			
		||||
        end
 | 
			
		||||
    })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
sourcet["default"] = sourcet["until-closed"]
 | 
			
		||||
 | 
			
		||||
source = choose(sourcet)
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ progression_balancing:
 | 
			
		|||
#  - "Big Keys"
 | 
			
		||||
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
 | 
			
		||||
#  - "Progressive Weapons"
 | 
			
		||||
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk. 
 | 
			
		||||
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
 | 
			
		||||
#  - "Master Sword Pedestal"
 | 
			
		||||
 | 
			
		||||
A Link to the Past:
 | 
			
		||||
| 
						 | 
				
			
			@ -261,7 +261,7 @@ A Link to the Past:
 | 
			
		|||
    25: 0   # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
 | 
			
		||||
    50: 0   # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
 | 
			
		||||
    75: 0   # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
 | 
			
		||||
    100: 0  # All junk fill items (rupees, bombs and arrows) are replaced with bees 
 | 
			
		||||
    100: 0  # All junk fill items (rupees, bombs and arrows) are replaced with bees
 | 
			
		||||
  beemizer_trap_chance:
 | 
			
		||||
    60: 50  # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
 | 
			
		||||
    70: 0   # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										2
									
								
								setup.py
								
								
								
								
							| 
						 | 
				
			
			@ -82,6 +82,8 @@ scripts = {
 | 
			
		|||
    "MinecraftClient.py": ("ArchipelagoMinecraftClient", False, mcicon),
 | 
			
		||||
    # Ocarina of Time
 | 
			
		||||
    "OoTAdjuster.py": ("ArchipelagoOoTAdjuster", True, icon),
 | 
			
		||||
    # FF1
 | 
			
		||||
    "FF1Client.py": ("ArchipelagoFF1Client", True, icon),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
exes = []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
import json
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Dict, Set, NamedTuple, List
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Item
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ItemData(NamedTuple):
 | 
			
		||||
    name: str
 | 
			
		||||
    code: int
 | 
			
		||||
    item_type: str
 | 
			
		||||
    progression: bool
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FF1_BRIDGE = 'Bridge'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FF1_STARTER_ITEMS = [
 | 
			
		||||
    "Ship"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FF1_PROGRESSION_LIST = [
 | 
			
		||||
    "Rod", "Cube", "Lute", "Key", "Chime", "Oxyale",
 | 
			
		||||
    "Ship", "Canoe", "Floater", "Canal",
 | 
			
		||||
    "Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle",
 | 
			
		||||
    "Shard",
 | 
			
		||||
    "EarthOrb", "FireOrb", "WaterOrb", "AirOrb"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FF1Items:
 | 
			
		||||
    _item_table: List[ItemData] = []
 | 
			
		||||
    _item_table_lookup: Dict[str, ItemData] = {}
 | 
			
		||||
 | 
			
		||||
    def _populate_item_table_from_data(self):
 | 
			
		||||
        base_path = Path(__file__).parent
 | 
			
		||||
        file_path = (base_path / "data/items.json").resolve()
 | 
			
		||||
        with open(file_path) as file:
 | 
			
		||||
            items = json.load(file)
 | 
			
		||||
            # Hardcode progression and categories for now
 | 
			
		||||
            self._item_table = [ItemData(name, code, "FF1Item", name in FF1_PROGRESSION_LIST)
 | 
			
		||||
                                for name, code in items.items()]
 | 
			
		||||
            self._item_table_lookup = {item.name: item for item in self._item_table}
 | 
			
		||||
 | 
			
		||||
    def _get_item_table(self) -> List[ItemData]:
 | 
			
		||||
        if not self._item_table or not self._item_table_lookup:
 | 
			
		||||
            self._populate_item_table_from_data()
 | 
			
		||||
        return self._item_table
 | 
			
		||||
 | 
			
		||||
    def _get_item_table_lookup(self) -> Dict[str, ItemData]:
 | 
			
		||||
        if not self._item_table or not self._item_table_lookup:
 | 
			
		||||
            self._populate_item_table_from_data()
 | 
			
		||||
        return self._item_table_lookup
 | 
			
		||||
 | 
			
		||||
    def get_item_names_per_category(self) -> Dict[str, Set[str]]:
 | 
			
		||||
        categories: Dict[str, Set[str]] = {}
 | 
			
		||||
 | 
			
		||||
        for item in self._get_item_table():
 | 
			
		||||
            categories.setdefault(item.item_type, set()).add(item.name)
 | 
			
		||||
 | 
			
		||||
        return categories
 | 
			
		||||
 | 
			
		||||
    def generate_item(self, name: str, player: int) -> Item:
 | 
			
		||||
        item = self._get_item_table_lookup().get(name)
 | 
			
		||||
        return Item(name, item.progression, item.code, player)
 | 
			
		||||
 | 
			
		||||
    def get_item_name_to_code_dict(self) -> Dict[str, int]:
 | 
			
		||||
        return {name: item.code for name, item in self._get_item_table_lookup().items()}
 | 
			
		||||
 | 
			
		||||
    def get_item(self, name: str) -> ItemData:
 | 
			
		||||
        return self._get_item_table_lookup()[name]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
import json
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Dict, NamedTuple, List, Optional
 | 
			
		||||
 | 
			
		||||
from BaseClasses import Region, RegionType, Location
 | 
			
		||||
 | 
			
		||||
EventId: Optional[int] = None
 | 
			
		||||
CHAOS_TERMINATED_EVENT = 'Terminated Chaos'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationData(NamedTuple):
 | 
			
		||||
    name: str
 | 
			
		||||
    address: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FF1Locations:
 | 
			
		||||
    _location_table: List[LocationData] = []
 | 
			
		||||
    _location_table_lookup: Dict[str, LocationData] = {}
 | 
			
		||||
 | 
			
		||||
    def _populate_item_table_from_data(self):
 | 
			
		||||
        base_path = Path(__file__).parent
 | 
			
		||||
        file_path = (base_path / "data/locations.json").resolve()
 | 
			
		||||
        with open(file_path) as file:
 | 
			
		||||
            locations = json.load(file)
 | 
			
		||||
            # Hardcode progression and categories for now
 | 
			
		||||
            self._location_table = [LocationData(name, code) for name, code in locations.items()]
 | 
			
		||||
            self._location_table_lookup = {item.name: item for item in self._location_table}
 | 
			
		||||
 | 
			
		||||
    def _get_location_table(self) -> List[LocationData]:
 | 
			
		||||
        if not self._location_table or not self._location_table_lookup:
 | 
			
		||||
            self._populate_item_table_from_data()
 | 
			
		||||
        return self._location_table
 | 
			
		||||
 | 
			
		||||
    def _get_location_table_lookup(self) -> Dict[str, LocationData]:
 | 
			
		||||
        if not self._location_table or not self._location_table_lookup:
 | 
			
		||||
            self._populate_item_table_from_data()
 | 
			
		||||
        return self._location_table_lookup
 | 
			
		||||
 | 
			
		||||
    def get_location_name_to_address_dict(self) -> Dict[str, int]:
 | 
			
		||||
        data = {name: location.address for name, location in self._get_location_table_lookup().items()}
 | 
			
		||||
        data[CHAOS_TERMINATED_EVENT] = EventId
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_menu_region(player: int, locations: Dict[str, int],
 | 
			
		||||
                           rules: Dict[str, List[List[str]]]) -> Region:
 | 
			
		||||
        menu_region = Region("Menu", RegionType.Generic, "Menu", player)
 | 
			
		||||
        for name, address in locations.items():
 | 
			
		||||
            location = Location(player, name, address, menu_region)
 | 
			
		||||
            ## TODO REMOVE WHEN LOGIC FOR TOFR IS CORRECT
 | 
			
		||||
            if "ToFR" in name:
 | 
			
		||||
                rules_list = [["Rod", "Cube", "Lute", "Key", "Chime", "Oxyale",
 | 
			
		||||
                               "Ship", "Canoe", "Floater", "Canal",
 | 
			
		||||
                               "Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle"]]
 | 
			
		||||
                location.access_rule = generate_rule(rules_list, player)
 | 
			
		||||
            elif name in rules:
 | 
			
		||||
                rules_list = rules[name]
 | 
			
		||||
                location.access_rule = generate_rule(rules_list, player)
 | 
			
		||||
            menu_region.locations.append(location)
 | 
			
		||||
 | 
			
		||||
        return menu_region
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_rule(rules_list, player):
 | 
			
		||||
    def x(state):
 | 
			
		||||
        for rule in rules_list:
 | 
			
		||||
            current_state = True
 | 
			
		||||
            for item in rule:
 | 
			
		||||
                if not state.has(item, player):
 | 
			
		||||
                    current_state = False
 | 
			
		||||
                    break
 | 
			
		||||
            if current_state:
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
    return x
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from Options import OptionDict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Locations(OptionDict):
 | 
			
		||||
    displayname = "locations"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Items(OptionDict):
 | 
			
		||||
    displayname = "items"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Rules(OptionDict):
 | 
			
		||||
    displayname = "rules"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ff1_options: Dict[str, OptionDict] = {
 | 
			
		||||
    "locations": Locations,
 | 
			
		||||
    "items": Items,
 | 
			
		||||
    "rules": Rules
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
from typing import Dict
 | 
			
		||||
from BaseClasses import Item, Location, MultiWorld
 | 
			
		||||
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
 | 
			
		||||
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
 | 
			
		||||
from .Options import ff1_options
 | 
			
		||||
from ..AutoWorld import World
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FF1World(World):
 | 
			
		||||
    """
 | 
			
		||||
    Final Fantasy 1, originally released on the NES on 1987, is the game that started the beloved, long running series.
 | 
			
		||||
    The randomizer takes the original 8-bit Final Fantasy game for NES (USA edition) and allows you to
 | 
			
		||||
    shuffle important aspects like the location of key items, the difficulty of monsters and fiends,
 | 
			
		||||
    and even the location of towns and dungeons.
 | 
			
		||||
    Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    options = ff1_options
 | 
			
		||||
    game = "Final Fantasy"
 | 
			
		||||
    topology_present = False
 | 
			
		||||
    remote_items = True
 | 
			
		||||
    data_version = 0
 | 
			
		||||
    remote_start_inventory = True
 | 
			
		||||
 | 
			
		||||
    ff1_items = FF1Items()
 | 
			
		||||
    ff1_locations = FF1Locations()
 | 
			
		||||
    item_name_groups = ff1_items.get_item_names_per_category()
 | 
			
		||||
    item_name_to_id = ff1_items.get_item_name_to_code_dict()
 | 
			
		||||
    location_name_to_id = ff1_locations.get_location_name_to_address_dict()
 | 
			
		||||
 | 
			
		||||
    def __init__(self, world: MultiWorld, player: int):
 | 
			
		||||
        super().__init__(world, player)
 | 
			
		||||
        self.locked_items = []
 | 
			
		||||
        self.locked_locations = []
 | 
			
		||||
 | 
			
		||||
    def generate_early(self):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def create_regions(self):
 | 
			
		||||
        locations = get_options(self.world, 'locations', self.player)
 | 
			
		||||
        rules = get_options(self.world, 'rules', self.player)
 | 
			
		||||
        menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
 | 
			
		||||
        terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
 | 
			
		||||
        terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player)
 | 
			
		||||
        terminated_event.place_locked_item(terminated_item)
 | 
			
		||||
 | 
			
		||||
        items = get_options(self.world, 'items', self.player)
 | 
			
		||||
        goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]],
 | 
			
		||||
                                  self.player)
 | 
			
		||||
        if "Shard" in items.keys():
 | 
			
		||||
            def goal_rule_and_shards(state):
 | 
			
		||||
                return goal_rule(state) and state.has("Shard", self.player, 32)
 | 
			
		||||
            terminated_event.access_rule = goal_rule_and_shards
 | 
			
		||||
 | 
			
		||||
        menu_region.locations.append(terminated_event)
 | 
			
		||||
        self.world.regions += [menu_region]
 | 
			
		||||
 | 
			
		||||
    def create_item(self, name: str) -> Item:
 | 
			
		||||
        return self.ff1_items.generate_item(name, self.player)
 | 
			
		||||
 | 
			
		||||
    def set_rules(self):
 | 
			
		||||
        self.world.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player)
 | 
			
		||||
 | 
			
		||||
    def generate_basic(self):
 | 
			
		||||
        items = get_options(self.world, 'items', self.player)
 | 
			
		||||
        if FF1_BRIDGE in items.keys():
 | 
			
		||||
            self._place_locked_item_in_sphere0(FF1_BRIDGE)
 | 
			
		||||
        if items:
 | 
			
		||||
            possible_early_items = [name for name in FF1_STARTER_ITEMS if name in items.keys()]
 | 
			
		||||
            if possible_early_items:
 | 
			
		||||
                progression_item = self.world.random.choice(possible_early_items)
 | 
			
		||||
                self._place_locked_item_in_sphere0(progression_item)
 | 
			
		||||
        items = [self.create_item(name) for name, data in items.items() for x in range(data['count']) if name not in
 | 
			
		||||
                 self.locked_items]
 | 
			
		||||
 | 
			
		||||
        self.world.itempool += items
 | 
			
		||||
 | 
			
		||||
    def _place_locked_item_in_sphere0(self, progression_item: str):
 | 
			
		||||
        if progression_item:
 | 
			
		||||
            rules = get_options(self.world, 'rules', self.player)
 | 
			
		||||
            sphere_0_locations = [name for name, rules in rules.items()
 | 
			
		||||
                                  if rules and len(rules[0]) == 0 and name not in self.locked_locations]
 | 
			
		||||
            if sphere_0_locations:
 | 
			
		||||
                initial_location = self.world.random.choice(sphere_0_locations)
 | 
			
		||||
                locked_location = self.world.get_location(initial_location, self.player)
 | 
			
		||||
                locked_location.place_locked_item(self.create_item(progression_item))
 | 
			
		||||
                self.locked_items.append(progression_item)
 | 
			
		||||
                self.locked_locations.append(locked_location.name)
 | 
			
		||||
 | 
			
		||||
    def fill_slot_data(self) -> Dict[str, object]:
 | 
			
		||||
        slot_data: Dict[str, object] = {}
 | 
			
		||||
 | 
			
		||||
        return slot_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_options(world: MultiWorld, name: str, player: int):
 | 
			
		||||
    return getattr(world, name, None)[player].value
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,194 @@
 | 
			
		|||
{
 | 
			
		||||
  "None": 256,
 | 
			
		||||
  "Lute": 257,
 | 
			
		||||
  "Crown": 258,
 | 
			
		||||
  "Crystal": 259,
 | 
			
		||||
  "Herb": 260,
 | 
			
		||||
  "Key": 261,
 | 
			
		||||
  "Tnt": 262,
 | 
			
		||||
  "Adamant": 263,
 | 
			
		||||
  "Slab": 264,
 | 
			
		||||
  "Ruby": 265,
 | 
			
		||||
  "Rod": 266,
 | 
			
		||||
  "Floater": 267,
 | 
			
		||||
  "Chime": 268,
 | 
			
		||||
  "Tail": 269,
 | 
			
		||||
  "Cube": 270,
 | 
			
		||||
  "Bottle": 271,
 | 
			
		||||
  "Oxyale": 272,
 | 
			
		||||
  "EarthOrb": 273,
 | 
			
		||||
  "FireOrb": 274,
 | 
			
		||||
  "WaterOrb": 275,
 | 
			
		||||
  "AirOrb": 276,
 | 
			
		||||
  "Shard": 277,
 | 
			
		||||
  "Tent": 278,
 | 
			
		||||
  "Cabin": 279,
 | 
			
		||||
  "House": 280,
 | 
			
		||||
  "Heal": 281,
 | 
			
		||||
  "Pure": 282,
 | 
			
		||||
  "Soft": 283,
 | 
			
		||||
  "WoodenNunchucks": 284,
 | 
			
		||||
  "SmallKnife": 285,
 | 
			
		||||
  "WoodenRod": 286,
 | 
			
		||||
  "Rapier": 287,
 | 
			
		||||
  "IronHammer": 288,
 | 
			
		||||
  "ShortSword": 289,
 | 
			
		||||
  "HandAxe": 290,
 | 
			
		||||
  "Scimitar": 291,
 | 
			
		||||
  "IronNunchucks": 292,
 | 
			
		||||
  "LargeKnife": 293,
 | 
			
		||||
  "IronStaff": 294,
 | 
			
		||||
  "Sabre": 295,
 | 
			
		||||
  "LongSword": 296,
 | 
			
		||||
  "GreatAxe": 297,
 | 
			
		||||
  "Falchon": 298,
 | 
			
		||||
  "SilverKnife": 299,
 | 
			
		||||
  "SilverSword": 300,
 | 
			
		||||
  "SilverHammer": 301,
 | 
			
		||||
  "SilverAxe": 302,
 | 
			
		||||
  "FlameSword": 303,
 | 
			
		||||
  "IceSword": 304,
 | 
			
		||||
  "DragonSword": 305,
 | 
			
		||||
  "GiantSword": 306,
 | 
			
		||||
  "SunSword": 307,
 | 
			
		||||
  "CoralSword": 308,
 | 
			
		||||
  "WereSword": 309,
 | 
			
		||||
  "RuneSword": 310,
 | 
			
		||||
  "PowerRod": 311,
 | 
			
		||||
  "LightAxe": 312,
 | 
			
		||||
  "HealRod": 313,
 | 
			
		||||
  "MageRod": 314,
 | 
			
		||||
  "Defense": 315,
 | 
			
		||||
  "WizardRod": 316,
 | 
			
		||||
  "Vorpal": 317,
 | 
			
		||||
  "CatClaw": 318,
 | 
			
		||||
  "ThorHammer": 319,
 | 
			
		||||
  "BaneSword": 320,
 | 
			
		||||
  "Katana": 321,
 | 
			
		||||
  "Xcalber": 322,
 | 
			
		||||
  "Masamune": 323,
 | 
			
		||||
  "Cloth": 324,
 | 
			
		||||
  "WoodenArmor": 325,
 | 
			
		||||
  "ChainArmor": 326,
 | 
			
		||||
  "IronArmor": 327,
 | 
			
		||||
  "SteelArmor": 328,
 | 
			
		||||
  "SilverArmor": 329,
 | 
			
		||||
  "FlameArmor": 330,
 | 
			
		||||
  "IceArmor": 331,
 | 
			
		||||
  "OpalArmor": 332,
 | 
			
		||||
  "DragonArmor": 333,
 | 
			
		||||
  "Copper": 334,
 | 
			
		||||
  "Silver": 335,
 | 
			
		||||
  "Gold": 336,
 | 
			
		||||
  "Opal": 337,
 | 
			
		||||
  "WhiteShirt": 338,
 | 
			
		||||
  "BlackShirt": 339,
 | 
			
		||||
  "WoodenShield": 340,
 | 
			
		||||
  "IronShield": 341,
 | 
			
		||||
  "SilverShield": 342,
 | 
			
		||||
  "FlameShield": 343,
 | 
			
		||||
  "IceShield": 344,
 | 
			
		||||
  "OpalShield": 345,
 | 
			
		||||
  "AegisShield": 346,
 | 
			
		||||
  "Buckler": 347,
 | 
			
		||||
  "ProCape": 348,
 | 
			
		||||
  "Cap": 349,
 | 
			
		||||
  "WoodenHelm": 350,
 | 
			
		||||
  "IronHelm": 351,
 | 
			
		||||
  "SilverHelm": 352,
 | 
			
		||||
  "OpalHelm": 353,
 | 
			
		||||
  "HealHelm": 354,
 | 
			
		||||
  "Ribbon": 355,
 | 
			
		||||
  "Gloves": 356,
 | 
			
		||||
  "CopperGauntlets": 357,
 | 
			
		||||
  "IronGauntlets": 358,
 | 
			
		||||
  "SilverGauntlets": 359,
 | 
			
		||||
  "ZeusGauntlets": 360,
 | 
			
		||||
  "PowerGauntlets": 361,
 | 
			
		||||
  "OpalGauntlets": 362,
 | 
			
		||||
  "ProRing": 363,
 | 
			
		||||
  "Gold10": 364,
 | 
			
		||||
  "Gold20": 365,
 | 
			
		||||
  "Gold25": 366,
 | 
			
		||||
  "Gold30": 367,
 | 
			
		||||
  "Gold55": 368,
 | 
			
		||||
  "Gold70": 369,
 | 
			
		||||
  "Gold85": 370,
 | 
			
		||||
  "Gold110": 371,
 | 
			
		||||
  "Gold135": 372,
 | 
			
		||||
  "Gold155": 373,
 | 
			
		||||
  "Gold160": 374,
 | 
			
		||||
  "Gold180": 375,
 | 
			
		||||
  "Gold240": 376,
 | 
			
		||||
  "Gold255": 377,
 | 
			
		||||
  "Gold260": 378,
 | 
			
		||||
  "Gold295": 379,
 | 
			
		||||
  "Gold300": 380,
 | 
			
		||||
  "Gold315": 381,
 | 
			
		||||
  "Gold330": 382,
 | 
			
		||||
  "Gold350": 383,
 | 
			
		||||
  "Gold385": 384,
 | 
			
		||||
  "Gold400": 385,
 | 
			
		||||
  "Gold450": 386,
 | 
			
		||||
  "Gold500": 387,
 | 
			
		||||
  "Gold530": 388,
 | 
			
		||||
  "Gold575": 389,
 | 
			
		||||
  "Gold620": 390,
 | 
			
		||||
  "Gold680": 391,
 | 
			
		||||
  "Gold750": 392,
 | 
			
		||||
  "Gold795": 393,
 | 
			
		||||
  "Gold880": 394,
 | 
			
		||||
  "Gold1020": 395,
 | 
			
		||||
  "Gold1250": 396,
 | 
			
		||||
  "Gold1455": 397,
 | 
			
		||||
  "Gold1520": 398,
 | 
			
		||||
  "Gold1760": 399,
 | 
			
		||||
  "Gold1975": 400,
 | 
			
		||||
  "Gold2000": 401,
 | 
			
		||||
  "Gold2750": 402,
 | 
			
		||||
  "Gold3400": 403,
 | 
			
		||||
  "Gold4150": 404,
 | 
			
		||||
  "Gold5000": 405,
 | 
			
		||||
  "Gold5450": 406,
 | 
			
		||||
  "Gold6400": 407,
 | 
			
		||||
  "Gold6720": 408,
 | 
			
		||||
  "Gold7340": 409,
 | 
			
		||||
  "Gold7690": 410,
 | 
			
		||||
  "Gold7900": 411,
 | 
			
		||||
  "Gold8135": 412,
 | 
			
		||||
  "Gold9000": 413,
 | 
			
		||||
  "Gold9300": 414,
 | 
			
		||||
  "Gold9500": 415,
 | 
			
		||||
  "Gold9900": 416,
 | 
			
		||||
  "Gold10000": 417,
 | 
			
		||||
  "Gold12350": 418,
 | 
			
		||||
  "Gold13000": 419,
 | 
			
		||||
  "Gold13450": 420,
 | 
			
		||||
  "Gold14050": 421,
 | 
			
		||||
  "Gold14720": 422,
 | 
			
		||||
  "Gold15000": 423,
 | 
			
		||||
  "Gold17490": 424,
 | 
			
		||||
  "Gold18010": 425,
 | 
			
		||||
  "Gold19990": 426,
 | 
			
		||||
  "Gold20000": 427,
 | 
			
		||||
  "Gold20010": 428,
 | 
			
		||||
  "Gold26000": 429,
 | 
			
		||||
  "Gold45000": 430,
 | 
			
		||||
  "Gold65000": 431,
 | 
			
		||||
  "Smoke": 435,
 | 
			
		||||
  "FullCure": 432,
 | 
			
		||||
  "Blast": 434,
 | 
			
		||||
  "Phoenix": 433,
 | 
			
		||||
  "Flare": 437,
 | 
			
		||||
  "Black": 438,
 | 
			
		||||
  "Refresh": 436,
 | 
			
		||||
  "Guard":  439,
 | 
			
		||||
  "Wizard": 442,
 | 
			
		||||
  "HighPotion": 441,
 | 
			
		||||
  "Cloak": 443,
 | 
			
		||||
  "Quick": 440,
 | 
			
		||||
  "Ship": 480,
 | 
			
		||||
  "Bridge": 488,
 | 
			
		||||
  "Canal": 492,
 | 
			
		||||
  "Canoe": 498
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,257 @@
 | 
			
		|||
{
 | 
			
		||||
  "Coneria1": 257,
 | 
			
		||||
  "Coneria2": 258,
 | 
			
		||||
  "ConeriaMajor": 259,
 | 
			
		||||
  "Coneria4": 260,
 | 
			
		||||
  "Coneria5": 261,
 | 
			
		||||
  "Coneria6": 262,
 | 
			
		||||
  "MatoyasCave1": 299,
 | 
			
		||||
  "MatoyasCave3": 301,
 | 
			
		||||
  "MatoyasCave2": 300,
 | 
			
		||||
  "NorthwestCastle1": 273,
 | 
			
		||||
  "NorthwestCastle3": 275,
 | 
			
		||||
  "NorthwestCastle2": 274,
 | 
			
		||||
  "ToFTopLeft1": 263,
 | 
			
		||||
  "ToFBottomLeft": 265,
 | 
			
		||||
  "ToFTopLeft2": 264,
 | 
			
		||||
  "ToFRevisited6": 509,
 | 
			
		||||
  "ToFRevisited4": 507,
 | 
			
		||||
  "ToFRMasmune": 504,
 | 
			
		||||
  "ToFRevisited5": 508,
 | 
			
		||||
  "ToFRevisited3": 506,
 | 
			
		||||
  "ToFRevisited2": 505,
 | 
			
		||||
  "ToFRevisited7": 510,
 | 
			
		||||
  "ToFTopRight1": 267,
 | 
			
		||||
  "ToFTopRight2": 268,
 | 
			
		||||
  "ToFBottomRight": 266,
 | 
			
		||||
  "IceCave15": 377,
 | 
			
		||||
  "IceCave16": 378,
 | 
			
		||||
  "IceCave9": 371,
 | 
			
		||||
  "IceCave11": 373,
 | 
			
		||||
  "IceCave10": 372,
 | 
			
		||||
  "IceCave12": 374,
 | 
			
		||||
  "IceCave13": 375,
 | 
			
		||||
  "IceCave14": 376,
 | 
			
		||||
  "IceCave1": 363,
 | 
			
		||||
  "IceCave2": 364,
 | 
			
		||||
  "IceCave3": 365,
 | 
			
		||||
  "IceCave4": 366,
 | 
			
		||||
  "IceCave5": 367,
 | 
			
		||||
  "IceCaveMajor": 370,
 | 
			
		||||
  "IceCave7": 369,
 | 
			
		||||
  "IceCave6": 368,
 | 
			
		||||
  "Elfland1": 269,
 | 
			
		||||
  "Elfland2": 270,
 | 
			
		||||
  "Elfland3": 271,
 | 
			
		||||
  "Elfland4": 272,
 | 
			
		||||
  "Ordeals5": 383,
 | 
			
		||||
  "Ordeals6": 384,
 | 
			
		||||
  "Ordeals7": 385,
 | 
			
		||||
  "Ordeals1": 379,
 | 
			
		||||
  "Ordeals2": 380,
 | 
			
		||||
  "Ordeals3": 381,
 | 
			
		||||
  "Ordeals4": 382,
 | 
			
		||||
  "OrdealsMajor": 387,
 | 
			
		||||
  "Ordeals8": 386,
 | 
			
		||||
  "SeaShrine7": 411,
 | 
			
		||||
  "SeaShrine8": 412,
 | 
			
		||||
  "SeaShrine9": 413,
 | 
			
		||||
  "SeaShrine10": 414,
 | 
			
		||||
  "SeaShrine1": 405,
 | 
			
		||||
  "SeaShrine2": 406,
 | 
			
		||||
  "SeaShrine3": 407,
 | 
			
		||||
  "SeaShrine4": 408,
 | 
			
		||||
  "SeaShrine5": 409,
 | 
			
		||||
  "SeaShrine6": 410,
 | 
			
		||||
  "SeaShrine13": 417,
 | 
			
		||||
  "SeaShrine14": 418,
 | 
			
		||||
  "SeaShrine11": 415,
 | 
			
		||||
  "SeaShrine15": 419,
 | 
			
		||||
  "SeaShrine16": 420,
 | 
			
		||||
  "SeaShrineLocked": 421,
 | 
			
		||||
  "SeaShrine18": 422,
 | 
			
		||||
  "SeaShrine19": 423,
 | 
			
		||||
  "SeaShrine20": 424,
 | 
			
		||||
  "SeaShrine23": 427,
 | 
			
		||||
  "SeaShrine21": 425,
 | 
			
		||||
  "SeaShrine22": 426,
 | 
			
		||||
  "SeaShrine24": 428,
 | 
			
		||||
  "SeaShrine26": 430,
 | 
			
		||||
  "SeaShrine28": 432,
 | 
			
		||||
  "SeaShrine25": 429,
 | 
			
		||||
  "SeaShrine30": 434,
 | 
			
		||||
  "SeaShrine31": 435,
 | 
			
		||||
  "SeaShrine27": 431,
 | 
			
		||||
  "SeaShrine29": 433,
 | 
			
		||||
  "SeaShrineMajor": 436,
 | 
			
		||||
  "SeaShrine12": 416,
 | 
			
		||||
  "DwarfCave3": 291,
 | 
			
		||||
  "DwarfCave4": 292,
 | 
			
		||||
  "DwarfCave6": 294,
 | 
			
		||||
  "DwarfCave7": 295,
 | 
			
		||||
  "DwarfCave5": 293,
 | 
			
		||||
  "DwarfCave8": 296,
 | 
			
		||||
  "DwarfCave9": 297,
 | 
			
		||||
  "DwarfCave10": 298,
 | 
			
		||||
  "DwarfCave1": 289,
 | 
			
		||||
  "DwarfCave2": 290,
 | 
			
		||||
  "Waterfall1": 437,
 | 
			
		||||
  "Waterfall2": 438,
 | 
			
		||||
  "Waterfall3": 439,
 | 
			
		||||
  "Waterfall4": 440,
 | 
			
		||||
  "Waterfall5": 441,
 | 
			
		||||
  "Waterfall6": 442,
 | 
			
		||||
  "MirageTower5": 456,
 | 
			
		||||
  "MirageTower16": 467,
 | 
			
		||||
  "MirageTower17": 468,
 | 
			
		||||
  "MirageTower15": 466,
 | 
			
		||||
  "MirageTower18": 469,
 | 
			
		||||
  "MirageTower14": 465,
 | 
			
		||||
  "SkyPalace1": 470,
 | 
			
		||||
  "SkyPalace2": 471,
 | 
			
		||||
  "SkyPalace3": 472,
 | 
			
		||||
  "SkyPalace4": 473,
 | 
			
		||||
  "SkyPalace18": 487,
 | 
			
		||||
  "SkyPalace19": 488,
 | 
			
		||||
  "SkyPalace16": 485,
 | 
			
		||||
  "SkyPalaceMajor": 489,
 | 
			
		||||
  "SkyPalace17": 486,
 | 
			
		||||
  "SkyPalace22": 491,
 | 
			
		||||
  "SkyPalace21": 490,
 | 
			
		||||
  "SkyPalace23": 492,
 | 
			
		||||
  "SkyPalace24": 493,
 | 
			
		||||
  "SkyPalace31": 500,
 | 
			
		||||
  "SkyPalace32": 501,
 | 
			
		||||
  "SkyPalace33": 502,
 | 
			
		||||
  "SkyPalace34": 503,
 | 
			
		||||
  "SkyPalace29": 498,
 | 
			
		||||
  "SkyPalace26": 495,
 | 
			
		||||
  "SkyPalace25": 494,
 | 
			
		||||
  "SkyPalace28": 497,
 | 
			
		||||
  "SkyPalace27": 496,
 | 
			
		||||
  "SkyPalace30": 499,
 | 
			
		||||
  "SkyPalace14": 483,
 | 
			
		||||
  "SkyPalace11": 480,
 | 
			
		||||
  "SkyPalace12": 481,
 | 
			
		||||
  "SkyPalace13": 482,
 | 
			
		||||
  "SkyPalace15": 484,
 | 
			
		||||
  "SkyPalace10": 479,
 | 
			
		||||
  "SkyPalace5": 474,
 | 
			
		||||
  "SkyPalace6": 475,
 | 
			
		||||
  "SkyPalace7": 476,
 | 
			
		||||
  "SkyPalace8": 477,
 | 
			
		||||
  "SkyPalace9": 478,
 | 
			
		||||
  "MirageTower9": 460,
 | 
			
		||||
  "MirageTower13": 464,
 | 
			
		||||
  "MirageTower10": 461,
 | 
			
		||||
  "MirageTower12": 463,
 | 
			
		||||
  "MirageTower11": 462,
 | 
			
		||||
  "MirageTower1": 452,
 | 
			
		||||
  "MirageTower2": 453,
 | 
			
		||||
  "MirageTower4": 455,
 | 
			
		||||
  "MirageTower3": 454,
 | 
			
		||||
  "MirageTower8": 459,
 | 
			
		||||
  "MirageTower7": 458,
 | 
			
		||||
  "MirageTower6": 457,
 | 
			
		||||
  "Volcano30": 359,
 | 
			
		||||
  "Volcano32": 361,
 | 
			
		||||
  "Volcano31": 360,
 | 
			
		||||
  "Volcano28": 357,
 | 
			
		||||
  "Volcano29": 358,
 | 
			
		||||
  "Volcano21": 350,
 | 
			
		||||
  "Volcano20": 349,
 | 
			
		||||
  "Volcano24": 353,
 | 
			
		||||
  "Volcano19": 348,
 | 
			
		||||
  "Volcano25": 354,
 | 
			
		||||
  "VolcanoMajor": 362,
 | 
			
		||||
  "Volcano26": 355,
 | 
			
		||||
  "Volcano27": 356,
 | 
			
		||||
  "Volcano22": 351,
 | 
			
		||||
  "Volcano23": 352,
 | 
			
		||||
  "Volcano1": 330,
 | 
			
		||||
  "Volcano9": 338,
 | 
			
		||||
  "Volcano2": 331,
 | 
			
		||||
  "Volcano10": 339,
 | 
			
		||||
  "Volcano3": 332,
 | 
			
		||||
  "Volcano8": 337,
 | 
			
		||||
  "Volcano4": 333,
 | 
			
		||||
  "Volcano13": 342,
 | 
			
		||||
  "Volcano11": 340,
 | 
			
		||||
  "Volcano7": 336,
 | 
			
		||||
  "Volcano6": 335,
 | 
			
		||||
  "Volcano5": 334,
 | 
			
		||||
  "Volcano14": 343,
 | 
			
		||||
  "Volcano12": 341,
 | 
			
		||||
  "Volcano15": 344,
 | 
			
		||||
  "Volcano18": 347,
 | 
			
		||||
  "Volcano17": 346,
 | 
			
		||||
  "Volcano16": 345,
 | 
			
		||||
  "MarshCave6": 281,
 | 
			
		||||
  "MarshCave5": 280,
 | 
			
		||||
  "MarshCave7": 282,
 | 
			
		||||
  "MarshCave8": 283,
 | 
			
		||||
  "MarshCave10": 285,
 | 
			
		||||
  "MarshCave2": 277,
 | 
			
		||||
  "MarshCave11": 286,
 | 
			
		||||
  "MarshCave3": 278,
 | 
			
		||||
  "MarshCaveMajor": 284,
 | 
			
		||||
  "MarshCave12": 287,
 | 
			
		||||
  "MarshCave4": 279,
 | 
			
		||||
  "MarshCave1": 276,
 | 
			
		||||
  "MarshCave13": 288,
 | 
			
		||||
  "TitansTunnel1": 326,
 | 
			
		||||
  "TitansTunnel2": 327,
 | 
			
		||||
  "TitansTunnel3": 328,
 | 
			
		||||
  "TitansTunnel4": 329,
 | 
			
		||||
  "EarthCave1": 302,
 | 
			
		||||
  "EarthCave2": 303,
 | 
			
		||||
  "EarthCave5": 306,
 | 
			
		||||
  "EarthCave3": 304,
 | 
			
		||||
  "EarthCave4": 305,
 | 
			
		||||
  "EarthCave9": 310,
 | 
			
		||||
  "EarthCave10": 311,
 | 
			
		||||
  "EarthCave11": 312,
 | 
			
		||||
  "EarthCave6": 307,
 | 
			
		||||
  "EarthCave7": 308,
 | 
			
		||||
  "EarthCave12": 313,
 | 
			
		||||
  "EarthCaveMajor": 317,
 | 
			
		||||
  "EarthCave19": 320,
 | 
			
		||||
  "EarthCave17": 318,
 | 
			
		||||
  "EarthCave18": 319,
 | 
			
		||||
  "EarthCave20": 321,
 | 
			
		||||
  "EarthCave24": 325,
 | 
			
		||||
  "EarthCave21": 322,
 | 
			
		||||
  "EarthCave22": 323,
 | 
			
		||||
  "EarthCave23": 324,
 | 
			
		||||
  "EarthCave13": 314,
 | 
			
		||||
  "EarthCave15": 316,
 | 
			
		||||
  "EarthCave14": 315,
 | 
			
		||||
  "EarthCave8": 309,
 | 
			
		||||
  "Cardia11": 398,
 | 
			
		||||
  "Cardia9": 396,
 | 
			
		||||
  "Cardia10": 397,
 | 
			
		||||
  "Cardia6": 393,
 | 
			
		||||
  "Cardia8": 395,
 | 
			
		||||
  "Cardia7": 394,
 | 
			
		||||
  "Cardia13": 400,
 | 
			
		||||
  "Cardia12": 399,
 | 
			
		||||
  "Cardia4": 391,
 | 
			
		||||
  "Cardia5": 392,
 | 
			
		||||
  "Cardia3": 390,
 | 
			
		||||
  "Cardia1": 388,
 | 
			
		||||
  "Cardia2": 389,
 | 
			
		||||
  "CaravanShop": 767,
 | 
			
		||||
  "King": 513,
 | 
			
		||||
  "Princess2": 530,
 | 
			
		||||
  "Matoya": 522,
 | 
			
		||||
  "Astos": 519,
 | 
			
		||||
  "Bikke": 516,
 | 
			
		||||
  "CanoeSage": 533,
 | 
			
		||||
  "ElfPrince": 518,
 | 
			
		||||
  "Nerrick": 520,
 | 
			
		||||
  "Smith": 521,
 | 
			
		||||
  "CubeBot": 529,
 | 
			
		||||
  "Sarda": 525,
 | 
			
		||||
  "Fairy": 531,
 | 
			
		||||
  "Lefein": 527
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue