from enum import IntEnum, auto from .generated import data_pb2 as data_pb2 from .items import SYMBOL_ITEMS from typing import TYPE_CHECKING, NamedTuple from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior if TYPE_CHECKING: from . import Lingo2World def calculate_letter_histogram(solution: str) -> dict[str, int]: histogram = dict() for l in solution: if l.isalpha(): real_l = l.upper() histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2) return histogram class AccessRequirements: items: set[str] progressives: dict[str, int] rooms: set[str] letters: dict[str, int] cyans: bool # This is an AND of ORs. or_logic: list[list["AccessRequirements"]] # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1. complete_at: int | None possibilities: list["AccessRequirements"] def __init__(self): self.items = set() self.progressives = dict() self.rooms = set() self.letters = dict() self.cyans = False self.or_logic = list() self.complete_at = None self.possibilities = list() def copy(self) -> "AccessRequirements": reqs = AccessRequirements() reqs.items = self.items.copy() reqs.progressives = self.progressives.copy() reqs.rooms = self.rooms.copy() reqs.letters = self.letters.copy() reqs.cyans = self.cyans reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic] reqs.complete_at = self.complete_at reqs.possibilities = self.possibilities.copy() return reqs def merge(self, other: "AccessRequirements"): for item in other.items: self.items.add(item) for item, amount in other.progressives.items(): self.progressives[item] = max(amount, self.progressives.get(item, 0)) for room in other.rooms: self.rooms.add(room) for letter, level in other.letters.items(): self.letters[letter] = max(self.letters.get(letter, 0), level) self.cyans = self.cyans or other.cyans for disjunction in other.or_logic: self.or_logic.append(disjunction) if other.complete_at is not None: # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports # conjunctions of requirements. if self.complete_at is not None: print("Merging requirements with complete_at > 1. This is messy and should be avoided!") left_req = AccessRequirements() left_req.complete_at = self.complete_at left_req.possibilities = self.possibilities self.or_logic.append([left_req]) self.complete_at = None self.possibilities = list() right_req = AccessRequirements() right_req.complete_at = other.complete_at right_req.possibilities = other.possibilities self.or_logic.append([right_req]) else: self.complete_at = other.complete_at self.possibilities = other.possibilities def is_empty(self) -> bool: return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None) def __eq__(self, other: "AccessRequirements"): return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and self.complete_at == other.complete_at and self.possibilities == other.possibilities) def simplify(self): resimplify = False if len(self.or_logic) > 0: old_or_logic = self.or_logic def remove_redundant(sub_reqs: "AccessRequirements"): sub_reqs.letters = {l: v for l, v in sub_reqs.letters.items() if self.letters.get(l, 0) < v} self.or_logic = [] for disjunction in old_or_logic: new_disjunction = [] for ssr in disjunction: remove_redundant(ssr) if not ssr.is_empty(): new_disjunction.append(ssr) else: new_disjunction.clear() break if len(new_disjunction) == 1: self.merge(new_disjunction[0]) resimplify = True elif len(new_disjunction) > 1: if all(cjr == new_disjunction[0] for cjr in new_disjunction): self.merge(new_disjunction[0]) resimplify = True else: self.or_logic.append(new_disjunction) if resimplify: self.simplify() def __repr__(self): parts = [] if len(self.items) > 0: parts.append(f"items={self.items}") if len(self.progressives) > 0: parts.append(f"progressives={self.progressives}") if len(self.rooms) > 0: parts.append(f"rooms={self.rooms}") if len(self.letters) > 0: parts.append(f"letters={self.letters}") if self.cyans: parts.append(f"cyans=True") if len(self.or_logic) > 0: parts.append(f"or_logic={self.or_logic}") if self.complete_at is not None: parts.append(f"complete_at={self.complete_at}") if len(self.possibilities) > 0: parts.append(f"possibilities={self.possibilities}") return f"AccessRequirements({", ".join(parts)})" class PlayerLocation(NamedTuple): code: int | None reqs: AccessRequirements class LetterBehavior(IntEnum): VANILLA = auto() ITEM = auto() UNLOCKED = auto() class Lingo2PlayerLogic: world: "Lingo2World" locations_by_room: dict[int, list[PlayerLocation]] event_loc_item_by_room: dict[int, dict[str, str]] item_by_door: dict[int, tuple[str, int]] panel_reqs: dict[int, AccessRequirements] proxy_reqs: dict[int, dict[str, AccessRequirements]] door_reqs: dict[int, AccessRequirements] real_items: list[str] double_letter_amount: dict[str, int] def __init__(self, world: "Lingo2World"): self.world = world self.locations_by_room = {} self.event_loc_item_by_room = {} self.item_by_door = {} self.panel_reqs = dict() self.proxy_reqs = dict() self.door_reqs = dict() self.real_items = list() self.double_letter_amount = dict() if self.world.options.shuffle_doors: for progressive in world.static_logic.objects.progressives: for i in range(0, len(progressive.doors)): self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) self.real_items.append(progressive.name) for door_group in world.static_logic.objects.door_groups: if door_group.type == data_pb2.DoorGroupType.CONNECTOR: if not self.world.options.shuffle_doors: continue elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR: if not self.world.options.shuffle_control_center_colors: continue elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP: if not self.world.options.shuffle_doors: continue else: continue for door in door_group.doors: self.item_by_door[door] = (door_group.name, 1) self.real_items.append(door_group.name) # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled # before we calculate any access requirements. for door in world.static_logic.objects.doors: if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: continue if door.id in self.item_by_door: continue if (door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and not self.world.options.shuffle_doors): continue if (door.type == data_pb2.DoorType.CONTROL_CENTER_COLOR and not self.world.options.shuffle_control_center_colors): continue if door.type == data_pb2.DoorType.GALLERY_PAINTING and not self.world.options.shuffle_gallery_paintings: continue door_item_name = self.world.static_logic.get_door_item_name(door) self.item_by_door[door.id] = (door_item_name, 1) self.real_items.append(door_item_name) # We handle cyan_door_behavior = Item after door shuffle, because cyan doors that are impacted by door shuffle # should be exempt from cyan_door_behavior. if world.options.cyan_door_behavior == CyanDoorBehavior.option_item: for door_group in world.static_logic.objects.door_groups: if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS: continue for door in door_group.doors: if not door in self.item_by_door: self.item_by_door[door] = (door_group.name, 1) self.real_items.append(door_group.name) for door in world.static_logic.objects.doors: if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id,
#include "godot_scene.h"
#include <absl/strings/str_split.h>
#include <absl/strings/string_view.h>
#include <fstream>
#include <sstream>
#include <variant>
namespace com::fourisland::lingo2_archipelago {
namespace {
struct Heading {
std::string type;
std::string id;
std::string path;
std::string resource_type;
std::string name;
std::string parent;
GodotInstanceType instance_type;
};
Heading ParseTscnHeading(absl::string_view line) {
std::string original_line(line);
Heading heading;
if (line[0] != '[') {
std::ostringstream errormsg;
errormsg << "Heading must start with [." << std::endl
<< "Bad heading: " << original_line;
throw std::invalid_argument(errormsg.str());
}
line.remove_prefix(1);
int divider = line.find_first_of(" ]");
if (divider == std::string_view::npos) {
std::ostringstream errormsg;
errormsg << "Malformatted heading: " << line << std::endl
<< "Original line: " << original_line;
throw std::invalid_argument(errormsg.str());
}
heading.type = std::string(line.substr(0, divider));
line.remove_prefix(divider + 1);
while (!line.empty()) {
divider = line.find_first_of("=");
if (divider == std::string_view::npos) {
std::ostringstream errormsg;
errormsg << "Malformatted heading: " << line << std::endl
<< "Original line: " << original_line;
throw std::invalid_argument(errormsg.str());
}
std::string key(line.substr(0, divider));
line.remove_prefix(divider + 1);
if (line[0] == '"') {
line.remove_prefix(1);
divider = line.find_first_of("\"");
if (divider == std::string_view::npos) {
std::ostringstream errormsg;
errormsg << "Malformatted heading: " << line << std::endl
<< "Original line: " << original_line;
throw std::invalid_argument(errormsg.str());
}
std::string strval(line.substr(0, divider));
line.remove_prefix(divider + 2);
if (key == "name") {
heading.name = strval;
} else if (key == "parent") {
heading.parent = strval;
} else if (key == "path") {
heading.path = strval;
} else if (key == "type") {
heading.resource_type = strval;
} else if (key == "id") {
heading.id = strval;
}
} else if (line[0] == 'S' || line[0] == 'E') {
GodotInstanceType rrval;
char internal = line[0];
line.remove_prefix(13); // SubResource("
divider = line.find_first_of("\"");
if (divider == std::string_view::npos) {
std::ostringstream errormsg;
errormsg << "Malformatted heading: " << line << std::endl
<< "Original line: " << original_line;
throw std::invalid_argument(errormsg.str());
}
std::string refid = std::string(line.substr(0, divider));
line.remove_prefix(divider + 3);
GodotInstanceType instance_type;
if (internal == 'E') {
instance_type = GodotExtResourceRef{.id = refid};
} else {
// SubResource is not supported right now.
}
if (key == "instance") {
heading.instance_type = instance_type;
} else {
// Other keys aren't supported right now.
}
} else {
divider = line.find_first_of(" ]");
if (divider == std::string_view::npos) {
std::ostringstream errormsg;
errormsg << "Malformatted heading: " << line << std::endl
<< "Original line: " << original_line;
throw std::invalid_argument(errormsg.str());
}
int numval = std::atoi(line.substr(0, divider).data());
line.remove_prefix(divider + 1);
// keyvals_[key] = numval;
}
}
return heading;
}
} // namespace
std::string GodotNode::GetPath() const {
if (parent.empty() || parent == ".") {
return name;
} else {
return parent + "/" + name;
}
}
GodotScene ReadGodotSceneFromFile(const std::string& path) {
std::map<std::string, GodotExtResource> ext_resources;
std::vector<GodotNode> nodes;
std::ifstream input(path);
std::string line;
bool section_started = false;
Heading cur_heading;
std::ostringstream cur_value;
bool value_started = false;
auto handle_end_of_section = [&]() {
section_started = false;
value_started = false;
if (cur_heading.type == "sub_resource") {
// sub_resources_[std::get<int>(cur_heading.GetKeyval("id"))] =
// {cur_heading, cur_value.str(), ""};
} else {
// other_.emplace_back(cur_heading, cur_value.str());
}
cur_value = {};
};
while (std::getline(input, line)) {
if (section_started && (line.empty() || line[0] == '[')) {
handle_end_of_section();
}
if (!line.empty() && line[0] == '[') {
Heading heading = ParseTscnHeading(line);
if (heading.type == "gd_scene") {
// file_descriptor_ = heading;
} else if (heading.type == "ext_resource") {
GodotExtResource ext_resource;
ext_resource.path = heading.path;
ext_resource.type = heading.resource_type;
ext_resources[heading.id] = ext_resource;
} else if (heading.type == "node") {
if (heading.parent != "") {
nodes.push_back(GodotNode{.name = heading.name,
.parent = heading.parent,
.instance_type = heading.instance_type});
}
} else {
cur_heading = heading;
section_started = true;
}
} else if (!line.empty()) {
if (value_started) {
cur_value << std::endl;
} else {
value_started = true;
}
cur_value << line;
}
}
if (section_started) {
handle_end_of_section();
}
return GodotScene(std::move(ext_resources), std::move(nodes));
}
} // namespace com::fourisland::lingo2_archipelago