extends Node

var SCRIPT_doorControl
var SCRIPT_effects
var SCRIPT_location
var SCRIPT_multiplayer
var SCRIPT_mypainting
var SCRIPT_notifier
var SCRIPT_panel
var SCRIPT_pilgrimage_terminator
var SCRIPT_textclient
var SCRIPT_uuid

var ap_server = ""
var ap_user = ""
var ap_pass = ""
var confusify_world = false
var enable_multiplayer = false
var track_player = false
var connection_history = []

const my_version = "4.2.1"
const ap_version = {"major": 0, "minor": 5, "build": 0, "class": "Version"}
const color_items = [
	"White", "Black", "Red", "Blue", "Green", "Brown", "Gray", "Orange", "Purple", "Yellow"
]
const progressive_items = {
	"Progressive Orange Tower":
	["Second Floor", "Third Floor", "Fourth Floor", "Fifth Floor", "Sixth Floor", "Seventh Floor"],
	"Progressive Art Gallery":
	["Second Floor", "Third Floor", "Fourth Floor", "Fifth Floor", "Exit"],
	"Progressive Hallway Room": ["First Door", "Second Door", "Third Door", "Fourth Door"],
	"Progressive Fearless": ["Second Floor", "Third Floor"],
	"Progressive Colorful":
	["White", "Black", "Red", "Yellow", "Blue", "Purple", "Orange", "Green", "Brown", "Gray"],
	"Progressive Pilgrimage":
	["1 Sunwarp", "2 Sunwarp", "3 Sunwarp", "4 Sunwarp", "5 Sunwarp", "6 Sunwarp"]
}

const kTHE_END = 0
const kTHE_MASTER = 1
const kLEVEL_2 = 2
const kPILGRIMAGE = 3

const kNO_PANEL_SHUFFLE = 0
const kREARRANGE_PANELS = 1

const kCLASSIFICATION_LOCAL_NORMAL = 1
const kCLASSIFICATION_LOCAL_REDUCED = 2
const kCLASSIFICATION_LOCAL_INSANITY = 4
const kCLASSIFICATION_LOCAL_SMALL_SPHERE_ONE = 8

const kCLASSIFICATION_REMOTE_NORMAL = 0
const kCLASSIFICATION_REMOTE_REDUCED = 1
const kCLASSIFICATION_REMOTE_INSANITY = 2

const kSUNWARP_ACCESS_NORMAL = 0
const kSUNWARP_ACCESS_DISABLED = 1
const kSUNWARP_ACCESS_UNLOCK = 2
const kSUNWARP_ACCESS_INDIVIDUAL = 3
const kSUNWARP_ACCESS_PROGRESSIVE = 4

var _client = WebSocketClient.new()
var _should_process = false
var _initiated_disconnect = false
var _try_wss = false

var _datapackages = {}
var _pending_packages = []
var _item_id_to_name = {}  # All games
var _location_id_to_name = {}  # All games
var _item_name_to_id = {}  # LINGO only
var _location_name_to_id = {}  # LINGO only

var _remote_version = {"major": 0, "minor": 0, "build": 0}
var _gen_version = {"major": 0, "minor": 0, "build": 0}

# TODO: caching per MW/slot, reset between connections
var _authenticated = false
var _seed = ""
var _team = 0
var _slot = 0
var _players = []
var _player_name_by_slot = {}
var _game_by_player = {}
var _checked_locations = []
var _slot_data = {}
var _paintings_mapping = {}
var _localdata_file = ""
var _death_link = false
var _victory_condition = 0  # THE END, THE MASTER, LEVEL 2
var _door_shuffle = false
var _color_shuffle = false
var _panel_shuffle = 0  # none, rearrange
var _painting_shuffle = false
var _sunwarp_access = 0  # normal, disabled, unlock, progressive
var _mastery_achievements = 21
var _level_2_requirement = 223
var _location_classification_bit = 0
var _early_color_hallways = false
var _pilgrimage_compatibility = false  # set to true for pre-0.4.6
var _pilgrimage_enabled = false
var _pilgrimage_allows_roof_access = false
var _pilgrimage_allows_paintings = false
var _sunwarp_shuffle = false
var _sunwarp_mapping = []
var _speed_boost_mode = false
var _slot_seed = 0

var _map_loaded = false
var _held_items = []
var _held_locations = []
var _last_new_item = -1
var _progressive_progress = {}
var _has_colors = ["white"]
var _received_indexes = []
var _puzzle_skips = 0
var _cached_slowness = 0
var _cached_iceland = 0
var _cached_atbash = 0
var _cached_speed_boosts = 0
var _geronimo_skip = false
var _checked_paintings = []
var _hints_key = ""
var _hinted_locations = []

signal could_not_connect
signal connect_status
signal client_connected
signal evaluate_solvability


func _init():
	global._print("Instantiated APClient")

	# Read AP settings from file, if there are any
	var file = File.new()
	if file.file_exists("user://settings/archipelago"):
		file.open("user://settings/archipelago", File.READ)
		var data = file.get_var(true)
		file.close()

		if typeof(data) != TYPE_ARRAY:
			global._print("AP settings file is corrupted")
			data = []

		if data.size() > 0:
			ap_server = data[0]
		if data.size() > 1:
			ap_user = data[1]
		if data.size() > 2:
			ap_pass = data[2]
		if data.size() > 3:
			_datapackages = data[3]
		if data.size() > 4:
			confusify_world = data[4]
		if data.size() > 5:
			enable_multiplayer = data[5]
		if data.size() > 6:
			track_player = data[6]
		if data.size() > 7:
			connection_history = data[7]

		processDatapackages()


func _ready():
	_client.connect("connection_closed", self, "_closed")
	_client.connect("connection_failed", self, "_closed")
	_client.connect("server_disconnected", self, "_closed")
	_client.connect("connection_error", self, "_errored")
	_client.connect("connection_established", self, "_connected")
	_client.connect("data_received", self, "_on_data")


func _reset_state():
	_should_process = false
	_authenticated = false
	_map_loaded = false
	_try_wss = false


func _errored():
	if _try_wss:
		global._print("Could not connect to AP with ws://, now trying wss://")
		connectToServer()
	else:
		global._print("AP connection failed")
		_reset_state()

		emit_signal(
			"could_not_connect",
			"Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information."
		)


func _closed(_was_clean = true):
	global._print("Connection closed")
	_reset_state()

	if not _initiated_disconnect:
		emit_signal("could_not_connect", "Disconnected from Archipelago")

	_initiated_disconnect = false


func _connected(_proto = ""):
	global._print("Connected!")
	_try_wss = false


func disconnect_from_ap():
	_initiated_disconnect = true
	_client.disconnect_from_host()


func _on_data():
	var packet = _client.get_peer(1).get_packet()
	global._print("Got data from server: " + packet.get_string_from_utf8())
	var data = JSON.parse(packet.get_string_from_utf8())
	if data.error != OK:
		global._print("Error parsing packet from AP: " + data.error_string)
		return

	for message in data.result:
		var cmd = message["cmd"]
		global._print("Received command: " + cmd)

		if cmd == "RoomInfo":
			_seed = message["seed_name"]
			_remote_version = message["version"]
			_gen_version = message["generator_version"]

			var needed_games = []
			for game in message["datapackage_checksums"].keys():
				if (
					!_datapackages.has(game)
					or _datapackages[game]["checksum"] != message["datapackage_checksums"][game]
				):
					needed_games.append(game)

			if !needed_games.empty():
				_pending_packages = needed_games
				var cur_needed = _pending_packages.pop_front()
				requestDatapackages([cur_needed])
			else:
				connectToRoom()

		elif cmd == "DataPackage":
			for game in message["data"]["games"].keys():
				_datapackages[game] = message["data"]["games"][game]
			saveSettings()

			if !_pending_packages.empty():
				var cur_needed = _pending_packages.pop_front()
				requestDatapackages([cur_needed])
			else:
				processDatapackages()
				connectToRoom()

		elif cmd == "Connected":
			_authenticated = true
			_team = message["team"]
			_slot = message["slot"]
			_players = message["players"]
			_checked_locations = message["checked_locations"]
			_slot_data = message["slot_data"]

			for player in _players:
				_player_name_by_slot[player["slot"]] = player["alias"]
				_game_by_player[player["slot"]] = message["slot_info"][str(player["slot"])]["game"]

			_death_link = _slot_data.has("death_link") and _slot_data["death_link"]
			if _death_link:
				sendConnectUpdate(["DeathLink"])

			if _slot_data.has("victory_condition"):
				_victory_condition = _slot_data["victory_condition"]
			if _slot_data.has("shuffle_colors"):
				_color_shuffle = _slot_data["shuffle_colors"]

			if _slot_data.has("shuffle_doors"):
				_door_shuffle = (_slot_data["shuffle_doors"] > 0)
			else:
				_door_shuffle = false

			if _slot_data.has("shuffle_paintings"):
				_painting_shuffle = _slot_data["shuffle_paintings"]
			if _slot_data.has("shuffle_panels"):
				_panel_shuffle = _slot_data["shuffle_panels"]
			if _slot_data.has("sunwarp_access"):
				_sunwarp_access = _slot_data["sunwarp_access"]
			else:
				_sunwarp_access = kSUNWARP_ACCESS_NORMAL
			if _slot_data.has("seed"):
				_slot_seed = _slot_data["seed"]
			if _slot_data.has("painting_entrance_to_exit"):
				_paintings_mapping = _slot_data["painting_entrance_to_exit"]
			if _slot_data.has("mastery_achievements"):
				_mastery_achievements = _slot_data["mastery_achievements"]
			if _slot_data.has("level_2_requirement"):
				_level_2_requirement = _slot_data["level_2_requirement"]

			if _slot_data.has("location_checks"):
				if _slot_data["location_checks"] == kCLASSIFICATION_REMOTE_NORMAL:
					_location_classification_bit = kCLASSIFICATION_LOCAL_NORMAL
				elif _slot_data["location_checks"] == kCLASSIFICATION_REMOTE_REDUCED:
					_location_classification_bit = kCLASSIFICATION_LOCAL_REDUCED
				elif _slot_data["location_checks"] == kCLASSIFICATION_REMOTE_INSANITY:
					_location_classification_bit = kCLASSIFICATION_LOCAL_INSANITY
			else:
				_location_classification_bit = kCLASSIFICATION_LOCAL_NORMAL

			if _slot_data.has("early_color_hallways"):
				_early_color_hallways = _slot_data["early_color_hallways"]
			else:
				_early_color_hallways = false
			if _slot_data.has("enable_pilgrimage"):
				_pilgrimage_enabled = _slot_data["enable_pilgrimage"]
			else:
				_pilgrimage_compatibility = true
				_pilgrimage_enabled = true
			if _slot_data.has("pilgrimage_allows_roof_access"):
				_pilgrimage_allows_roof_access = _slot_data["pilgrimage_allows_roof_access"]
			else:
				_pilgrimage_allows_roof_access = true
			if _slot_data.has("pilgrimage_allows_paintings"):
				_pilgrimage_allows_paintings = _slot_data["pilgrimage_allows_paintings"]
			else:
				_pilgrimage_allows_paintings = true
			if _slot_data.has("shuffle_sunwarps"):
				_sunwarp_shuffle = _slot_data["shuffle_sunwarps"]
			else:
				_sunwarp_shuffle = false
			if _slot_data.has("sunwarp_permutation"):
				_sunwarp_mapping = _slot_data["sunwarp_permutation"]
			if _slot_data.has("speed_boost_mode"):
				_speed_boost_mode = _slot_data["speed_boost_mode"]
			else:
				_speed_boost_mode = false

			if (
				_location_classification_bit != kCLASSIFICATION_LOCAL_INSANITY
				and _door_shuffle
				and not _early_color_hallways
			):
				_location_classification_bit += kCLASSIFICATION_LOCAL_SMALL_SPHERE_ONE

			if track_player:
				setValue("PlayerPos", {"x": 0, "z": 0})
			else:
				setValue("PlayerPos", null)

			_puzzle_skips = 0
			_last_new_item = -1
			_cached_slowness = 0
			_cached_iceland = 0
			_cached_atbash = 0
			_cached_speed_boosts = 0
			_geronimo_skip = false

			_localdata_file = "user://archipelago_data/%s_%d" % [_seed, _slot]
			var ap_file = File.new()
			if ap_file.file_exists(_localdata_file):
				ap_file.open(_localdata_file, File.READ)
				var localdata = ap_file.get_var(true)
				ap_file.close()

				if typeof(localdata) != TYPE_ARRAY:
					global._print("AP localdata file is corrupted")
					localdata = []

				if localdata.size() > 0:
					_last_new_item = localdata[0]

				if localdata.size() > 1:
					_puzzle_skips = localdata[1]

				if localdata.size() > 2:
					_cached_slowness = localdata[2]

				if localdata.size() > 3:
					_cached_iceland = localdata[3]

				if localdata.size() > 4:
					_cached_atbash = localdata[4]

				if localdata.size() > 5:
					_geronimo_skip = localdata[5]

				if localdata.size() > 6:
					_cached_speed_boosts = localdata[6]

			requestSync()

			sendMessage(
				[
					{
						"cmd": "Set",
						"key": "Lingo_%d_Paintings" % [_slot],
						"default": [],
						"want_reply": true,
						"operations": [{"operation": "default", "value": []}]
					}
				]
			)

			_hints_key = "_read_hints_%d_%d" % [_team, _slot]
			sendMessage(
				[{"cmd": "SetNotify", "keys": [_hints_key]}, {"cmd": "Get", "keys": [_hints_key]}]
			)

			emit_signal("client_connected")

		elif cmd == "ConnectionRefused":
			var error_message = ""
			for error in message["errors"]:
				var submsg = ""
				if error == "InvalidSlot":
					submsg = "Invalid player name."
				elif error == "InvalidGame":
					submsg = "The specified player is not playing Lingo."
				elif error == "IncompatibleVersion":
					submsg = (
						"The Archipelago server is not the correct version for this client. Expected v%d.%d.%d. Found v%d.%d.%d."
						% [
							ap_version["major"],
							ap_version["minor"],
							ap_version["build"],
							_remote_version["major"],
							_remote_version["minor"],
							_remote_version["build"]
						]
					)
				elif error == "InvalidPassword":
					submsg = "Incorrect password."
				elif error == "InvalidItemsHandling":
					submsg = "Invalid item handling flag. This is a bug with the client. Please report it to the lingo-archipelago GitHub."

				if submsg != "":
					if error_message != "":
						error_message += " "
					error_message += submsg

			if error_message == "":
				error_message = "Unknown error."

			_initiated_disconnect = true
			_client.disconnect_from_host()

			emit_signal("could_not_connect", error_message)
			global._print("Connection to AP refused")
			global._print(message)

		elif cmd == "ReceivedItems":
			var i = 0
			for item in message["items"]:
				if _map_loaded:
					processItem(item["item"], message["index"] + i, item["player"], item["flags"])
				else:
					_held_items.append(
						{
							"item": item["item"],
							"index": message["index"] + i,
							"from": item["player"],
							"flags": item["flags"]
						}
					)
				i += 1

		elif cmd == "PrintJSON":
			parse_printjson(message)

			if (
				!message.has("receiving")
				or !message.has("item")
				or message["item"]["player"] != _slot
			):
				continue

			var item_name = "Unknown"
			var item_player_game = _game_by_player[message["receiving"]]
			if _item_id_to_name[item_player_game].has(message["item"]["item"]):
				item_name = _item_id_to_name[item_player_game][message["item"]["item"]]

			var location_name = "Unknown"
			var location_player_game = _game_by_player[message["item"]["player"]]
			if _location_id_to_name[location_player_game].has(message["item"]["location"]):
				location_name = _location_id_to_name[location_player_game][message["item"]["location"]]

			var player_name = "Unknown"
			if _player_name_by_slot.has(message["receiving"]):
				player_name = _player_name_by_slot[message["receiving"]]

			var item_color = colorForItemType(message["item"]["flags"])

			if message["type"] == "Hint":
				var is_for = ""
				if message["receiving"] != _slot:
					is_for = " for %s" % player_name
				if !message.has("found") || !message["found"]:
					messages.showMessage(
						(
							"Hint: [color=%s]%s[/color]%s is on %s"
							% [item_color, item_name, is_for, location_name]
						)
					)
			else:
				if message["receiving"] != _slot:
					var sentMsg = (
						"Sent [color=%s]%s[/color] to %s" % [item_color, item_name, player_name]
					)
					if _hinted_locations.has(message["item"]["location"]):
						sentMsg += " ([color=#fafad2]Hinted![/color])"
					messages.showMessage(sentMsg)

		elif cmd == "Bounced":
			if (
				_death_link
				and message.has("tags")
				and message.has("data")
				and message["tags"].has("DeathLink")
			):
				var first_sentence = "Received Death"
				var second_sentence = ""
				if message["data"].has("source"):
					first_sentence = "Received Death from %s" % message["data"]["source"]
				if message["data"].has("cause") and message["data"]["cause"] != "":
					second_sentence = ". Reason: %s" % message["data"]["cause"]
				messages.showMessage(first_sentence + second_sentence)

				# Return the player home.
				get_tree().get_root().get_node("Spatial/player/pause_menu")._reload()

		elif cmd == "SetReply":
			if message.has("key"):
				if message["key"] == ("Lingo_%d_Paintings" % _slot):
					_checked_paintings = message["value"]
				elif message["key"] == _hints_key:
					parseHints(message["value"])

		elif cmd == "Retrieved":
			if message.has("keys") and message["keys"].has(_hints_key):
				parseHints(message["keys"][_hints_key])


func _process(_delta):
	if _should_process:
		_client.poll()


func saveSettings():
	# Save the AP settings to disk.
	var dir = Directory.new()
	var path = "user://settings"
	if dir.dir_exists(path):
		pass
	else:
		dir.make_dir(path)

	var file = File.new()
	file.open("user://settings/archipelago", File.WRITE)

	var data = [
		ap_server,
		ap_user,
		ap_pass,
		_datapackages,
		confusify_world,
		enable_multiplayer,
		track_player,
		connection_history
	]
	file.store_var(data, true)
	file.close()


func saveLocaldata():
	# Save the MW/slot specific settings to disk.
	var dir = Directory.new()
	var path = "user://archipelago_data"
	if dir.dir_exists(path):
		pass
	else:
		dir.make_dir(path)

	var file = File.new()
	file.open(_localdata_file, File.WRITE)

	var effects_node = get_tree().get_root().get_node("Spatial/AP_Effects")

	var data = [
		_last_new_item,
		_puzzle_skips,
		effects_node.slowness_remaining,
		effects_node.iceland_remaining,
		effects_node.atbash_remaining,
		_geronimo_skip,
		effects_node.speed_boosts_remaining,
	]
	file.store_var(data, true)
	file.close()


func getSaveFileName():
	return "zzAP_%s_%d" % [_seed, _slot]


func connectToServer():
	_initiated_disconnect = false

	var url = ""
	if ap_server.begins_with("ws://") or ap_server.begins_with("wss://"):
		url = ap_server
		_try_wss = false
	elif _try_wss:
		url = "wss://" + ap_server
		_try_wss = false
	else:
		url = "ws://" + ap_server
		_try_wss = true

	var err = _client.connect_to_url(url)
	if err != OK:
		emit_signal(
			"could_not_connect",
			(
				"Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information. Error code: %d."
				% err
			)
		)
		global._print("Could not connect to AP: " + err)
		return
	_should_process = true

	emit_signal("connect_status", "Connecting...")


func sendMessage(msg):
	var payload = JSON.print(msg)
	_client.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT)
	_client.get_peer(1).put_packet(payload.to_utf8())


func requestDatapackages(games):
	emit_signal("connect_status", "Downloading %s data package..." % games[0])

	sendMessage([{"cmd": "GetDataPackage", "games": games}])


func processDatapackages():
	_item_id_to_name = {}
	_location_id_to_name = {}
	for game in _datapackages.keys():
		var package = _datapackages[game]

		_item_id_to_name[game] = {}
		for name in package["item_name_to_id"].keys():
			_item_id_to_name[game][package["item_name_to_id"][name]] = name

		_location_id_to_name[game] = {}
		for name in package["location_name_to_id"].keys():
			_location_id_to_name[game][package["location_name_to_id"][name]] = name

	if _datapackages.has("Lingo"):
		_item_name_to_id = _datapackages["Lingo"]["item_name_to_id"]
		_location_name_to_id = _datapackages["Lingo"]["location_name_to_id"]


func connectToRoom():
	emit_signal("connect_status", "Authenticating...")

	sendMessage(
		[
			{
				"cmd": "Connect",
				"password": ap_pass,
				"game": "Lingo",
				"name": ap_user,
				"uuid": SCRIPT_uuid.v4(),
				"version": ap_version,
				"items_handling": 0b111,  # always receive our items
				"tags": [],
				"slot_data": true
			}
		]
	)


func sendConnectUpdate(tags):
	sendMessage([{"cmd": "ConnectUpdate", "tags": tags}])


func requestSync():
	sendMessage([{"cmd": "Sync"}])


func sendLocation(loc_id):
	if _map_loaded:
		sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}])
	else:
		_held_locations.append(loc_id)


func setValue(key, value, operation = "replace"):
	sendMessage(
		[
			{
				"cmd": "Set",
				"key": "Lingo_%d_%s" % [_slot, key],
				"want_reply": false,
				"operations": [{"operation": operation, "value": value}]
			}
		]
	)


func say(textdata):
	sendMessage([{"cmd": "Say", "text": textdata}])


func completedGoal():
	sendMessage([{"cmd": "StatusUpdate", "status": 30}])  # CLIENT_GOAL

	messages.showMessage("You have completed your goal!")


func mapFinishedLoading():
	if !_map_loaded:
		_received_indexes.clear()
		_progressive_progress.clear()
		_has_colors = ["white"]
		emit_signal("evaluate_solvability")

		for item in _held_items:
			processItem(item["item"], item["index"], item["from"], item["flags"])

		sendMessage([{"cmd": "LocationChecks", "locations": _held_locations}])

		_map_loaded = true
		_held_items = []
		_held_locations = []


func processItem(item, index, from, flags):
	if index != null:
		if _received_indexes.has(index):
			# Do not re-process items.
			return

		_received_indexes.append(index)

	global._print(item)

	var gamedata = $Gamedata
	var item_name = "Unknown"
	if _item_id_to_name["Lingo"].has(item):
		item_name = _item_id_to_name["Lingo"][item]

	if gamedata.door_ids_by_item_id.has(int(item)):
		var doorsNode = get_tree().get_root().get_node("Spatial/Doors")
		for door_id in gamedata.door_ids_by_item_id[int(item)]:
			doorsNode.get_node(door_id).openDoor()

	if gamedata.painting_ids_by_item_id.has(int(item)):
		var real_parent_node = get_tree().get_root().get_node("Spatial/Decorations/Paintings")
		var fake_parent_node = get_tree().get_root().get_node_or_null("Spatial/AP_Paintings")

		for painting_id in gamedata.painting_ids_by_item_id[int(item)]:
			var painting_node = real_parent_node.get_node_or_null(painting_id)
			if painting_node != null:
				painting_node.movePainting()

			if _painting_shuffle:
				painting_node = fake_parent_node.get_node_or_null(painting_id)
				if painting_node != null:
					painting_node.get_node("Script").movePainting()

	if gamedata.warp_ids_by_item_id.has(int(item)):
		var warpsNode = get_tree().get_root().get_node("Spatial/Warps")
		for warp_id in gamedata.warp_ids_by_item_id[int(item)]:
			warpsNode.get_node(warp_id).unlock_warp()

	# Handle progressive items.
	if int(item) in gamedata.items_by_progressive_id.keys():
		if not int(item) in _progressive_progress:
			_progressive_progress[int(item)] = 0

		if _progressive_progress[int(item)] < gamedata.items_by_progressive_id[int(item)].size():
			var subitems = gamedata.items_by_progressive_id[int(item)]
			var subitem_id = subitems[_progressive_progress[int(item)]]
			global._print("Subitem: %d" % subitem_id)
			processItem(subitem_id, null, null, null)
			_progressive_progress[int(item)] += 1

	if _color_shuffle and color_items.has(_item_id_to_name["Lingo"][item]):
		var lcol = _item_id_to_name["Lingo"][item].to_lower()
		if not _has_colors.has(lcol):
			_has_colors.append(lcol)
			emit_signal("evaluate_solvability")

	# Show a message about the item if it's new. Also apply effects here.
	if index != null and index > _last_new_item:
		_last_new_item = index
		saveLocaldata()

		if item_name in progressive_items:
			var subitem = progressive_items[item_name][_progressive_progress[int(item)] - 1]
			item_name += " (%s)" % subitem

		var player_name = "Unknown"
		if _player_name_by_slot.has(from):
			player_name = _player_name_by_slot[from]

		var item_color = colorForItemType(flags)

		if from == _slot:
			messages.showMessage("Found [color=%s]%s[/color]" % [item_color, item_name])
		else:
			messages.showMessage(
				"Received [color=%s]%s[/color] from %s" % [item_color, item_name, player_name]
			)

		var effects_node = get_tree().get_root().get_node("Spatial/AP_Effects")
		if item_name == "Slowness Trap" and !_speed_boost_mode:
			effects_node.trigger_slowness_trap()
		if item_name == "Iceland Trap":
			effects_node.trigger_iceland_trap()
		if item_name == "Atbash Trap":
			effects_node.trigger_atbash_trap()
		if item_name == "Speed Boost" and _speed_boost_mode:
			effects_node.trigger_speed_boost()
		if item_name == "Puzzle Skip":
			_puzzle_skips += 1

			saveLocaldata()


func doorIsVanilla(door):
	return !$Gamedata.mentioned_doors.has(door)


func paintingIsVanilla(painting):
	return !$Gamedata.mentioned_paintings.has(painting)


func warpIsVanilla(warp):
	return !$Gamedata.mentioned_warps.has(warp)


func evaluateSolvability():
	emit_signal("evaluate_solvability")


func getAvailablePuzzleSkips():
	return _puzzle_skips


func usePuzzleSkip():
	_puzzle_skips -= 1

	saveLocaldata()


func geronimo():
	if !_geronimo_skip:
		messages.showMessage("Geronimo! You found a puzzle skip.")

		_puzzle_skips += 1
		_geronimo_skip = true
		saveLocaldata()


func checkPainting(painting_id):
	if _checked_paintings.has(painting_id):
		return

	_checked_paintings.append(painting_id)

	setValue("Paintings", [painting_id], "add")


func parseHints(hints):
	_hinted_locations.clear()

	for hint in hints:
		if hint["finding_player"] == int(_slot):
			_hinted_locations.append(hint["location"])


func colorForItemType(flags):
	var int_flags = int(flags)
	if int_flags & 1:  # progression
		return "#bc51e0"
	elif int_flags & 2:  # useful
		return "#2b67ff"
	elif int_flags & 4:  # trap
		return "#d63a22"
	else:  # filler
		return "#14de9e"


func parse_printjson(message):
	var parts = []
	for message_part in message["data"]:
		if !message_part.has("type") and message_part.has("text"):
			parts.append(message_part["text"])
		elif message_part["type"] == "player_id":
			if int(message_part["text"]) == _slot:
				parts.append("[color=#ee00ee]%s[/color]" % _player_name_by_slot[_slot])
			else:
				var from = float(message_part["text"])
				parts.append("[color=#fafad2]%s[/color]" % _player_name_by_slot[from])
		elif message_part["type"] == "item_id":
			var item_name = "Unknown"
			var item_player_game = _game_by_player[message_part["player"]]
			if _item_id_to_name[item_player_game].has(float(message_part["text"])):
				item_name = _item_id_to_name[item_player_game][float(message_part["text"])]

			parts.append(
				"[color=%s]%s[/color]" % [colorForItemType(message_part["flags"]), item_name]
			)
		elif message_part["type"] == "location_id":
			var location_name = "Unknown"
			var location_player_game = _game_by_player[message_part["player"]]
			if _location_id_to_name[location_player_game].has(float(message_part["text"])):
				location_name = _location_id_to_name[location_player_game][float(
					message_part["text"]
				)]

			parts.append("[color=#00ff7f]%s[/color]" % location_name)
		elif message_part.has("text"):
			parts.append(message_part["text"])

	var textclient_node = get_tree().get_root().get_node_or_null("Spatial/AP_TextClient")
	if textclient_node != null:
		textclient_node.parse_printjson("".join(parts))


func get_player_name():
	return _player_name_by_slot[_slot]


func compareVersion(lhs, rhs):
	if lhs["major"] == rhs["major"]:
		if lhs["minor"] == rhs["minor"]:
			return lhs["build"] < rhs["build"]
		else:
			return lhs["minor"] < rhs["minor"]
	else:
		return lhs["major"] < rhs["major"]


func wasGeneratedBeforeVersion(major, minor, build):
	return compareVersion(_gen_version, {"major": major, "minor": minor, "build": build})