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