Last active
October 9, 2022 17:47
-
-
Save pineapplemachine/b6ad5e9e94a502f5e6781273f256cb94 to your computer and use it in GitHub Desktop.
PokEditor human-readable CSV formatter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| This is a script that can be used in conjunection with the PokEditor tool. | |
| https://github.com/turtleisaac/PokEditor-v2 | |
| When using PokEditor locally, without online Google Sheets, game data is | |
| represented in a set of CSVs in the `local/` subdirectory of the user's | |
| chosen project directory. | |
| These CSVs contain Excel-style Google Sheets formulas that are used to | |
| refer to the value in another CSV. For example, the CSV which contains | |
| mon evolution data will use the text `=Personal!$B$2` to refer to | |
| the species name `BULBASAUR`, which is defined in the second column | |
| of the second row of `Personal.csv`. | |
| Unfortunately, this presents an issue to anyone who would like to modify | |
| these CSVs locally. The data is not easily human-readable in this form. | |
| This script can be used to take a PokEditor project's `local/` directory | |
| and resolve all of these cell formulae to their final value, and write | |
| the resulting CSVs to an output directory. The CSVs in the output will | |
| have, for instance, the text `BULBASAUR` instead of the formula | |
| `=Personal!$B$2`. | |
| Use the `encode` command to do this. | |
| $ python pokeditor_csv_formatter.py decode project/local project/local-readable | |
| After making changes to the human-readable CSVs in this outputted | |
| directory, the script can then be used to translate those human-readable | |
| CSVs back into the more forumla-friendly format that PokEditor expects. | |
| Use the `decode` command to do this. | |
| $ python pokeditor_csv_formatter.py encode project/local project/local-readable | |
| """ | |
| import os | |
| import sys | |
| import csv | |
| FORMATTING_CSV_NAME = "Formatting (DO NOT TOUCH)" | |
| def print_usage(): | |
| print("Usage:") | |
| print("$ python pokeditor_csv_formatter.py decode \"pokedit/project/local\" \"pokedit/project/local-human-readable\"") | |
| print("$ python pokeditor_csv_formatter.py encode \"pokedit/project/local\" \"pokedit/project/local-human-readable\"") | |
| def read_pokedit_csv(csv_path): | |
| rows = [] | |
| with open(csv_path, "rt", encoding="utf-8") as csv_file: | |
| for line in csv_file.readlines(): | |
| line_stripped = line[:-2] if line.endswith("\r\n") else line[:-1] | |
| rows.append(line_stripped.split(", ")) | |
| return rows | |
| def write_pokedit_csv(csv_path, rows): | |
| with open(csv_path, "wt", newline="", encoding="utf-8") as csv_file: | |
| for row in rows: | |
| csv_file.write(", ".join(row)) | |
| csv_file.write("\r\n") | |
| class ProjectIndirection: | |
| def __init__(self, name, column_index): | |
| self.name = name | |
| self.column_index = column_index | |
| def __eq__(self, other): | |
| return self.name == other.name and self.column_index == other.column_index | |
| def encode(self, row_number): | |
| name = "'%s'" % self.name if " " in self.name else self.name | |
| column_name = chr(ord("A") + self.column_index) | |
| return "=%s!$%s$%s" % (name, column_name, row_number) | |
| class ProjectFile: | |
| def __init__(self, project, name, encoded_rows=None, decoded_rows=None, indirections=None): | |
| self.project = project | |
| self.name = name | |
| self.encoded_rows = encoded_rows | |
| self.decoded_rows = decoded_rows | |
| self.indirections = indirections | |
| self.column_maps = {} | |
| @classmethod | |
| def read_encoded(cls, project, csv_path): | |
| name = os.path.splitext(os.path.basename(csv_path))[0] | |
| return cls(project, name, encoded_rows=read_pokedit_csv(csv_path)) | |
| @classmethod | |
| def read_decoded(cls, project, csv_path, ind_path, enc_path): | |
| name = os.path.splitext(os.path.basename(csv_path))[0] | |
| indirections = {} | |
| encoded_rows = None | |
| with open(ind_path, "rt", newline="", encoding="utf-8") as ind_file: | |
| reader = csv.reader(ind_file, delimiter=",") | |
| for row in reader: | |
| i = int(row[0]) | |
| if i not in indirections: | |
| indirections[i] = [] | |
| indirections[i].append(ProjectIndirection(row[1], int(row[2]))) | |
| with open(csv_path, "rt", newline="", encoding="utf-8") as csv_file: | |
| reader = csv.reader(csv_file, delimiter=",") | |
| decoded_rows = list(reader) | |
| if os.path.exists(enc_path): | |
| encoded_rows = read_pokedit_csv(enc_path) | |
| return cls( | |
| project, | |
| name, | |
| encoded_rows=encoded_rows, | |
| decoded_rows=decoded_rows, | |
| indirections=indirections, | |
| ) | |
| def write_encoded(self, csv_path): | |
| write_pokedit_csv(csv_path, self.encoded_rows) | |
| def write_decoded(self, csv_path, ind_path): | |
| with open(csv_path, "wt", newline="", encoding="utf-8") as csv_file: | |
| writer = csv.writer(csv_file, delimiter=",") | |
| writer.writerows(self.decoded_rows) | |
| with open(ind_path, "wt", newline="", encoding="utf-8") as ind_file: | |
| writer = csv.writer(ind_file, delimiter=",") | |
| for column_index, indirection_list in self.indirections.items(): | |
| for indirection in indirection_list: | |
| writer.writerow([ | |
| column_index, | |
| indirection.name, | |
| indirection.column_index, | |
| ]) | |
| def get_indirect_cell(self, column_index, row_number): | |
| if self.decoded_rows is not None: | |
| return self.decoded_rows[row_number - 1][column_index] | |
| else: | |
| return self.encoded_rows[row_number - 1][column_index] | |
| def find_indirect_cell(self, column_index, value): | |
| if column_index not in self.column_maps: | |
| self.column_maps[column_index] = self.build_column_map(column_index) | |
| return self.column_maps[column_index].get(value, None) | |
| def build_column_map(self, column_index): | |
| column_map = {} | |
| for row_index, row in enumerate(self.decoded_rows): | |
| column_map[row[column_index]] = 1 + row_index | |
| return column_map | |
| def decode_row(self, encoded_row): | |
| decoded_row = list() | |
| for column_index, encoded_cell in enumerate(encoded_row): | |
| decoded_row.append(self.decode_cell(column_index, encoded_cell)) | |
| return decoded_row | |
| def decode_cell(self, column_index, cell): | |
| parts = None | |
| if cell.startswith("='"): | |
| parts = cell[2:].split("$") | |
| if not parts[0].endswith("'!"): | |
| raise ValueError("Malformed indirection in %s." % self.name) | |
| parts[0] = parts[0][:-2] | |
| elif cell.startswith("="): | |
| parts = cell[1:].split("$") | |
| if not parts[0].endswith("!"): | |
| raise ValueError("Malformed indirection in %s." % self.name) | |
| parts[0] = parts[0][:-1] | |
| if parts is not None: | |
| ind_name = parts[0] | |
| ind_column_index = ord(parts[1]) - ord("A") | |
| ind_row_number = int(parts[2]) | |
| ind_file = self.project.files_map[ind_name] | |
| ind = ProjectIndirection(ind_name, ind_column_index) | |
| if column_index not in self.indirections: | |
| self.indirections[column_index] = [ind] | |
| elif ind not in self.indirections[column_index]: | |
| self.indirections[column_index].append(ind) | |
| try: | |
| return ind_file.get_indirect_cell( | |
| ind_column_index, ind_row_number | |
| ) | |
| except: | |
| return cell | |
| else: | |
| return cell | |
| def decode(self): | |
| self.decoded_rows = [] | |
| self.indirections = {} | |
| for encoded_row in self.encoded_rows: | |
| self.decoded_rows.append(self.decode_row(encoded_row)) | |
| def encode_row(self, decoded_row): | |
| encoded_row = list() | |
| for column_index, decoded_cell in enumerate(decoded_row): | |
| encoded_row.append(self.encode_cell(column_index, decoded_cell)) | |
| return encoded_row | |
| def encode_cell(self, column_index, cell): | |
| if column_index not in self.indirections: | |
| return cell | |
| else: | |
| for ind in self.indirections[column_index]: | |
| ind_file = self.project.files_map[ind.name] | |
| row_number = ind_file.find_indirect_cell(ind.column_index, cell) | |
| if self.name != "TM Learnsets" and ind.name == "Personal" and cell == " BULBASAUR": | |
| raise ValueError("Wtf %s %s", self.name, row_number) | |
| if row_number is not None: | |
| return ind.encode(row_number) | |
| return cell | |
| def encode(self): | |
| self.encoded_rows = [] | |
| for row_index, decoded_row in enumerate(self.decoded_rows): | |
| if row_index == 0: | |
| self.encoded_rows.append(decoded_row) | |
| else: | |
| self.encoded_rows.append(self.encode_row(decoded_row)) | |
| class Project: | |
| def __init__(self): | |
| self.files_map = {} | |
| def read_encoded(self, project_path): | |
| for root, dirs, files in os.walk(project_path): | |
| for file_name in files: | |
| if not file_name.endswith(".csv"): | |
| continue | |
| file_path = os.path.join(root, file_name) | |
| project_file = ProjectFile.read_encoded(self, file_path) | |
| self.files_map[project_file.name] = project_file | |
| def read_decoded(self, project_path): | |
| for root, dirs, files in os.walk(project_path): | |
| for file_name in files: | |
| if not file_name.endswith(".csv"): | |
| continue | |
| csv_path = os.path.join(root, file_name) | |
| base_path = os.path.splitext(csv_path)[0] | |
| ind_path = base_path + ".ind" | |
| enc_path = base_path + ".enc" | |
| project_file = ProjectFile.read_decoded( | |
| self, csv_path, ind_path, enc_path | |
| ) | |
| self.files_map[project_file.name] = project_file | |
| def decode_files(self): | |
| self.files_map[FORMATTING_CSV_NAME].decode() | |
| for file in self.files_map.values(): | |
| if file.decoded_rows is None: | |
| file.decode() | |
| def encode_files(self): | |
| for file in self.files_map.values(): | |
| if file.encoded_rows is None: | |
| file.encode() | |
| def write_encoded(self, project_path): | |
| os.makedirs(project_path, exist_ok=True) | |
| for file in self.files_map.values(): | |
| csv_path = os.path.join(project_path, file.name) + ".csv" | |
| file.write_encoded(csv_path) | |
| def write_decoded(self, project_path): | |
| os.makedirs(project_path, exist_ok=True) | |
| for file in self.files_map.values(): | |
| csv_path = os.path.join(project_path, file.name) + ".csv" | |
| ind_path = os.path.join(project_path, file.name) + ".ind" | |
| enc_path = os.path.join(project_path, file.name) + ".enc" | |
| file.write_decoded(csv_path, ind_path) | |
| if file.name == FORMATTING_CSV_NAME: | |
| file.write_encoded(enc_path) | |
| def test_project(encoded_input_path, decoded_output_path): | |
| if encoded_input_path == decoded_output_path: | |
| raise ValueError("Project input and output paths must be different.") | |
| project_test_path = decoded_output_path + ".test" | |
| decode_project(encoded_input_path, decoded_output_path) | |
| encode_project(decoded_output_path, project_test_path) | |
| any_not_matched = False | |
| for root, dirs, files in os.walk(encoded_input_path): | |
| for file_name in files: | |
| if file_name.endswith(".csv"): | |
| in_path = os.path.join(root, file_name) | |
| out_path = os.path.join(project_test_path, file_name) | |
| with open(in_path, "rt", encoding="utf-8") as in_file: | |
| in_content = in_file.read() | |
| with open(out_path, "rt", encoding="utf-8") as out_file: | |
| out_content = out_file.read() | |
| result = in_content == out_content | |
| any_not_matched = any_not_matched or (not result) | |
| print("%s: %s matches %s" % (result, in_path, out_path)) | |
| if any_not_matched: | |
| print("TEST FAILED") | |
| else: | |
| print("TEST PASSED") | |
| def encode_project(encoded_output_path, decoded_input_path): | |
| if decoded_input_path == encoded_output_path: | |
| raise ValueError("Project input and output paths must be different.") | |
| project = Project() | |
| project.read_decoded(decoded_input_path) | |
| project.encode_files() | |
| project.write_encoded(encoded_output_path) | |
| def decode_project(encoded_input_path, decoded_output_path): | |
| if encoded_input_path == decoded_output_path: | |
| raise ValueError("Project input and output paths must be different.") | |
| project = Project() | |
| project.read_encoded(encoded_input_path) | |
| project.decode_files() | |
| project.write_decoded(decoded_output_path) | |
| def __main__(): | |
| if len(sys.argv) != 4: | |
| print_usage() | |
| sys.exit(1) | |
| elif sys.argv[1] == "decode": | |
| decode_project(sys.argv[2], sys.argv[3]) | |
| elif sys.argv[1] == "encode": | |
| encode_project(sys.argv[2], sys.argv[3]) | |
| elif sys.argv[1] == "test": | |
| test_project(sys.argv[2], sys.argv[3]) | |
| else: | |
| print_usage() | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| __main__() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment