From 0585caf651e2b551d885ba7ea98f2dba55ce5ca1 Mon Sep 17 00:00:00 2001 From: Kono Tyran Date: Mon, 10 May 2021 20:36:56 -0700 Subject: [PATCH 1/7] fix !reamining only looking at alttp item pool. --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 0f383f992329..6fca84879cd6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -786,7 +786,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -799,7 +799,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") for item_id in remaining_item_ids)) else: self.output("No remaining items found.") From b786900eb1e740fbce6a40348fc42b076087ba17 Mon Sep 17 00:00:00 2001 From: Kono Tyran Date: Mon, 10 May 2021 22:26:39 -0700 Subject: [PATCH 2/7] update tutorial to reflect change to /connect command. --- WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md index 02706350e8a3..751ff5ecee2e 100644 --- a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md +++ b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md @@ -126,8 +126,8 @@ previously. After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client. -Once in game type `/connect ()` where `` is the address of the -Archipelago server. `()` +Once in game type `/connect (Port) (Password)` where `` is the address of the +Archipelago server. `(Port)` is only required if the ap server is not using the default port of 38281. `(Password)` is only required if the Archipleago server you are using has a password set. ### Play the game From 1b2283b1734a8f24ddaa8c754367510e881be597 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 11 May 2021 13:28:58 +0200 Subject: [PATCH 3/7] Factorio: correctly cache control_template to allow multiple Factorio worlds --- worlds/factorio/Mod.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 1558d24a0ab2..2a9aa95773b9 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -15,6 +15,7 @@ template: Optional[jinja2.Template] = None locale_template: Optional[jinja2.Template] = None +control_template: Optional[jinja2.Template] = None template_load_lock = threading.Lock() @@ -46,7 +47,7 @@ } def generate_mod(world: MultiWorld, player: int, seedname: str): - global template, locale_template + global template, locale_template, control_template with template_load_lock: if not template: mod_template_folder = Utils.local_path("data", "factorio", "mod_template") From a23185091178e1c58b59a4dedc92e06e60a3f963 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 11 May 2021 23:08:50 +0200 Subject: [PATCH 4/7] Make hint costs relative --- CommonClient.py | 8 ++--- Main.py | 15 ++++---- MultiServer.py | 86 ++++++++++++++++++++++------------------------ Mystery.py | 5 ++- Utils.py | 2 +- host.yaml | 3 +- worlds/__init__.py | 2 ++ 7 files changed, 62 insertions(+), 59 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 019828f394e9..b56c5df398dd 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -129,7 +129,7 @@ def __init__(self, server_address, password, found_items: bool): self.input_requests = 0 # game state - self.player_names: typing.Dict[int: str] = {0: "Server"} + self.player_names: typing.Dict[int: str] = {0: "Archipelago"} self.exit_event = asyncio.Event() self.watcher_event = asyncio.Event() @@ -194,7 +194,7 @@ async def send_msgs(self, msgs): def consume_players_package(self, package: typing.List[tuple]): self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team} - self.player_names[0] = "Server" + self.player_names[0] = "Archipelago" def event_invalid_slot(self): raise Exception('Invalid Slot; please verify that you have connected to the correct world.') @@ -305,8 +305,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict): logger.info('Password required') logger.info(f"Forfeit setting: {args['forfeit_mode']}") logger.info(f"Remaining setting: {args['remaining_mode']}") - logger.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}" - f" for each location checked.") + logger.info(f"A !hint costs {args['hint_cost']}% of checks points and you get {args['location_check_points']}" + f" for each location checked. Use !hint for more information.") ctx.hint_cost = int(args['hint_cost']) ctx.check_points = int(args['location_check_points']) ctx.forfeit_mode = args['forfeit_mode'] diff --git a/Main.py b/Main.py index 8dc772a892ff..cfa9116f34d0 100644 --- a/Main.py +++ b/Main.py @@ -7,7 +7,7 @@ import zlib import concurrent.futures import pickle -from typing import Dict +from typing import Dict, Tuple from BaseClasses import MultiWorld, CollectionState, Region, Item from worlds.alttp.Items import ItemFactory, item_name_groups @@ -501,7 +501,7 @@ def write_multidata(roms, mods): rom_names.append(rom_name) slot_data = {} client_versions = {} - minimum_versions = {"server": (0, 0, 4), "clients": client_versions} + minimum_versions = {"server": (0, 1, 1), "clients": client_versions} games = {} for slot in world.player_ids: client_versions[slot] = (0, 0, 3) @@ -520,6 +520,11 @@ def write_multidata(roms, mods): slots_data[option_name] = int(option.value) for slot in world.minecraft_player_ids: slot_data[slot] = fill_minecraft_slot_data(world, slot) + + locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids} + for location in world.get_filled_locations(): + if type(location.address) == int: + locations_data[location.player][location.address] = (location.item.code, location.item.player) multidata = zlib.compress(pickle.dumps({ "slot_data" : slot_data, "games": games, @@ -527,11 +532,7 @@ def write_multidata(roms, mods): "connect_names": connect_names, "remote_items": {player for player in range(1, world.players + 1) if world.remote_items[player]}, - "locations": { - (location.address, location.player): - (location.item.code, location.item.player) - for location in world.get_filled_locations() if - type(location.address) is int}, + "locations": locations_data, "checks_in_area": checks_in_area, "server_options": get_options()["server_options"], "er_hint_data": er_hint_data, diff --git a/MultiServer.py b/MultiServer.py index 0f383f992329..53e4387c6cc0 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -78,7 +78,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.connect_names = {} # names of slots clients can connect to self.allow_forfeits = {} self.remote_items = set() - self.locations = {} + self.locations:typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {} self.host = host self.port = port self.server_password = server_password @@ -114,6 +114,11 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} self.seed_name = "" + def get_hint_cost(self, slot): + if self.hint_cost: + return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot]))) + return 0 + def load(self, multidatapath: str, use_embedded_server_options: bool = False): with open(multidatapath, 'rb') as f: data = f.read() @@ -132,7 +137,7 @@ def _load(self, decoded_obj: dict, use_embedded_server_options: bool): mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > Utils._version_tuple: - raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver}," + raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," f"however this server is of version {Utils._version_tuple}") clients_ver = decoded_obj["minimum_versions"].get("clients", {}) self.minimum_client_versions = {} @@ -437,10 +442,6 @@ def get_received_items(ctx: Context, team: int, player: int) -> typing.List[Netw return ctx.received_items.setdefault((team, player), []) -def tuplize_received_items(items): - return [NetworkItem(item.item, item.location, item.player) for item in items] - - def send_new_items(ctx: Context): for client in ctx.endpoints: if client.auth: # can't send to disconnected client @@ -449,22 +450,22 @@ def send_new_items(ctx: Context): asyncio.create_task(ctx.send_msgs(client, [{ "cmd": "ReceivedItems", "index": client.send_index, - "items": tuplize_received_items(items)[client.send_index:]}])) + "items": items[client.send_index:]}])) client.send_index = len(items) def forfeit_player(ctx: Context, team: int, slot: int): # register any locations that are in the multidata - all_locations = {location_id for location_id, location_slot in ctx.locations if location_slot == slot} + all_locations = set(ctx.locations[slot]) ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1)) register_location_checks(ctx, team, slot, all_locations) def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: items = [] - for (location, location_slot) in ctx.locations: - if location_slot == slot and location not in ctx.location_checks[team, slot]: - items.append(ctx.locations[location, slot][0]) # item ID + for location_id in ctx.locations[slot]: + if location_id not in ctx.location_checks[team, slot]: + items.append(ctx.locations[slot][location_id][0]) # item ID return sorted(items) @@ -473,8 +474,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi if new_locations: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) for location in new_locations: - if (location, slot) in ctx.locations: - item_id, target_player = ctx.locations[(location, slot)] + if location in ctx.locations[slot]: + item_id, target_player = ctx.locations[slot][location] new_item = NetworkItem(item_id, location, slot) if target_player != slot or slot in ctx.remote_items: get_received_items(ctx, team, target_player).append(new_item) @@ -504,29 +505,26 @@ def notify_team(ctx: Context, team: int, text: str): def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: hints = [] seeked_item_id = lookup_any_item_name_to_id[item] - for check, result in ctx.locations.items(): - item_id, receiving_player = result - if receiving_player == slot and item_id == seeked_item_id: - location_id, finding_player = check - found = location_id in ctx.location_checks[team, finding_player] - entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") - hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance)) + for finding_player, check_data in ctx.locations.items(): + for location_id, result in check_data.items(): + item_id, receiving_player = result + if receiving_player == slot and item_id == seeked_item_id: + found = location_id in ctx.location_checks[team, finding_player] + entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") + hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance)) return hints def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: - hints = [] - seeked_location = Regions.lookup_name_to_id[location] - for check, result in ctx.locations.items(): - location_id, finding_player = check - if finding_player == slot and location_id == seeked_location: - item_id, receiving_player = result - found = location_id in ctx.location_checks[team, finding_player] - entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") - hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance)) - break # each location has 1 item - return hints + + seeked_location: int = Regions.lookup_name_to_id[location] + item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None)) + if item_id: + found = seeked_location in ctx.location_checks[team, slot] + entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") + return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance)] + return [] def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: @@ -864,7 +862,7 @@ def _cmd_hint(self, item_or_location: str = "") -> bool: """Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """ points_available = get_client_points(self.ctx, self.client) if not item_or_location: - self.output(f"A hint costs {self.ctx.hint_cost} points. " + self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. " f"You have {points_available} points.") hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} @@ -885,7 +883,7 @@ def _cmd_hint(self, item_or_location: str = "") -> bool: hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name) else: # location name hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name) - + cost = self.ctx.get_hint_cost(self.client.slot) if hints: new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot] old_hints = set(hints) - new_hints @@ -899,8 +897,8 @@ def _cmd_hint(self, item_or_location: str = "") -> bool: if not not_found_hints: # everything's been found, no need to pay can_pay = 1000 - elif self.ctx.hint_cost: - can_pay = points_available // self.ctx.hint_cost + elif cost: + can_pay = points_available // cost else: can_pay = 1000 @@ -926,7 +924,7 @@ def _cmd_hint(self, item_or_location: str = "") -> bool: else: self.output(f"You can't afford the hint. " f"You have {points_available} points and need at least " - f"{self.ctx.hint_cost}") + f"{self.ctx.get_hint_cost(self.client.slot)}") notify_hints(self.ctx, self.client.team, hints) self.ctx.save() return True @@ -941,21 +939,19 @@ def _cmd_hint(self, item_or_location: str = "") -> bool: def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]: return [location_id for - location_id, slot in ctx.locations if - slot == client.slot and + location_id in ctx.locations[client.slot] if location_id in ctx.location_checks[client.team, client.slot]] def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]: return [location_id for - location_id, slot in ctx.locations if - slot == client.slot and + location_id in ctx.locations[client.slot] if location_id not in ctx.location_checks[client.team, client.slot]] def get_client_points(ctx: Context, client: Client) -> int: return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - - ctx.hint_cost * ctx.hints_used[client.team, client.slot]) + ctx.get_hint_cost(client.slot) * ctx.hints_used[client.team, client.slot]) async def process_client_cmd(ctx: Context, client: Client, args: dict): @@ -1032,7 +1028,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): }] items = get_received_items(ctx, client.team, client.slot) if items: - reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)}) + reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items}) client.send_index = len(items) await ctx.send_msgs(client, reply) @@ -1047,7 +1043,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): if items: client.send_index = len(items) await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0, - "items": tuplize_received_items(items)}]) + "items": items}]) elif cmd == 'LocationChecks': register_location_checks(ctx, client.team, client.slot, args["locations"]) @@ -1058,7 +1054,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): if type(location) is not int or location not in lookup_any_location_id_to_name: await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}]) return - target_item, target_player = ctx.locations[location, client.slot] + target_item, target_player = ctx.locations[client.slot][location] locs.append(NetworkItem(target_item, location, target_player)) await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) @@ -1206,7 +1202,7 @@ def _cmd_send(self, player_name: str, *item_name: str) -> bool: if usable: for client in self.ctx.endpoints: if client.name == seeked_player: - new_item = NetworkItem(lookup_any_item_name_to_id[item], -1, client.slot) + new_item = NetworkItem(lookup_any_item_name_to_id[item], -1, 0) get_received_items(self.ctx, client.team, client.slot).append(new_item) self.ctx.notify_all('Cheat console: sending "' + item + '" to ' + self.ctx.get_aliased_name(client.team, client.slot)) diff --git a/Mystery.py b/Mystery.py index aae386b2b158..4abe1183ed6e 100644 --- a/Mystery.py +++ b/Mystery.py @@ -303,7 +303,10 @@ def handle_name(name: str, player: int, name_counter: Counter): name] > 1 else ''), player=player, PLAYER=(player if player > 1 else ''))) - return new_name.strip().replace(' ', '_')[:16] + new_name = new_name.strip().replace(' ', '_')[:16] + if new_name == "Archipelago": + raise Exception(f"You cannot name yourself \"{new_name}\"") + return new_name def prefer_int(input_data: str) -> typing.Union[str, int]: diff --git a/Utils.py b/Utils.py index 55b047ce609a..adaef75f2650 100644 --- a/Utils.py +++ b/Utils.py @@ -12,7 +12,7 @@ class Version(typing.NamedTuple): minor: int build: int -__version__ = "0.1.0" +__version__ = "0.1.1" _version_tuple = tuplize_version(__version__) import builtins diff --git a/host.yaml b/host.yaml index 68c72c4d7416..46e13d7fbd55 100644 --- a/host.yaml +++ b/host.yaml @@ -19,7 +19,8 @@ server_options: # Client hint system # Points given to a player for each acquired item in their world location_check_points: 1 - # Point cost to receive a hint via !hint for players + # Relative point cost to receive a hint via !hint for players + # so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5 hint_cost: 1000 # Set to 0 if you want free hints # Forfeit modes # "disabled" -> clients can't forfeit, diff --git a/worlds/__init__.py b/worlds/__init__.py index 14744e625d5c..5222f2cbd0f2 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -13,6 +13,7 @@ lookup_any_item_id_to_name = {**alttp, **hk, **Technologies.lookup_id_to_name, **mc} assert len(alttp) + len(hk) + len(Technologies.lookup_id_to_name) + len(mc) == len(lookup_any_item_id_to_name) lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()} +# assert len(lookup_any_item_name_to_id) == len(lookup_any_item_id_to_name) # currently broken: Single Arrow from .alttp import Regions from .hk import Locations @@ -24,6 +25,7 @@ len(Technologies.lookup_id_to_name) + len(Advancements.lookup_id_to_name) == \ len(lookup_any_location_id_to_name) lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_id_to_name.items()} +assert len(lookup_any_location_name_to_id) == len(lookup_any_location_id_to_name) network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name, "lookup_any_item_id_to_name": lookup_any_item_id_to_name, From 5cdf7c7dc511a99213801f916418e8859ac476ea Mon Sep 17 00:00:00 2001 From: Kono Tyran Date: Mon, 10 May 2021 20:36:56 -0700 Subject: [PATCH 5/7] fix !reamining only looking at alttp item pool. --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 53e4387c6cc0..23d96be51580 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -784,7 +784,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -797,7 +797,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") for item_id in remaining_item_ids)) else: self.output("No remaining items found.") From 76a219eea5a957923c327bf08bc2b459282afc13 Mon Sep 17 00:00:00 2001 From: Kono Tyran Date: Mon, 10 May 2021 22:26:39 -0700 Subject: [PATCH 6/7] update tutorial to reflect change to /connect command. --- WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md index 02706350e8a3..751ff5ecee2e 100644 --- a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md +++ b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md @@ -126,8 +126,8 @@ previously. After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client. -Once in game type `/connect ()` where `` is the address of the -Archipelago server. `()` +Once in game type `/connect (Port) (Password)` where `` is the address of the +Archipelago server. `(Port)` is only required if the ap server is not using the default port of 38281. `(Password)` is only required if the Archipleago server you are using has a password set. ### Play the game From 469c7d1b8f8ad4bfe10153f076d3abf75d31b025 Mon Sep 17 00:00:00 2001 From: Kono Tyran Date: Tue, 11 May 2021 14:23:00 -0700 Subject: [PATCH 7/7] fixed abbreviation ap to full name. --- WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md index 751ff5ecee2e..d085b0b7d30b 100644 --- a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md +++ b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md @@ -127,7 +127,7 @@ After having placed your data file in the `APData` folder, start the Forge serve status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client. Once in game type `/connect (Port) (Password)` where `` is the address of the -Archipelago server. `(Port)` is only required if the ap server is not using the default port of 38281. `(Password)` +Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)` is only required if the Archipleago server you are using has a password set. ### Play the game