about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--apworld/__init__.py1
-rw-r--r--apworld/client/client.gd17
-rw-r--r--apworld/client/door.gd31
-rw-r--r--apworld/client/main.gd1
-rw-r--r--apworld/client/manager.gd15
-rw-r--r--apworld/client/paintingAuto.gd43
-rw-r--r--apworld/client/player.gd5
-rw-r--r--apworld/context.py54
-rw-r--r--apworld/options.py15
-rw-r--r--apworld/player_logic.py55
-rw-r--r--apworld/regions.py41
-rw-r--r--apworld/static_logic.py3
-rw-r--r--data/maps/daedalus/doors.txtpb3
-rw-r--r--data/maps/the_bearer/doors.txtpb1
-rw-r--r--data/maps/the_digital/doors.txtpb1
-rw-r--r--data/maps/the_entry/doors.txtpb1
-rw-r--r--data/maps/the_great/doors.txtpb3
-rw-r--r--data/maps/the_impressive/doors.txtpb1
-rw-r--r--data/maps/the_owl/doors.txtpb1
-rw-r--r--data/maps/the_repetitive/rooms/Entry Connector.txtpb2
-rw-r--r--data/maps/the_shop/doors.txtpb3
-rw-r--r--data/maps/the_tree/doors.txtpb1
-rw-r--r--data/maps/the_unkempt/doors.txtpb1
-rw-r--r--tools/datapacker/main.cpp5
24 files changed, 282 insertions, 22 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index e126fc0..6540b08 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -130,6 +130,7 @@ class Lingo2World(World):
130 slot_options = [ 130 slot_options = [
131 "cyan_door_behavior", 131 "cyan_door_behavior",
132 "daedalus_roof_access", 132 "daedalus_roof_access",
133 "enable_icarus",
133 "keyholder_sanity", 134 "keyholder_sanity",
134 "shuffle_control_center_colors", 135 "shuffle_control_center_colors",
135 "shuffle_doors", 136 "shuffle_doors",
diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 9a4b402..ce5ac7e 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd
@@ -25,6 +25,7 @@ var _slot_data = {}
25var _accessible_locations = [] 25var _accessible_locations = []
26var _accessible_worldports = [] 26var _accessible_worldports = []
27var _goal_accessible = false 27var _goal_accessible = false
28var _latched_doors = []
28 29
29signal could_not_connect 30signal could_not_connect
30signal connect_status 31signal connect_status
@@ -34,6 +35,7 @@ signal location_scout_received(location_id, item_name, player_name, flags, for_s
34signal text_message_received(message) 35signal text_message_received(message)
35signal item_sent_notification(message) 36signal item_sent_notification(message)
36signal hint_received(message) 37signal hint_received(message)
38signal door_latched(id)
37signal accessible_locations_updated 39signal accessible_locations_updated
38signal checked_locations_updated 40signal checked_locations_updated
39signal checked_worldports_updated 41signal checked_worldports_updated
@@ -189,6 +191,14 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo
189 message["type"], int(message.get("id", null)), message["path"] 191 message["type"], int(message.get("id", null)), message["path"]
190 ) 192 )
191 193
194 elif cmd == "UpdateLatches":
195 for id in message["latches"]:
196 var iid = int(id)
197 if not _latched_doors.has(iid):
198 _latched_doors.append(iid)
199
200 door_latched.emit(iid)
201
192 202
193func connectToServer(server, un, pw): 203func connectToServer(server, un, pw):
194 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) 204 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
@@ -255,6 +265,13 @@ func checkWorldport(port_id):
255 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}]) 265 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}])
256 266
257 267
268func latchDoor(id):
269 if not _latched_doors.has(id):
270 _latched_doors.append(id)
271
272 sendMessage([{"cmd": "LatchDoor", "door": id}])
273
274
258func getLogicalPath(object_type, object_id): 275func getLogicalPath(object_type, object_id):
259 var msg = {"cmd": "GetPath", "type": object_type} 276 var msg = {"cmd": "GetPath", "type": object_type}
260 if object_id != null: 277 if object_id != null:
diff --git a/apworld/client/door.gd b/apworld/client/door.gd index 49f5728..63cfa99 100644 --- a/apworld/client/door.gd +++ b/apworld/client/door.gd
@@ -1,7 +1,9 @@
1extends "res://scripts/nodes/door.gd" 1extends "res://scripts/nodes/door.gd"
2 2
3var door_id
3var item_id 4var item_id
4var item_amount 5var item_amount
6var latched = false
5 7
6 8
7func _ready(): 9func _ready():
@@ -10,7 +12,7 @@ func _ready():
10 ) 12 )
11 13
12 var gamedata = global.get_node("Gamedata") 14 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 15 door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null: 16 if door_id != null:
15 var ap = global.get_node("Archipelago") 17 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id) 18 var item_lock = ap.get_item_id_for_door(door_id)
@@ -27,6 +29,12 @@ func _ready():
27 self.excludeSenders = [] 29 self.excludeSenders = []
28 30
29 call_deferred("_readier") 31 call_deferred("_readier")
32 else:
33 var door_data = gamedata.objects.get_doors()[door_id]
34 if door_data.has_latch() and door_data.get_latch():
35 _check_latched.call_deferred(door_id)
36
37 latched = true
30 38
31 if global.map == "the_sun_temple": 39 if global.map == "the_sun_temple":
32 if name == "spe_EndPlatform" or name == "spe_entry_2": 40 if name == "spe_EndPlatform" or name == "spe_entry_2":
@@ -44,3 +52,24 @@ func _readier():
44 52
45 if ap.client.getItemAmount(item_id) >= item_amount: 53 if ap.client.getItemAmount(item_id) >= item_amount:
46 handleTriggered() 54 handleTriggered()
55
56
57func _check_latched(door_id):
58 var ap = global.get_node("Archipelago")
59
60 if ap.client._latched_doors.has(door_id):
61 triggered = total
62 handleTriggered()
63
64
65func handleTriggered():
66 super.handleTriggered()
67
68 if latched and ran:
69 var ap = global.get_node("Archipelago")
70 ap.client.latchDoor(door_id)
71
72
73func handleUntriggered():
74 if not latched or not ran:
75 super.handleUntriggered()
diff --git a/apworld/client/main.gd b/apworld/client/main.gd index e1f9610..3a62f81 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd
@@ -43,6 +43,7 @@ func _ready():
43 installScriptExtension(runtime.load_script("keyHolderChecker.gd")) 43 installScriptExtension(runtime.load_script("keyHolderChecker.gd"))
44 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd")) 44 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd"))
45 installScriptExtension(runtime.load_script("painting.gd")) 45 installScriptExtension(runtime.load_script("painting.gd"))
46 installScriptExtension(runtime.load_script("paintingAuto.gd"))
46 installScriptExtension(runtime.load_script("panel.gd")) 47 installScriptExtension(runtime.load_script("panel.gd"))
47 installScriptExtension(runtime.load_script("pauseMenu.gd")) 48 installScriptExtension(runtime.load_script("pauseMenu.gd"))
48 installScriptExtension(runtime.load_script("player.gd")) 49 installScriptExtension(runtime.load_script("player.gd"))
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index dac09b2..a17bee8 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd
@@ -141,6 +141,7 @@ func _ready():
141 client.accessible_locations_updated.connect(_on_accessible_locations_updated) 141 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
142 client.checked_locations_updated.connect(_on_checked_locations_updated) 142 client.checked_locations_updated.connect(_on_checked_locations_updated)
143 client.checked_worldports_updated.connect(_on_checked_worldports_updated) 143 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
144 client.door_latched.connect(_on_door_latched)
144 145
145 client.could_not_connect.connect(_client_could_not_connect) 146 client.could_not_connect.connect(_client_could_not_connect)
146 client.connect_status.connect(_client_connect_status) 147 client.connect_status.connect(_client_connect_status)
@@ -376,6 +377,20 @@ func _on_checked_worldports_updated():
376 textclient_node.update_worldports() 377 textclient_node.update_worldports()
377 378
378 379
380func _on_door_latched(door_id):
381 var gamedata = global.get_node("Gamedata")
382 if gamedata.get_door_map_name(door_id) != global.map:
383 return
384
385 var receivers = gamedata.get_door_receivers(door_id)
386 var scene = get_tree().get_root().get_node_or_null("scene")
387 if scene != null:
388 for receiver in receivers:
389 var rnode = scene.get_node_or_null(receiver)
390 if rnode != null:
391 rnode.handleTriggered()
392
393
379func _client_could_not_connect(message): 394func _client_could_not_connect(message):
380 could_not_connect.emit(message) 395 could_not_connect.emit(message)
381 396
diff --git a/apworld/client/paintingAuto.gd b/apworld/client/paintingAuto.gd new file mode 100644 index 0000000..553c2c9 --- /dev/null +++ b/apworld/client/paintingAuto.gd
@@ -0,0 +1,43 @@
1extends "res://scripts/nodes/paintingAuto.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 call_deferred("_readier")
30
31 super._ready()
32
33 if item_id != null and activate_on_sender_complete:
34 enabled = false
35 if not hide_particles:
36 get_node("Hinge/paintingColliders/TeleportParticles").emitting = false
37
38
39func _readier():
40 var ap = global.get_node("Archipelago")
41
42 if ap.client.getItemAmount(item_id) >= item_amount:
43 handleTriggered()
diff --git a/apworld/client/player.gd b/apworld/client/player.gd index b73f61e..1330e24 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd
@@ -313,6 +313,11 @@ func _ready():
313 entry1.get_parent().add_child.call_deferred(entry12) 313 entry1.get_parent().add_child.call_deferred(entry12)
314 entry1.queue_free() 314 entry1.queue_free()
315 315
316 # Move the Plaza RTE trigger outside of the turtle.
317 if global.map == "the_plaza":
318 var rte_trigger = get_node("/root/scene/Components/Warps/triggerArea")
319 rte_trigger.position.z = 0
320
316 ap.update_job_well_done_sign() 321 ap.update_job_well_done_sign()
317 322
318 var minimap = ap.SCRIPT_minimap.new() 323 var minimap = ap.SCRIPT_minimap.new()
diff --git a/apworld/context.py b/apworld/context.py index a0ee34d..7975686 100644 --- a/apworld/context.py +++ b/apworld/context.py
@@ -38,6 +38,7 @@ class Lingo2Manager:
38 keyboard: dict[str, int] 38 keyboard: dict[str, int]
39 worldports: set[int] 39 worldports: set[int]
40 goaled: bool 40 goaled: bool
41 latches: set[int]
41 42
42 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): 43 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"):
43 self.game_ctx = game_ctx 44 self.game_ctx = game_ctx
@@ -47,6 +48,7 @@ class Lingo2Manager:
47 self.tracker = Tracker(self) 48 self.tracker = Tracker(self)
48 self.keyboard = {} 49 self.keyboard = {}
49 self.worldports = set() 50 self.worldports = set()
51 self.latches = set()
50 52
51 self.reset() 53 self.reset()
52 54
@@ -56,6 +58,7 @@ class Lingo2Manager:
56 58
57 self.worldports = set() 59 self.worldports = set()
58 self.goaled = False 60 self.goaled = False
61 self.latches = set()
59 62
60 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: 63 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]:
61 ret: dict[str, int] = {} 64 ret: dict[str, int] = {}
@@ -81,6 +84,12 @@ class Lingo2Manager:
81 84
82 return ret 85 return ret
83 86
87 def update_latches(self, new_latches: set[int]) -> set[int]:
88 ret = new_latches.difference(self.latches)
89 self.latches.update(new_latches)
90
91 return ret
92
84 93
85class Lingo2GameContext: 94class Lingo2GameContext:
86 server: Endpoint | None 95 server: Endpoint | None
@@ -244,6 +253,17 @@ class Lingo2GameContext:
244 253
245 async_start(self.send_msgs([msg]), name="path reply") 254 async_start(self.send_msgs([msg]), name="path reply")
246 255
256 def send_update_latches(self, latches):
257 if self.server is None:
258 return
259
260 msg = {
261 "cmd": "UpdateLatches",
262 "latches": latches,
263 }
264
265 async_start(self.send_msgs([msg]), name="update latches")
266
247 async def send_msgs(self, msgs: list[Any]) -> None: 267 async def send_msgs(self, msgs: list[Any]) -> None:
248 """ `msgs` JSON serializable """ 268 """ `msgs` JSON serializable """
249 if not self.server or not self.server.socket.open or self.server.socket.closed: 269 if not self.server or not self.server.socket.open or self.server.socket.closed:
@@ -298,7 +318,7 @@ class Lingo2ClientContext(CommonContext):
298 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" 318 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}"
299 319
300 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), 320 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"),
301 self.victory_data_storage_key) 321 self.victory_data_storage_key, self.get_datastorage_key("latches"))
302 msg_batch = [{ 322 msg_batch = [{
303 "cmd": "Set", 323 "cmd": "Set",
304 "key": self.get_datastorage_key("keyboard1"), 324 "key": self.get_datastorage_key("keyboard1"),
@@ -311,6 +331,12 @@ class Lingo2ClientContext(CommonContext):
311 "default": 0, 331 "default": 0,
312 "want_reply": True, 332 "want_reply": True,
313 "operations": [{"operation": "default", "value": 0}] 333 "operations": [{"operation": "default", "value": 0}]
334 }, {
335 "cmd": "Set",
336 "key": self.get_datastorage_key("latches"),
337 "default": [],
338 "want_reply": True,
339 "operations": [{"operation": "default", "value": []}]
314 }] 340 }]
315 341
316 if self.slot_data.get("shuffle_worldports", False): 342 if self.slot_data.get("shuffle_worldports", False):
@@ -420,6 +446,10 @@ class Lingo2ClientContext(CommonContext):
420 self.manager.game_ctx.send_update_worldports(updates) 446 self.manager.game_ctx.send_update_worldports(updates)
421 elif args["key"] == self.victory_data_storage_key: 447 elif args["key"] == self.victory_data_storage_key:
422 self.handle_status_update(args["value"]) 448 self.handle_status_update(args["value"])
449 elif args["key"] == self.get_datastorage_key("latches"):
450 updates = self.manager.update_latches(set(args["value"]))
451 if len(updates) > 0:
452 self.manager.game_ctx.send_update_latches(updates)
423 453
424 def get_datastorage_key(self, name: str): 454 def get_datastorage_key(self, name: str):
425 return f"Lingo2_{self.slot}_{name}" 455 return f"Lingo2_{self.slot}_{name}"
@@ -501,6 +531,17 @@ class Lingo2ClientContext(CommonContext):
501 self.manager.tracker.refresh_state() 531 self.manager.tracker.refresh_state()
502 self.manager.game_ctx.send_accessible_locations() 532 self.manager.game_ctx.send_accessible_locations()
503 533
534 async def update_latches(self, updates: set[int]):
535 await self.send_msgs([{
536 "cmd": "Set",
537 "key": self.get_datastorage_key("latches"),
538 "want_reply": True,
539 "operations": [{
540 "operation": "update",
541 "value": updates
542 }]
543 }])
544
504 545
505async def pipe_loop(manager: Lingo2Manager): 546async def pipe_loop(manager: Lingo2Manager):
506 while not manager.client_ctx.exit_event.is_set(): 547 while not manager.client_ctx.exit_event.is_set():
@@ -550,8 +591,11 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict):
550 elif cmd == "CheckWorldport": 591 elif cmd == "CheckWorldport":
551 port_id = args["port_id"] 592 port_id = args["port_id"]
552 worldports = {port_id} 593 worldports = {port_id}
553 if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: 594
554 worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) 595 # Also check the reverse port if it's a two-way connection.
596 port_pairings = manager.client_ctx.slot_data["port_pairings"]
597 if str(port_id) in port_pairings and port_pairings.get(str(port_pairings[str(port_id)]), None) == port_id:
598 worldports.add(port_pairings[str(port_id)])
555 599
556 updates = manager.update_worldports(worldports) 600 updates = manager.update_worldports(worldports)
557 if len(updates) > 0: 601 if len(updates) > 0:
@@ -568,6 +612,10 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict):
568 path = manager.tracker.get_path_to_goal() 612 path = manager.tracker.get_path_to_goal()
569 613
570 manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path) 614 manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path)
615 elif cmd == "LatchDoor":
616 updates = manager.update_latches({args["door"]})
617 if len(updates) > 0:
618 async_start(manager.client_ctx.update_latches(updates), name="client update latches")
571 elif cmd == "Quit": 619 elif cmd == "Quit":
572 manager.client_ctx.exit_event.set() 620 manager.client_ctx.exit_event.set()
573 621
diff --git a/apworld/options.py b/apworld/options.py index 3d7c9a5..600df6a 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -56,11 +56,7 @@ class ShuffleWorldports(Toggle):
56 """ 56 """
57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in 57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in
58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections 58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections
59 like the one between The Shop and Control Center. Connections that depend on placing letters in keyholders are also 59 like the one between The Shop and Control Center.
60 currently not shuffled.
61
62 NOTE: It is highly recommended that you turn on Shuffle Control Center Colors when using Shuffle Worldports. Not
63 doing so runs the risk of creating an unfinishable seed.
64 """ 60 """
65 display_name = "Shuffle Worldports" 61 display_name = "Shuffle Worldports"
66 62
@@ -95,6 +91,14 @@ class CyanDoorBehavior(Choice):
95 option_item = 2 91 option_item = 2
96 92
97 93
94class EnableIcarus(Toggle):
95 """
96 Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for
97 it, and its worldport will not be shuffled when worldport shuffle is on.
98 """
99 display_name = "Enable Icarus"
100
101
98class DaedalusRoofAccess(Toggle): 102class DaedalusRoofAccess(Toggle):
99 """ 103 """
100 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 104 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus
@@ -173,6 +177,7 @@ class Lingo2Options(PerGameCommonOptions):
173 shuffle_worldports: ShuffleWorldports 177 shuffle_worldports: ShuffleWorldports
174 keyholder_sanity: KeyholderSanity 178 keyholder_sanity: KeyholderSanity
175 cyan_door_behavior: CyanDoorBehavior 179 cyan_door_behavior: CyanDoorBehavior
180 enable_icarus: EnableIcarus
176 daedalus_roof_access: DaedalusRoofAccess 181 daedalus_roof_access: DaedalusRoofAccess
177 strict_purple_ending: StrictPurpleEnding 182 strict_purple_ending: StrictPurpleEnding
178 strict_cyan_ending: StrictCyanEnding 183 strict_cyan_ending: StrictCyanEnding
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 5271ed1..0cf0473 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -202,6 +202,8 @@ class LetterBehavior(IntEnum):
202class Lingo2PlayerLogic: 202class Lingo2PlayerLogic:
203 world: "Lingo2World" 203 world: "Lingo2World"
204 204
205 shuffled_maps: set[int]
206
205 locations_by_room: dict[int, list[PlayerLocation]] 207 locations_by_room: dict[int, list[PlayerLocation]]
206 event_loc_item_by_room: dict[int, dict[str, str]] 208 event_loc_item_by_room: dict[int, dict[str, str]]
207 209
@@ -227,9 +229,24 @@ class Lingo2PlayerLogic:
227 self.real_items = list() 229 self.real_items = list()
228 self.double_letter_amount = dict() 230 self.double_letter_amount = dict()
229 231
232 def should_shuffle_map(game_map) -> bool:
233 if game_map.type == data_pb2.MapType.NORMAL_MAP:
234 return True
235 elif game_map.type == data_pb2.MapType.ICARUS:
236 return bool(world.options.enable_icarus)
237
238 return False
239
240 self.shuffled_maps = set(game_map.id for game_map in world.static_logic.objects.maps
241 if should_shuffle_map(game_map))
242
230 if self.world.options.shuffle_doors: 243 if self.world.options.shuffle_doors:
231 for progressive in world.static_logic.objects.progressives: 244 for progressive in world.static_logic.objects.progressives:
232 for i in range(0, len(progressive.doors)): 245 for i in range(0, len(progressive.doors)):
246 door = world.static_logic.objects.doors[progressive.doors[i]]
247 if door.map_id not in self.shuffled_maps:
248 continue
249
233 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) 250 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
234 self.real_items.append(progressive.name) 251 self.real_items.append(progressive.name)
235 252
@@ -246,14 +263,21 @@ class Lingo2PlayerLogic:
246 else: 263 else:
247 continue 264 continue
248 265
249 for door in door_group.doors: 266 shuffleable_doors = [door_id for door_id in door_group.doors
250 self.item_by_door[door] = (door_group.name, 1) 267 if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps]
251 268
252 self.real_items.append(door_group.name) 269 if len(shuffleable_doors) > 0:
270 for door in shuffleable_doors:
271 self.item_by_door[door] = (door_group.name, 1)
272
273 self.real_items.append(door_group.name)
253 274
254 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled 275 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
255 # before we calculate any access requirements. 276 # before we calculate any access requirements.
256 for door in world.static_logic.objects.doors: 277 for door in world.static_logic.objects.doors:
278 if door.map_id not in self.shuffled_maps:
279 continue
280
257 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 281 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
258 continue 282 continue
259 283
@@ -282,18 +306,28 @@ class Lingo2PlayerLogic:
282 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS: 306 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS:
283 continue 307 continue
284 308
285 for door in door_group.doors: 309 shuffleable_doors = [door_id for door_id in door_group.doors
286 if not door in self.item_by_door: 310 if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps
311 and door_id not in self.item_by_door]
312
313 if len(shuffleable_doors) > 0:
314 for door in shuffleable_doors:
287 self.item_by_door[door] = (door_group.name, 1) 315 self.item_by_door[door] = (door_group.name, 1)
288 316
289 self.real_items.append(door_group.name) 317 self.real_items.append(door_group.name)
290 318
291 for door in world.static_logic.objects.doors: 319 for door in world.static_logic.objects.doors:
320 if door.map_id not in self.shuffled_maps:
321 continue
322
292 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 323 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
293 self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, 324 self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id,
294 self.get_door_reqs(door.id))) 325 self.get_door_reqs(door.id)))
295 326
296 for letter in world.static_logic.objects.letters: 327 for letter in world.static_logic.objects.letters:
328 if world.static_logic.get_room_object_map_id(letter) not in self.shuffled_maps:
329 continue
330
297 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, 331 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
298 AccessRequirements())) 332 AccessRequirements()))
299 behavior = self.get_letter_behavior(letter.key, letter.level2) 333 behavior = self.get_letter_behavior(letter.key, letter.level2)
@@ -313,10 +347,16 @@ class Lingo2PlayerLogic:
313 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1 347 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1
314 348
315 for mastery in world.static_logic.objects.masteries: 349 for mastery in world.static_logic.objects.masteries:
350 if world.static_logic.get_room_object_map_id(mastery) not in self.shuffled_maps:
351 continue
352
316 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, 353 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id,
317 AccessRequirements())) 354 AccessRequirements()))
318 355
319 for ending in world.static_logic.objects.endings: 356 for ending in world.static_logic.objects.endings:
357 if world.static_logic.get_room_object_map_id(ending) not in self.shuffled_maps:
358 continue
359
320 # Don't create a location for your selected ending, and never create a location for White Ending. 360 # Don't create a location for your selected ending, and never create a location for White Ending.
321 if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\ 361 if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\
322 and ending.name != "WHITE": 362 and ending.name != "WHITE":
@@ -335,6 +375,9 @@ class Lingo2PlayerLogic:
335 if self.world.options.keyholder_sanity: 375 if self.world.options.keyholder_sanity:
336 for keyholder in world.static_logic.objects.keyholders: 376 for keyholder in world.static_logic.objects.keyholders:
337 if keyholder.HasField("key"): 377 if keyholder.HasField("key"):
378 if world.static_logic.get_room_object_map_id(keyholder) not in self.shuffled_maps:
379 continue
380
338 reqs = AccessRequirements() 381 reqs = AccessRequirements()
339 382
340 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED: 383 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED:
diff --git a/apworld/regions.py b/apworld/regions.py index 0c3858d..1118603 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -62,6 +62,9 @@ def create_regions(world: "Lingo2World"):
62 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is 62 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
63 # faster than having to look them up during access checking. 63 # faster than having to look them up during access checking.
64 for room in world.static_logic.objects.rooms: 64 for room in world.static_logic.objects.rooms:
65 if room.map_id not in world.player_logic.shuffled_maps:
66 continue
67
65 region = create_region(room, world) 68 region = create_region(room, world)
66 regions[region.name] = region 69 regions[region.name] = region
67 region_and_room.append((region, room)) 70 region_and_room.append((region, room))
@@ -156,10 +159,42 @@ def shuffle_entrances(world: "Lingo2World"):
156 159
157 port_id_by_name: dict[str, int] = {} 160 port_id_by_name: dict[str, int] = {}
158 161
159 for port in world.static_logic.objects.ports: 162 shuffleable_ports = [port for port in world.static_logic.objects.ports
160 if port.no_shuffle: 163 if not port.no_shuffle
161 continue 164 and world.static_logic.get_room_object_map_id(port) in world.player_logic.shuffled_maps]
165
166 if len(shuffleable_ports) % 2 == 1:
167 # We have an odd number of shuffleable ports! Pick a port from a room that has more than one, and make it a
168 # redundant warp to another port.
169 redundant_rooms = set(room.id for room in world.static_logic.objects.rooms if len(room.ports) > 1)
170 redundant_ports = [port for port in shuffleable_ports if port.room_id in redundant_rooms]
171 chosen_port = world.random.choice(redundant_ports)
172
173 shuffleable_ports.remove(chosen_port)
174
175 chosen_destination = world.random.choice(shuffleable_ports)
176
177 world.port_pairings[chosen_port.id] = chosen_destination.id
178
179 from_region_name = world.static_logic.get_room_region_name(chosen_port.room_id)
180 to_region_name = world.static_logic.get_room_region_name(chosen_destination.room_id)
181
182 from_region = world.multiworld.get_region(from_region_name, world.player)
183 to_region = world.multiworld.get_region(to_region_name, world.player)
184
185 connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region)
186 from_region.exits.append(connection)
187 connection.connect(to_region)
188
189 if chosen_port.HasField("required_door"):
190 door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door)
191 connection.access_rule = make_location_lambda(door_reqs, world, None)
192
193 for region in door_reqs.get_referenced_rooms():
194 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
195 connection)
162 196
197 for port in shuffleable_ports:
163 port_region_name = world.static_logic.get_room_region_name(port.room_id) 198 port_region_name = world.static_logic.get_room_region_name(port.room_id)
164 port_region = world.multiworld.get_region(port_region_name, world.player) 199 port_region = world.multiworld.get_region(port_region_name, world.player)
165 200
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index e59a47d..2546007 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -166,6 +166,9 @@ class Lingo2StaticLogic:
166 else: 166 else:
167 return game_map.display_name 167 return game_map.display_name
168 168
169 def get_room_object_map_id(self, obj) -> int:
170 return self.objects.rooms[obj.room_id].map_id
171
169 def get_data_version(self) -> list[int]: 172 def get_data_version(self) -> list[int]:
170 version = self.objects.version 173 version = self.objects.version
171 return [version.major, version.minor, version.patch] 174 return [version.major, version.minor, version.patch]
diff --git a/data/maps/daedalus/doors.txtpb b/data/maps/daedalus/doors.txtpb index b6881b3..de6971e 100644 --- a/data/maps/daedalus/doors.txtpb +++ b/data/maps/daedalus/doors.txtpb
@@ -866,6 +866,7 @@ doors {
866doors { 866doors {
867 name: "Control Center Orange Door" 867 name: "Control Center Orange Door"
868 type: CONTROL_CENTER_COLOR 868 type: CONTROL_CENTER_COLOR
869 latch: true
869 receivers: "Components/Doors/Halls/oroom_6" 870 receivers: "Components/Doors/Halls/oroom_6"
870 control_center_color: "orange" 871 control_center_color: "orange"
871} 872}
@@ -893,12 +894,14 @@ doors {
893doors { 894doors {
894 name: "White Hallway From Entry" 895 name: "White Hallway From Entry"
895 type: CONTROL_CENTER_COLOR 896 type: CONTROL_CENTER_COLOR
897 latch: true
896 receivers: "Components/Doors/Halls/froom_6" 898 receivers: "Components/Doors/Halls/froom_6"
897 control_center_color: "white" 899 control_center_color: "white"
898} 900}
899doors { 901doors {
900 name: "Purple Hallway From Great" 902 name: "Purple Hallway From Great"
901 type: CONTROL_CENTER_COLOR 903 type: CONTROL_CENTER_COLOR
904 latch: true
902 receivers: "Components/Doors/Halls/froom_7" 905 receivers: "Components/Doors/Halls/froom_7"
903 control_center_color: "purple" 906 control_center_color: "purple"
904} 907}
diff --git a/data/maps/the_bearer/doors.txtpb b/data/maps/the_bearer/doors.txtpb index f1f5a57..1893455 100644 --- a/data/maps/the_bearer/doors.txtpb +++ b/data/maps/the_bearer/doors.txtpb
@@ -241,6 +241,7 @@ doors {
241doors { 241doors {
242 name: "Control Center Brown Door" 242 name: "Control Center Brown Door"
243 type: CONTROL_CENTER_COLOR 243 type: CONTROL_CENTER_COLOR
244 latch: true
244 receivers: "Components/Doors/brown_1" 245 receivers: "Components/Doors/brown_1"
245 control_center_color: "brown" 246 control_center_color: "brown"
246} 247}
diff --git a/data/maps/the_digital/doors.txtpb b/data/maps/the_digital/doors.txtpb index 3a2e381..35cfa81 100644 --- a/data/maps/the_digital/doors.txtpb +++ b/data/maps/the_digital/doors.txtpb
@@ -42,6 +42,7 @@ doors {
42doors { 42doors {
43 name: "Control Center Blue Door" 43 name: "Control Center Blue Door"
44 type: CONTROL_CENTER_COLOR 44 type: CONTROL_CENTER_COLOR
45 latch: true
45 receivers: "Components/Doors/maze2" 46 receivers: "Components/Doors/maze2"
46 control_center_color: "blue" 47 control_center_color: "blue"
47} 48}
diff --git a/data/maps/the_entry/doors.txtpb b/data/maps/the_entry/doors.txtpb index 40e486a..5a07322 100644 --- a/data/maps/the_entry/doors.txtpb +++ b/data/maps/the_entry/doors.txtpb
@@ -161,6 +161,7 @@ doors {
161doors { 161doors {
162 name: "Control Center White Door" 162 name: "Control Center White Door"
163 type: CONTROL_CENTER_COLOR 163 type: CONTROL_CENTER_COLOR
164 latch: true
164 receivers: "Components/Doors/back_left_7" 165 receivers: "Components/Doors/back_left_7"
165 control_center_color: "white" 166 control_center_color: "white"
166} 167}
diff --git a/data/maps/the_great/doors.txtpb b/data/maps/the_great/doors.txtpb index bf28421..132aa6f 100644 --- a/data/maps/the_great/doors.txtpb +++ b/data/maps/the_great/doors.txtpb
@@ -54,18 +54,21 @@ doors {
54doors { 54doors {
55 name: "Control Center Purple Door" 55 name: "Control Center Purple Door"
56 type: CONTROL_CENTER_COLOR 56 type: CONTROL_CENTER_COLOR
57 latch: true
57 receivers: "Components/Doors/entry_23" 58 receivers: "Components/Doors/entry_23"
58 control_center_color: "purple" 59 control_center_color: "purple"
59} 60}
60doors { 61doors {
61 name: "Control Center Gray Door" 62 name: "Control Center Gray Door"
62 type: CONTROL_CENTER_COLOR 63 type: CONTROL_CENTER_COLOR
64 latch: true
63 receivers: "Components/Doors/Gates/Gate/animationListener" 65 receivers: "Components/Doors/Gates/Gate/animationListener"
64 control_center_color: "gray" 66 control_center_color: "gray"
65} 67}
66doors { 68doors {
67 name: "Control Center Red Door" 69 name: "Control Center Red Door"
68 type: CONTROL_CENTER_COLOR 70 type: CONTROL_CENTER_COLOR
71 latch: true
69 receivers: "Components/Doors/entry_18" 72 receivers: "Components/Doors/entry_18"
70 control_center_color: "red" 73 control_center_color: "red"
71} 74}
diff --git a/data/maps/the_impressive/doors.txtpb b/data/maps/the_impressive/doors.txtpb index e27d531..03ec9f5 100644 --- a/data/maps/the_impressive/doors.txtpb +++ b/data/maps/the_impressive/doors.txtpb
@@ -32,6 +32,7 @@ doors {
32doors { 32doors {
33 name: "Control Center Green Door" 33 name: "Control Center Green Door"
34 type: CONTROL_CENTER_COLOR 34 type: CONTROL_CENTER_COLOR
35 latch: true
35 receivers: "Components/Doors/entry_2" 36 receivers: "Components/Doors/entry_2"
36 control_center_color: "green" 37 control_center_color: "green"
37} 38}
diff --git a/data/maps/the_owl/doors.txtpb b/data/maps/the_owl/doors.txtpb index 9254c2a..032863e 100644 --- a/data/maps/the_owl/doors.txtpb +++ b/data/maps/the_owl/doors.txtpb
@@ -59,6 +59,7 @@ doors {
59doors { 59doors {
60 name: "Control Center Magenta Door" 60 name: "Control Center Magenta Door"
61 type: CONTROL_CENTER_COLOR 61 type: CONTROL_CENTER_COLOR
62 latch: true
62 receivers: "Components/Doors/entry_18" 63 receivers: "Components/Doors/entry_18"
63 control_center_color: "magenta" 64 control_center_color: "magenta"
64} 65}
diff --git a/data/maps/the_repetitive/rooms/Entry Connector.txtpb b/data/maps/the_repetitive/rooms/Entry Connector.txtpb index d953ecc..1508145 100644 --- a/data/maps/the_repetitive/rooms/Entry Connector.txtpb +++ b/data/maps/the_repetitive/rooms/Entry Connector.txtpb
@@ -4,5 +4,5 @@ ports {
4 display_name: "Northwest Worldport" 4 display_name: "Northwest Worldport"
5 path: "Components/Warps/worldport2" 5 path: "Components/Warps/worldport2"
6 destination { x: -11 y: 0 z: 13 } 6 destination { x: -11 y: 0 z: 13 }
7 rotation: 0 7 rotation: 90
8} 8}
diff --git a/data/maps/the_shop/doors.txtpb b/data/maps/the_shop/doors.txtpb index 5362614..2ce7c71 100644 --- a/data/maps/the_shop/doors.txtpb +++ b/data/maps/the_shop/doors.txtpb
@@ -33,5 +33,8 @@ doors {
33doors { 33doors {
34 name: "N Entered" 34 name: "N Entered"
35 type: EVENT 35 type: EVENT
36 latch: true
37 receivers: "Components/Doors/entry_1"
38 receivers: "Components/Doors/entry_2"
36 keyholders { room: "Main Area" name: "N" key: "n" } 39 keyholders { room: "Main Area" name: "N" key: "n" }
37} 40}
diff --git a/data/maps/the_tree/doors.txtpb b/data/maps/the_tree/doors.txtpb index 6cb4086..1932aa7 100644 --- a/data/maps/the_tree/doors.txtpb +++ b/data/maps/the_tree/doors.txtpb
@@ -1,6 +1,7 @@
1doors { 1doors {
2 name: "Control Center Brown Door" 2 name: "Control Center Brown Door"
3 type: CONTROL_CENTER_COLOR 3 type: CONTROL_CENTER_COLOR
4 latch: true
4 receivers: "Components/Doors/entry_1" 5 receivers: "Components/Doors/entry_1"
5 control_center_color: "brown" 6 control_center_color: "brown"
6} 7}
diff --git a/data/maps/the_unkempt/doors.txtpb b/data/maps/the_unkempt/doors.txtpb index 10165ee..446fe69 100644 --- a/data/maps/the_unkempt/doors.txtpb +++ b/data/maps/the_unkempt/doors.txtpb
@@ -68,6 +68,7 @@ doors {
68doors { 68doors {
69 name: "Control Center Orange Door" 69 name: "Control Center Orange Door"
70 type: CONTROL_CENTER_COLOR 70 type: CONTROL_CENTER_COLOR
71 latch: true
71 receivers: "Components/Doors/entry_6" 72 receivers: "Components/Doors/entry_6"
72 receivers: "Components/Doors/entry_13" 73 receivers: "Components/Doors/entry_13"
73 receivers: "Panels/Assorted/panel_1/teleportListener" 74 receivers: "Panels/Assorted/panel_1/teleportListener"
diff --git a/tools/datapacker/main.cpp b/tools/datapacker/main.cpp index 6c2f543..e3ab100 100644 --- a/tools/datapacker/main.cpp +++ b/tools/datapacker/main.cpp
@@ -96,9 +96,8 @@ class DataPacker {
96 96
97 if (metadata.has_worldport_entrance()) { 97 if (metadata.has_worldport_entrance()) {
98 map.set_worldport_entrance(container_.FindOrAddPort( 98 map.set_worldport_entrance(container_.FindOrAddPort(
99 metadata.worldport_entrance().map(), 99 map_name, metadata.worldport_entrance().room(),
100 metadata.worldport_entrance().room(), 100 metadata.worldport_entrance().name(), std::nullopt, std::nullopt));
101 metadata.worldport_entrance().name(), map_name, std::nullopt));
102 } 101 }
103 } 102 }
104 103