# lingo2-archipelago [Archipelago](https://archipelago.gg/) is an open-source project that supports randomizing a number of different games and combining them into one cooperative experience. Items from each game are hidden in other games. For more information about Archipelago, you can look at their website. This is a project that modifies the game [Lingo 2](https://www.lingothegame.com/lingo2.html) so that it can be played as part of an Archipelago multiworld game. ## Installation 1. Download the Lingo 2 Apworld from [the releases page](https://code.fourisland.com/lingo2-archipelago/about/CHANGELOG.md). 2. If you do not already have it, download and install the [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/). 3. Double click on `lingo2.apworld` to install it, or copy it manually to the `custom_worlds` folder of your Archipelago installation. ## Joining a Multiworld game (Windows) 1. Open the Archipelago Launcher. 2. Select "Lingo 2 Client". 3. The first time you do this, Archipelago will prompt you for the location of the Lingo 2 executable file ("Lingo2.exe"). You can find this by right-clicking on Lingo 2 in Steam, going to "Manage", and clicking "Browse local files". 4. Lingo 2 will open, and you will see a form asking for your connection details. Enter the Archipelago address, slot name, and password into the fields. 5. Press Connect. 6. Enjoy! To continue an earlier game, you can perform the exact same steps as above. ## Joining a Multiworld game (Non-Windows) Lingo 2 only officially supports Windows, but has been known to work on Linux using Proton. Archipelago can be played on a non-Windows system, but the process is a little more complex. 1. Download [archipelago.tscn](https://code.fourisland.com/lingo2-archipelago/plain/client/archipelago.tscn) and put it in your custom maps folder. You only have to do this once. 2. Open Lingo 2, and select Archipelago from the level selection list. 3. Put the path to your `lingo2.apworld` into the field provided. You only have to do this once, as the game will remember what you put in. 4. Click Start and wait for the connection settings screen to load. 5. Open the Archipelago Launcher. 6. Select "Lingo 2 Client". 7. You should see "Connected to Lingo 2!" You can then return to Lingo 2 and fill out your connection details. 8. Press Connect. 9. Enjoy! ## Frequently Asked Questions ### Why aren't the starting room letters shuffled? The letter requirements for solving puzzles are very restrictive, especially in the early game. It is possible for the generator to find some subset of letters and doors to place in the starting room such that you are not trapped, but this places a lot of strain on generation and leads to significantly more generation failures. As a result, the starting room letters (H1, I1, N1, and T1) are always present in the starting room, even when remote letter shuffle is enabled. These letters will _also_ count as clearing a check, so you will send out another item at the same time as collecting the letter. ### What areas are randomized? Almost all maps that you can access from the base game are randomized. The exceptions are: - Demo - The Hinterlands (this will probably be repurposed) ### Is my progress saved locally? Lingo 2 autosaves your progress every time you solve a puzzle, get a collectable, or interact with a keyholder. The randomizer generates a savefile name based on your Multiworld seed and slot number, so you should be able to seamlessly switch between multiworlds and even slots within a multiworld. The exception to this is different rooms created from the same multiworld seed. The client is unable to tell rooms in a seed apart (this is a limitation of the Archipelago API), so the client will use the same save file for the same slot in different rooms on the same seed. You can work around this by manually moving or removing the save folder from the users directory in Lingo 2's game files. If you play the base game again, you will see one or more save files with a long name that begins with "zzAP\_". These are the saves for your multiworlds. They can be safely deleted after you have completed the associated multiworld. It is not recommended to load these save files outside of the randomizer. A connection to Archipelago is required to resume playing a multiworld. This is because the set of items you have received is not stored locally. ### What about wall snipes? "Wall sniping" refers to the fact that you are able to solve puzzles on the other side of opaque walls. The player is never expected to or required to do this in normal gameplay. This randomizer does not change how wall snipes work, but it will likewise never require the use of them. ### How do cyan doors work? In the base game, there are a number of cyan-colored doors that ordinarily open once you collect H2 in The Repetitive. There are also a handful of panels that only appear upon getting H2 as well, which the apworld treats the same as the cyan doors. There is an option that lets you choose how these doors and panels behave. By default, they act the same as in the base game: they only open or appear after collecting H2. Note that this means the actual H2 collectable in The Repetitive. Receiving H2 via remote letter shuffle does not count for this requirement. However, you can also make cyan doors activate upon collecting or receiving your first double letter, regardless of what it is or if it's remote. Finally, you can lock cyan doors behind an item called "Cyan Doors". It is important to note, however, that the Cyan Door Behavior option only applies to cyan doors that are not already affected by another type of shuffling. When door shuffle is on, the following cyan doors are activated by individual items and are not impacted by your choice of Cyan Door Behavior: - The entrance to The Tower from The Great (The Great - Tower Entrance) - The entrance to The Butterfly from The Bearer (The Bearer - Butterfly Entrance) - The entrance to The Repetitive from The Entry (The Entry - Repetitive Entrance) - The eye painting near the yellow color hallway in Daedalus (Daedalus - Eye Painting) - The Red I room in The Repetitive (The Repetitive - Anti Collectable Room) Additionally, when control center color shuffle is enabled, the orange door in The Unkempt (which ordinarily doubles as a cyan door) opens upon receiving the Control Center Orange Doors item, instead of following the Cyan Door Behavior option. ### Help! I lost C/G in The Congruent! If you place C or G into the relevant keyholders in The Congruent, the keyholder disappears. You can retrieve your letter immediately by pressing C or G again before leaving solve mode, as the keyholder will still be considered to be "focused", even though it has moved. If you have already moved, though, there is another way to get your letters back: just use the Key Return in The Entry. ### Why is the tracker telling me to solve a panel that's currently red? Red usually indicates that a panel cannot be solved because of missing letters. However, that only applies to the puzzle's main answer. If a puzzle has alternate answers, you may be expected to use one of those instead of the main one. As long as you have all of the necessary letters, an alternate answer can be typed into a red panel even though it does not show you typing. When you finish typing the answer, the panel will solve as normal. ### Why does the tracker say "The Entry (Colored Doors Area) - OPEN" is in logic? This is an infamous panel, both in the base game and in the randomizer. There are _two_ valid answers that open the door / clear the location. These are "ORANGE" and "WALL". ### I can't solve the COLORS panel in The Sturdy! The Sturdy contains a rainbow painting that leads to the Gold Ending area in Daedalus. There are three ways to spawn this painting, which have different logical requirements: - Solve the COLORS panel that appears after collecting S2. This is the most well-known way, and causes the most confusion because you may be expected to enter the painting even if you are unable to solve the panel (e.g. if you are missing letters or missing Boxes Symbol). - Solve the panels in the order that you walk across the colors on the way toward S2: Magenta, Red, Orange, Yellow, Green, Blue, Purple, Cyan. This has the same logic as accessing S2. - Type "MOVE" into the Green and Yellow panels, and none of the other ones. This is a subset of the logic for accessing S2, so you may actually be expected to use the rainbow painting before you can even collect S2. ### How does Icarus work? While Icarus is easily accessible during normal play, it is not randomized by default. The main reason for this is that Icarus employs significantly more use of gravity changing mechanics than the rest of the game and as a result tends to cause motion sickness in a lot of players. It is also an infamously confusing area to navigate. Because of this, the player may enter and exit Icarus from the usual place in Daedalus, but it will not contain any locations, and no items will be added to the pool for it. The worldport will not be included in the randomization if worldport shuffle is on. Icarus can also still be entered from The Crystalline, but doing so (in order to then access Daedalus) will not be logically required. However, Icarus can be randomized via the "Enable Icarus" option. Doing so creates locations and items for the map, and includes the worldport in worldport shuffle. The aforementioned connection from The Crystalline also becomes logical, if The Crystalline is enabled. It is not trivial to telegraph exactly what is logical within Icarus. It is very easy to break logic because the gravity changers allow you to fall in almost any direction you want to. In general, falling is only in logic if it is "guided", i.e. falling through a hole or an open door to another platform, or using a gravity inverter. You may also sometimes be required to solve panels that are physically near you and easily visible, but not on your plane of gravity. The tracker can help you determine what is considered logical, if you want to stay within the randomizer's logic. ### How do the gift maps work? The beta tester gift maps are hidden levels intended for specific people. By default, these are not accessible at all from within the randomizer. The "Enable Gift Maps" option allows you to enter the maps, and creates items and locations for them. If worldport shuffle is on, their worldports will be included in the randomization. The gift maps are accessed via a panel in The Entry's Starting Room, which only appears if at least one gift map is enabled. It is also treated like a cyan door, and will not appear until the condition specified in the Cyan Door Behavior option is satisfied. Solving this panel with the name of one of the beta testers will teleport you to their corresponding gift map. This README purposefully does not list the names you need to enter the maps via the panel. In the base game, nothing happens once you complete a gift map. Masteries have been added to the gift maps in the randomizer so that the player can be rewarded for completing them. Note that the gift maps were originally only intended to be played by specific people, and as a result may be frustrating or require knowledge of inside jokes. The Crystalline is particularly difficult as it requires completing a parkour course. It is highly recommended that you complete these maps vanilla or solo before bringing them to a multiworld. It is also perfectly acceptable to never enable them. ## Running from source The randomizer is mostly written in Python and GDScript, which do not need to be compiled. However, there are three files that need to be generated before the apworld can be used. The first file is `data.binpb`, the datafile containing the randomizer logic. You can read about how to generate it on [its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md). Once you have it, put it in a subfolder of `apworld` called `generated`. The second generated file is `data_pb2.py`. This file allows Archipelago to read the datafile. We use `protoc`, the Protocol Buffer compiler, to generate it. As of 0.6.3, Archipelago has protobuf 3.20.3 packaged with it, which means we need to compile our proto file with a similar version. If you followed the steps to generate `data.binpb` and compiled the `datapacker` tool yourself, you will already have protobuf version 3.21.12 installed through vcpkg. You can then run a command similar to this in order to generate the python file. ```shell .\out\build\x64-Debug\vcpkg_installed\x64-windows\tools\protobuf\protoc.exe -Iproto\ ^ --python_out=apworld\generated\ .\proto\data.proto ``` The exact path to `protoc.exe` is going to depend on where vcpkg installed its packages. The above location is where Visual Studio will probably put it. The third generated file is `proto.gd`. This is the GDScript version of the previous file. We use a Godot script to generate it, which means [the Godot Editor](https://godotengine.org/download/) is required. From the root of the repository: ```shell cd vendor\godobuf godot --headless -s addons\protobuf\protobuf_cmdln.gd --input=..\..\proto\data.proto ^ --output=..\..\apworld\generated\proto.gd ``` If you are not on Windows, replace the forward slashes with backslashes as appropriate (and the caret with a forward slash). You will also probably need to replace "godot" at the start of the second line with a path to a Godot Editor executable. After generating those three files, the apworld should be functional. You can copy it into an Archipelago source tree (rename the folder `apworld` to `lingo2` if you do so) if you want to edit/debug the code. Otherwise, you can zip up the folder and rename it to `lingo2.apworld` in order to package it for distribution. ref='#n314'>314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
extends CanvasLayer
var tabs
var panel
var label
var entry
var is_open = false
var locations_overlay
var location_texture
var worldport_texture
var goal_texture
var tracker_tree
var tracker_loc_tree_item_by_id = {}
var tracker_port_tree_item_by_id = {}
var tracker_goal_tree_item = null
var tracker_object_by_index = {}
var worldports_tab
var worldports_tree
var port_tree_item_by_map = {}
var port_tree_item_by_map_port = {}
const kLocation = 0
const kWorldport = 1
const kGoal = 2
func _ready():
process_mode = ProcessMode.PROCESS_MODE_ALWAYS
layer = 2
locations_overlay = RichTextLabel.new()
locations_overlay.name = "LocationsOverlay"
locations_overlay.offset_top = 220
locations_overlay.offset_bottom = 720
locations_overlay.offset_left = 20
locations_overlay.anchor_right = 1.0
locations_overlay.offset_right = -10
locations_overlay.scroll_active = false
locations_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
locations_overlay.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
add_child(locations_overlay)
update_locations_visibility()
tabs = TabContainer.new()
tabs.name = "Tabs"
tabs.offset_left = 100
tabs.offset_right = 1820
tabs.offset_top = 100
tabs.offset_bottom = 980
tabs.visible = false
tabs.theme = preload("res://assets/themes/baseUI.tres")
tabs.add_theme_font_size_override("font_size", 36)
add_child(tabs)
panel = MarginContainer.new()
panel.name = "Text Client"
panel.add_theme_constant_override("margin_top", 60)
panel.add_theme_constant_override("margin_left", 60)
panel.add_theme_constant_override("margin_right", 60)
panel.add_theme_constant_override("margin_bottom", 60)
tabs.add_child(panel)
label = RichTextLabel.new()
label.set_name("Label")
label.scroll_following = true
label.selection_enabled = true
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.size_flags_vertical = Control.SIZE_EXPAND_FILL
label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
label.push_font_size(30)
var entry_style = StyleBoxFlat.new()
entry_style.bg_color = Color(0.9, 0.9, 0.9, 1)
entry = LineEdit.new()
entry.set_name("Entry")
entry.add_theme_font_override("font", preload("res://assets/fonts/Lingo2.ttf"))
entry.add_theme_font_size_override("font_size", 36)
entry.add_theme_color_override("font_color", Color(0, 0, 0, 1))
entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1))
entry.add_theme_stylebox_override("focus", entry_style)
entry.text_submitted.connect(text_entered)
var tc_arranger = VBoxContainer.new()
tc_arranger.add_child(label)
tc_arranger.add_child(entry)
tc_arranger.add_theme_constant_override("separation", 40)
panel.add_child(tc_arranger)
var tracker_margins = MarginContainer.new()
tracker_margins.name = "Locations"
tracker_margins.add_theme_constant_override("margin_top", 60)
tracker_margins.add_theme_constant_override("margin_left", 60)
tracker_margins.add_theme_constant_override("margin_right", 60)
tracker_margins.add_theme_constant_override("margin_bottom", 60)
tabs.add_child(tracker_margins)
tracker_tree = Tree.new()
tracker_tree.columns = 3
tracker_tree.hide_root = true
tracker_tree.add_theme_font_size_override("font_size", 24)
tracker_tree.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 1))
tracker_tree.add_theme_constant_override("v_separation", 1)
tracker_tree.item_edited.connect(_on_tracker_button_clicked)
tracker_tree.set_column_expand(0, false)
tracker_tree.set_column_expand(1, true)
tracker_tree.set_column_expand(2, false)
tracker_tree.set_column_custom_minimum_width(2, 200)
tracker_margins.add_child(tracker_tree)
worldports_tab = MarginContainer.new()
worldports_tab.name = "Worldports"
worldports_tab.add_theme_constant_override("margin_top", 60)
worldports_tab.add_theme_constant_override("margin_left", 60)
worldports_tab.add_theme_constant_override("margin_right", 60)
worldports_tab.add_theme_constant_override("margin_bottom", 60)
tabs.add_child(worldports_tab)
tabs.set_tab_hidden(2, true)
worldports_tree = Tree.new()
worldports_tree.columns = 2
worldports_tree.hide_root = true
worldports_tree.theme = preload("res://assets/themes/baseUI.tres")
worldports_tree.add_theme_font_size_override("font_size", 24)
worldports_tab.add_child(worldports_tree)
var runtime = global.get_node("Runtime")
var location_image = Image.new()
location_image.load_png_from_buffer(runtime.read_path("assets/location.png"))
location_texture = ImageTexture.create_from_image(location_image)
var worldport_image = Image.new()
worldport_image.load_png_from_buffer(runtime.read_path("assets/worldport.png"))
worldport_texture = ImageTexture.create_from_image(worldport_image)
var goal_image = Image.new()
goal_image.load_png_from_buffer(runtime.read_path("assets/goal.png"))
goal_texture = ImageTexture.create_from_image(goal_image)
func _input(event):
if global.loaded and event is InputEventKey and event.pressed:
if event.keycode == KEY_TAB and !Input.is_key_pressed(KEY_SHIFT):
if !get_tree().paused:
is_open = true
get_tree().paused = true
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
tabs.visible = true
entry.grab_focus()
get_viewport().set_input_as_handled()
else:
dismiss()
elif event.keycode == KEY_ESCAPE:
if is_open:
dismiss()
get_viewport().set_input_as_handled()
func dismiss():
if is_open:
get_tree().paused = false
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
tabs.visible = false
is_open = false
func parse_printjson(text):
label.append_text("[p]" + text + "[/p]")
func text_entered(text):
var ap = global.get_node("Archipelago")
var cmd = text.trim_suffix("\n")
entry.text = ""
if OS.is_debug_build():
if cmd.begins_with("/tp_map "):
var new_map = cmd.substr(8)
global.map = new_map
global.sets_entry_point = false
switcher.switch_map("res://objects/scenes/%s.tscn" % new_map)
return
ap.client.say(cmd)
func update_locations(reset_locations = true):
var ap = global.get_node("Archipelago")
var gamedata = global.get_node("Gamedata")
locations_overlay.clear()
locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf"))
locations_overlay.push_font_size(24)
locations_overlay.push_color(Color(0.9, 0.9, 0.9, 1))
locations_overlay.push_outline_color(Color(0, 0, 0, 1))
locations_overlay.push_outline_size(2)
var locations = []
for location_id in ap.client._accessible_locations:
if not ap.client._checked_locations.has(location_id):
var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)")
(
locations
. append(
{
"name": location_name,
"type": kLocation,
"id": location_id,
}
)
)
for port_id in ap.client._accessible_worldports:
if not ap.client._checked_worldports.has(port_id):
var port_name = gamedata.get_worldport_display_name(port_id)
(
locations
. append(
{
"name": port_name,
"type": kWorldport,
"id": port_id,
}
)
)
locations.sort_custom(func(a, b): return a["name"] < b["name"])
if ap.client._goal_accessible:
var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
ap.victory_condition
]]
(
locations
. push_front(
{
"name": location_name,
"type": kGoal,
}
)
)
var count = 0
for location in locations:
if count < 18:
locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT)
locations_overlay.append_text(location["name"])
locations_overlay.append_text(" ")
if location["type"] == kLocation:
locations_overlay.add_image(location_texture)
elif location["type"] == kWorldport:
locations_overlay.add_image(worldport_texture)
elif location["type"] == kGoal:
locations_overlay.add_image(goal_texture)
locations_overlay.pop()
count += 1
if count > 18:
locations_overlay.append_text("[p align=right][lb]...[rb][/p]")
if reset_locations:
reset_tracker_tab()
var root_ti = tracker_tree.create_item(null)
for location in locations:
var loc_row = root_ti.create_child()
loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON)
loc_row.set_selectable(0, false)
loc_row.set_text(1, location["name"])
loc_row.set_selectable(1, false)
loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM)
loc_row.set_text(2, "Show Path")
loc_row.set_custom_as_button(2, true)
loc_row.set_editable(2, true)
loc_row.set_selectable(2, false)
loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER)
if location["type"] == kLocation:
loc_row.set_icon(0, location_texture)
tracker_loc_tree_item_by_id[location["id"]] = loc_row
elif location["type"] == kWorldport:
loc_row.set_icon(0, worldport_texture)
tracker_port_tree_item_by_id[location["id"]] = loc_row
elif location["type"] == kGoal:
loc_row.set_icon(0, goal_texture)
tracker_goal_tree_item = loc_row
tracker_object_by_index[loc_row.get_index()] = location
else:
for loc_row in tracker_tree.get_root().get_children():
loc_row.visible = false
for location_id in tracker_loc_tree_item_by_id.keys():
if (
ap.client._accessible_locations.has(location_id)
and not ap.client._checked_locations.has(location_id)
):
tracker_loc_tree_item_by_id[location_id].visible = true
for port_id in tracker_port_tree_item_by_id.keys():
if (
ap.client._accessible_worldports.has(port_id)
and not ap.client._checked_worldports.has(port_id)
):
tracker_port_tree_item_by_id[port_id].visible = true
if tracker_goal_tree_item != null and ap.client._goal_accessible:
tracker_goal_tree_item.visible = true
func update_locations_visibility():
var ap = global.get_node("Archipelago")
locations_overlay.visible = ap.show_locations
func _on_tracker_button_clicked():
var edited_item = tracker_tree.get_edited()
var edited_index = edited_item.get_index()
if tracker_object_by_index.has(edited_index):
var tracker_object = tracker_object_by_index[edited_index]
var ap = global.get_node("Archipelago")
var type_str = ""
if tracker_object["type"] == kLocation:
type_str = "location"
elif tracker_object["type"] == kWorldport:
type_str = "worldport"
elif tracker_object["type"] == kGoal:
type_str = "goal"
ap.client.getLogicalPath(type_str, tracker_object.get("id", null))
func display_logical_path(object_type, object_id, paths):
var ap = global.get_node("Archipelago")
var gamedata = global.get_node("Gamedata")
var location_name = "(Unknown)"
if object_type == "location" and object_id != null:
location_name = gamedata.location_name_by_id.get(object_id, "(Unknown)")
elif object_type == "worldport" and object_id != null:
location_name = gamedata.get_worldport_display_name(object_id)
elif object_type == "goal":
location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
ap.victory_condition
]]
label.append_text("[p]Path to %s:[/p]" % location_name)
label.append_text("[ol]" + "\n".join(paths) + "[/ol]")
panel.visible = true
func setup_worldports():
tabs.set_tab_hidden(2, false)
var root_ti = worldports_tree.create_item(null)
var ports_by_map_id = {}
var display_names_by_map_id = {}
var display_names_by_port_id = {}
var ap = global.get_node("Archipelago")
var gamedata = global.get_node("Gamedata")
for fpid in ap.port_pairings:
var port = gamedata.objects.get_ports()[fpid]
var room = gamedata.objects.get_rooms()[port.get_room_id()]
if not ports_by_map_id.has(room.get_map_id()):
ports_by_map_id[room.get_map_id()] = []
var map = gamedata.objects.get_maps()[room.get_map_id()]
display_names_by_map_id[map.get_id()] = map.get_display_name()
ports_by_map_id[room.get_map_id()].append(fpid)
display_names_by_port_id[fpid] = port.get_display_name()
var sorted_map_ids = ports_by_map_id.keys().duplicate()
sorted_map_ids.sort_custom(
func(a, b): return display_names_by_map_id[a] < display_names_by_map_id[b]
)
for map_id in sorted_map_ids:
var map_ti = root_ti.create_child()
map_ti.set_text(0, display_names_by_map_id[map_id])
map_ti.visible = false
map_ti.collapsed = true
port_tree_item_by_map[map_id] = map_ti
port_tree_item_by_map_port[map_id] = {}
var port_ids = ports_by_map_id[map_id]
port_ids.sort_custom(
func(a, b): return display_names_by_port_id[a] < display_names_by_port_id[b]
)
for port_id in port_ids:
var port_ti = map_ti.create_child()
port_ti.set_text(0, display_names_by_port_id[port_id])
port_ti.set_text(1, gamedata.get_worldport_display_name(ap.port_pairings[port_id]))
port_ti.visible = false
port_tree_item_by_map_port[map_id][port_id] = port_ti
update_worldports()
func update_worldports():
var ap = global.get_node("Archipelago")
for map_id in port_tree_item_by_map_port.keys():
var map_visible = false
for port_id in port_tree_item_by_map_port[map_id].keys():
var ti = port_tree_item_by_map_port[map_id][port_id]
ti.visible = ap.client._checked_worldports.has(port_id)
if ti.visible:
map_visible = true
port_tree_item_by_map[map_id].visible = map_visible
func reset():
locations_overlay.clear()
tabs.set_tab_hidden(2, true)
port_tree_item_by_map.clear()
port_tree_item_by_map_port.clear()
worldports_tree.clear()
reset_tracker_tab()
func reset_tracker_tab():
tracker_loc_tree_item_by_id.clear()
tracker_port_tree_item_by_id.clear()
tracker_goal_tree_item = null
tracker_object_by_index.clear()
tracker_tree.clear()