From 1e58288b3fdb5a94b50014931f3adb3144c5ee23 Mon Sep 17 00:00:00 2001 From: maple! Date: Tue, 9 Jul 2024 15:05:52 +0200 Subject: [PATCH] start rebasing the generated bindings (#205) --- .gitignore | 1 + binding_generator/README.md | 24 ++ binding_generator/generate.py | 364 ++++++++++++++++++++ binding_generator/templates/__init__.py | 1 + binding_generator/templates/copyright.tmpl | 18 + binding_generator/templates/rpg_header.tmpl | 73 ++++ binding_generator/templates/rpg_source.tmpl | 70 ++++ src/ui/MainWindow.qml | 42 +++ src/ui/Ui.qrc | 5 + 9 files changed, 598 insertions(+) create mode 100644 binding_generator/README.md create mode 100755 binding_generator/generate.py create mode 100644 binding_generator/templates/__init__.py create mode 100644 binding_generator/templates/copyright.tmpl create mode 100644 binding_generator/templates/rpg_header.tmpl create mode 100644 binding_generator/templates/rpg_source.tmpl create mode 100644 src/ui/MainWindow.qml create mode 100644 src/ui/Ui.qrc diff --git a/.gitignore b/.gitignore index 390782dd..a77c0e59 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ Makefile* /bin/platforms *.cbp translations/ +/src/binding/generated/ # flatpak /.flatpak-builder diff --git a/binding_generator/README.md b/binding_generator/README.md new file mode 100644 index 00000000..f800a570 --- /dev/null +++ b/binding_generator/README.md @@ -0,0 +1,24 @@ +# EasyRPG Editor binding code generator + +Files in the `generated` subdirectory of `src/binding` directory are +regenerated automatically when running the `generate.py` script. + +These source code files are generated with the `templates` subfolder. +As first argument pass the path to the liblcf `generator/csv` directory. + +## Requirements + +* Python interpreter 3. +* Jinja2 template engine. +* pandas data analysis library +* CSV files can be modified with any text editor or (at your option) any + spreadsheet editor. + + +## Usage + +1. Open one of the .csv files in the `csv` subdirectory to edit or add new + data then save file changes. +2. Run the script file `generate.py` from the `generator` folder. +3. Add any newly created .cpp and .h files to project files if needed. +4. Recompile EasyRPG Editor. diff --git a/binding_generator/generate.py b/binding_generator/generate.py new file mode 100755 index 00000000..eefc26d6 --- /dev/null +++ b/binding_generator/generate.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 + +import pandas as pd +import numpy as np +import sys +import os +import re +import shutil +import filecmp +from collections import namedtuple, OrderedDict +from itertools import groupby +import operator + +from jinja2 import Environment, PackageLoader, select_autoescape +env = Environment( + loader=PackageLoader('templates', ''), + autoescape=select_autoescape([]), + keep_trailing_newline=True, + lstrip_blocks=True +) + +gen_dir = os.path.dirname(os.path.abspath(__file__)) +csv_dir = sys.argv[1] +dest_dir = os.path.abspath(os.path.join(gen_dir, "..", "src", "binding", "generated")) +tmp_dir = os.path.join(dest_dir, "tmp") + +qt_types = { + 'Boolean': 'bool', + 'Double': 'double', + 'UInt8': 'int', + 'UInt16': 'int', + 'UInt32': 'int', + 'Int8': 'int', + 'Int16': 'int', + 'Int32': 'int', + 'String': 'QString', + 'DBString': 'QString', + 'DBBitArray': 'QVector', +} + +# Additional Jinja 2 functions +def qt_type(ty, prefix=True): + if ty in qt_types: + return qt_types[ty] + + if ty == "DatabaseVersion": + return 'int' + + if ty == "EmptyBlock": + return 'void' + + m = re.match(r'Count<(.*)>', ty) + if m: + return qt_type(m.group(1), prefix) + + m = re.match(r'Array<(.*):(.*)>', ty) + if m: + return 'ArrayAdapter*' + + m = re.match(r'(Vector|Array)<(.*)>', ty) + if m: + if type_is_struct(m.group(2)): + return 'ArrayAdapter*' + return 'QVector<%s>' % qt_type(m.group(2), prefix) + + m = re.match(r'DBArray<(.*)>', ty) + if m: + return 'QVector<%s>' % qt_type(m.group(1), prefix) + + m = re.match(r'Ref<(.*):(.*)>', ty) + if m: + return qt_type(m.group(2), prefix) + + m = re.match(r'Ref<(.*)>', ty) + if m: + return 'int' + + m = re.match(r'Enum<(.*)>', ty) + if m: + return 'int' + + m = re.match(r'(.*)_Flags$', ty) + if m: + ty = m.expand(r'\1::Flags') + if prefix: + ty = "Binding::" + ty + "*" + return ty + + if prefix: + ty = "Binding::" + ty + "*" + + return ty + +def inner_type(ty): + m = re.match(r'.*?<([^:]+).*>', ty) + if m: + return m.group(1) + return ty + +def num_flags(flag): + return len(flag) + +def filter_structs_without_codes(structs): + for struct in structs: + if all(f.code for f in sfields[struct.name]): + yield struct +# End of Jinja 2 functions + +int_types = { + 'UInt8': 'uint8_t', + 'UInt16': 'uint16_t', + 'UInt32': 'uint32_t', + 'Int16': 'int16_t', + 'Int32': 'int32_t' +} + +def struct_headers(ty, header_map): + m = re.match(r'Ref<(.*):(.*)>', ty) + if m: + return struct_headers(m.group(2), header_map) + + m = re.match(r'Array<(.*):(.*)>', ty) + if m: + return struct_headers(m.group(1), header_map) + + m = re.match(r'(Vector|Array)<(.*)>', ty) + if m: + return struct_headers(m.group(2), header_map) + + header = header_map.get(ty) + if header is not None: + return ['"%s.h"' % header] + + if ty in ['Parameters', 'Equipment', 'EventCommand', 'MoveCommand', 'Rect', 'TreeMap']: + return ['"%s.h"' % ty.lower()] + + return [] + +def merge_dicts(dicts): + # Merges multiple dicts into one + out_dict = dicts[0] + + for d in dicts[1:]: + for k,v in d.items(): + if k in out_dict: + # Append new values + for vv in v: + out_dict[k].append(vv) + else: + # Insert whole key + out_dict[k] = v + + return out_dict + +def process_file(filename, namedtup): + # Mapping is: All elements of the line grouped by the first column + + path = os.path.join(csv_dir, filename) + df = pd.read_csv(path, comment='#', dtype=str) + df = df.fillna("") + + lines = [ list(r) for _i, r in df.iterrows() ] + + result = OrderedDict() + for k, g in groupby(lines, operator.itemgetter(0)): + result[k] = list(map(lambda x: namedtup(*x[1:]), list(g))) + + return result + +def get_structs(*filenames): + Struct = namedtuple("Struct", "name base hasid") + + results = list(map(lambda x: process_file(x, Struct), filenames)) + + processed_result = OrderedDict() + + for k, struct in merge_dicts(results).items(): + processed_result[k] = [] + + for elem in struct: + elem = Struct(elem.name, elem.base, bool(int(elem.hasid)) if elem.hasid else None) + processed_result[k].append(elem) + + processed_flat = [] + for filetype, struct in processed_result.items(): + for elem in struct: + processed_flat.append(elem) + + return processed_result, processed_flat + +def get_fields(*filenames): + Field = namedtuple("Field", "name size type code default presentifdefault is2k3 comment") + + results = list(map(lambda x: process_file(x, Field), filenames)) + + processed_result = OrderedDict() + + for k, field in merge_dicts(results).items(): + processed_result[k] = [] + for elem in field: + if elem.size == 't': + continue + if not elem.type or elem.type == "EmptyBlock": + continue + elem = Field( + elem.name, + True if elem.size == 't' else False, + elem.type, + 0 if elem.code == '' else int(elem.code, 0), + elem.default, + elem.presentifdefault, + elem.is2k3, + elem.comment) + processed_result[k].append(elem) + + return processed_result + +def get_enums(*filenames): + results = list(map(lambda x: process_file(x, namedtuple("Enum", "entry value index")), filenames)) + new_result = OrderedDict() + + # Additional processing to group by the Enum Entry + # Results in e.g. EventCommand -> Code -> List of (Name, Index) + for k, v in merge_dicts(results).items(): + new_result[k] = OrderedDict() + for kk, gg in groupby(v, operator.attrgetter("entry")): + new_result[k][kk] = list(map(lambda x: (x.value, x.index), gg)) + + return new_result + +def get_flags(*filenames): + results = list(map(lambda x: process_file(x, namedtuple("Flag", "field is2k3")), filenames)) + return merge_dicts(results) + +def get_functions(*filenames): + Function = namedtuple("Function", "method static headers") + + results = list(map(lambda x: process_file(x, Function), filenames)) + + processed_result = OrderedDict() + + for k, field in merge_dicts(results).items(): + processed_result[k] = [] + for elem in field: + elem = Function( + elem.method, + elem.static == 't', + elem.headers) + processed_result[k].append(elem) + + return processed_result + +def get_constants(filename='constants.csv'): + return process_file(filename, namedtuple("Constant", "name type value comment")) + +def type_is_db_string(ty): + return ty == 'DBString' + +def type_is_string(ty): + return ty == 'String' + +def type_is_array(ty): + return re.match(r'(Vector|Array|DBArray)<(.*)>', ty) or ty == "DBBitArray" + +def type_is_struct(ty): + return ty in [ x.name for x in structs_flat ] + +def type_can_write(ty): + if qt_type(ty) in ["bool", "int", "double", "QString"]: + return True + return type_is_array(ty) and not type_is_array_of_struct(ty) + +def type_is_array_of_struct(ty): + m = re.match(r'(Vector|Array|DBArray)<(.*)>', ty) + return m and type_is_struct(m.group(2).split(":")[0]) + +def openToRender(path): + subdir = os.path.dirname(path) + if not os.path.exists(subdir): + os.makedirs(subdir) + return open(path, 'w') + +def generate(): + if not os.path.exists(tmp_dir): + os.mkdir(tmp_dir) + + for filetype, structlist in structs.items(): + for struct in structlist: + filename = struct.name.lower() + + filepath = os.path.join(tmp_dir, '%s.h' % filename) + with openToRender(filepath) as f: + f.write(rpg_header_tmpl.render( + struct_name=struct.name, + struct_base=struct.base, + has_id=struct.hasid + )) + + filepath = os.path.join(tmp_dir, '%s.cpp' % filename) + with openToRender(filepath) as f: + f.write(rpg_source_tmpl.render( + struct_name=struct.name, + struct_base=struct.base, + has_id=struct.hasid, + filename=filename + )) + + for dirname, subdirlist, filelist in os.walk(tmp_dir, topdown=False): + subdir = os.path.relpath(dirname, tmp_dir) + + for tmp_file in filelist: + tmp_path = os.path.join(tmp_dir, subdir, tmp_file) + dest_path = os.path.join(dest_dir, subdir, tmp_file) + dest_subdir = os.path.dirname(dest_path) + if not os.path.exists(dest_subdir): + os.mkdir(dest_subdir) + if not (os.path.exists(dest_path) and filecmp.cmp(tmp_path, dest_path)): + shutil.copyfile(tmp_path, dest_path) + os.remove(tmp_path) + os.rmdir(os.path.join(dirname)) + +def main(argv): + if not os.path.exists(dest_dir): + os.mkdir(dest_dir) + + global structs, structs_flat, sfields, enums, flags, functions, constants + global chunk_tmpl, lcf_struct_tmpl, rpg_header_tmpl, rpg_source_tmpl, flags_tmpl, enums_tmpl, fwd_tmpl, fwd_struct_tmpl + + structs, structs_flat = get_structs('structs.csv','structs_easyrpg.csv') + sfields = get_fields('fields.csv','fields_easyrpg.csv') + enums = get_enums('enums.csv','enums_easyrpg.csv') + flags = get_flags('flags.csv') + functions = get_functions('functions.csv') + constants = get_constants() + + # Setup Jinja + env.filters["qt_type"] = qt_type + env.filters["inner_type"] = inner_type + env.filters["struct_has_code"] = filter_structs_without_codes + env.filters["num_flags"] = num_flags + env.tests['is_db_string'] = type_is_db_string + env.tests['is_string'] = type_is_string + env.tests['is_array'] = type_is_array + env.tests['is_array_of_struct'] = type_is_array_of_struct + env.tests['is_struct'] = type_is_struct + env.tests['can_write'] = type_can_write + + globals = dict( + structs=structs, + structs_flat=structs_flat, + fields=sfields, + flags=flags, + enums=enums, + functions=functions, + constants=constants, + ) + + rpg_header_tmpl = env.get_template('rpg_header.tmpl', globals=globals) + rpg_source_tmpl = env.get_template('rpg_source.tmpl', globals=globals) + + generate() + +if __name__ == '__main__': + main(sys.argv) diff --git a/binding_generator/templates/__init__.py b/binding_generator/templates/__init__.py new file mode 100644 index 00000000..11d3a947 --- /dev/null +++ b/binding_generator/templates/__init__.py @@ -0,0 +1 @@ +# Required to make the Jinja2 Template Loader happy \ No newline at end of file diff --git a/binding_generator/templates/copyright.tmpl b/binding_generator/templates/copyright.tmpl new file mode 100644 index 00000000..cf10b91f --- /dev/null +++ b/binding_generator/templates/copyright.tmpl @@ -0,0 +1,18 @@ +/* !!!! GENERATED FILE - DO NOT EDIT !!!! + * -------------------------------------- + * + * This file is part of EasyRPG Editor. + * + * EasyRPG Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Editor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Editor. If not, see . + */ diff --git a/binding_generator/templates/rpg_header.tmpl b/binding_generator/templates/rpg_header.tmpl new file mode 100644 index 00000000..e38c6197 --- /dev/null +++ b/binding_generator/templates/rpg_header.tmpl @@ -0,0 +1,73 @@ +{% include "copyright.tmpl" %} +#pragma once + +// Headers +#include +#include "binding/binding_base.h" +#include "binding/array_adapter.h" +{%- for field in fields[struct_name]|sort %} +{%- if not field.size and field.type is is_array_of_struct or field.type is is_struct %} +#include "binding/{{ field.type|inner_type|lower }}.h" +{%- endif %} +{%- endfor %} + +class ProjectData; + +/** + * Binding::Generated::{{ struct_name }} class. + * Exposes lcf::rpg::{{ struct_name }} to QML. + */ +namespace Binding::Generated { +class {{ struct_name }} : public Binding::BindingBase { + Q_OBJECT + {%- if has_id %} + Q_PROPERTY(int id READ id CONSTANT) + {%- endif %} + {%- for field in fields[struct_name] %} + {%- if not field.type.endswith("_Flags") %} + {%- if field.type is can_write %} + Q_PROPERTY({{ field.type|qt_type }} {{ field.name }} READ {{ field.name }} WRITE set_{{ field.name }} NOTIFY {{ field.name}}_changed) + {%- else %} + Q_PROPERTY({{ field.type|qt_type }} {{ field.name }} READ {{ field.name }} CONSTANT) + {%- endif %} + {%- endif %} + {%- endfor %} + +public: + {{ struct_name }}(ProjectData& project, lcf::rpg::{{ struct_name }}& data, QObject* parent = nullptr); + + lcf::rpg::{{ struct_name }}& data(); + + {%- if has_id %} + int id(); + {%- endif %} + {%- for field in fields[struct_name] %} + {%- if not field.type.endswith("_Flags") %} + {{ field.type|qt_type }} {{ field.name }}(); + {%- if field.type is can_write %} + void set_{{ field.name }}(const {{ field.type|qt_type }}& new_{{ field.name }}); + {%- endif %} + {%- endif %} + {%- endfor %} + +signals: + {%- for field in fields[struct_name] %} + {%- if not field.type.endswith("_Flags") %} + {%- if field.type is can_write %} + void {{ field.name}}_changed(); + {%- endif %} + {%- endif %} + {%- endfor %} + +protected: + lcf::rpg::{{ struct_name }}& m_data; + + {%- for field in fields[struct_name] %} + {%- if not field.type.endswith("_Flags") %} + {%- if field.type is is_array_of_struct or field.type is is_struct %} + {{ field.type|qt_type }} m_{{ field.name }}; + {%- endif %} + {%- endif %} + {%- endfor %} +}; +} // namespace Binding::Generated diff --git a/binding_generator/templates/rpg_source.tmpl b/binding_generator/templates/rpg_source.tmpl new file mode 100644 index 00000000..d6f67dfe --- /dev/null +++ b/binding_generator/templates/rpg_source.tmpl @@ -0,0 +1,70 @@ +{% include "copyright.tmpl" %} +// Headers +#include "{{ filename }}.h" +#include "common/dbstring.h" + +namespace Binding::Generated { + {{ struct_name }}::{{ struct_name }}(ProjectData& project, lcf::rpg::{{ struct_name }}& data, QObject* parent) : Binding::BindingBase(project, parent), m_data(data) { + {%- for field in fields[struct_name] %} + {%- if not field.type.endswith("_Flags") %} + {%- if field.type is is_array_of_struct %} + { + m_{{ field.name }} = new ArrayAdapter(this); + auto& arr = m_{{ field.name }}->data(); + for (auto& var: m_data.{{ field.name }}) + arr.push_back(new Binding::{{ field.type|inner_type }}(m_project, var, this)); + } + {%- elif field.type is is_struct %} + m_{{ field.name }} = new Binding::{{ field.type }}(m_project, m_data.{{ field.name }}, this); + {%- endif %} + {%- endif %} + {%- endfor %} + } + + {%- if has_id %} + int {{ struct_name }}::id() { + return m_data.ID; + } + {%- endif %} + {%- for field in fields[struct_name] %} + {%- if not field.type.endswith("_Flags") %} + {{ field.type|qt_type }} {{ struct_name }}::{{ field.name }}() { + {%- if field.type is is_db_string %} + return ToQString(m_data.{{ field.name }}); + {%- elif field.type is is_string %} + return QString::fromStdString(m_data.{{ field.name }}); + {%- elif field.type is is_array_of_struct %} + return m_{{ field.name }}; + {%- elif field.type is is_array %} + return {{ field.type|qt_type }}(m_data.{{ field.name }}.begin(), m_data.{{ field.name }}.end()); + {%- elif field.type is is_struct %} + return m_{{ field.name }}; + {%- else %} + return m_data.{{ field.name }}; + {%- endif %} + } + {%- if field.type is can_write %} + void {{ struct_name }}::set_{{ field.name }}(const {{ field.type|qt_type }}& new_{{ field.name }}) { + {%- if field.type is is_db_string %} + if (ToQString(m_data.{{ field.name }}) == new_{{ field.name }}) + return; + m_data.{{ field.name }} = ToDBString(new_{{ field.name }}); + {%- elif field.type is is_string %} + if (QString::fromStdString(m_data.{{ field.name }}) == new_{{ field.name }}) + return; + m_data.{{ field.name }} = new_{{ field.name }}.toStdString(); + {%- elif field.type is is_array %} + if ({{ field.type|qt_type }}(m_data.{{ field.name }}.begin(), m_data.{{ field.name }}.end()) == new_{{ field.name }}) + return; + m_data.{{ field.name }} = decltype(m_data.{{ field.name }})(new_{{ field.name }}.begin(), new_{{ field.name }}.end()); + {%- else %} + if (m_data.{{ field.name }} == new_{{ field.name }}) + return; + m_data.{{ field.name }} = new_{{ field.name }}; + {%- endif %} + emit {{ field.name }}_changed(); + } + {%- endif %} + {% endif %} + {%- endfor %} +} // namespace Binding::Generated diff --git a/src/ui/MainWindow.qml b/src/ui/MainWindow.qml new file mode 100644 index 00000000..f4b3d4ed --- /dev/null +++ b/src/ui/MainWindow.qml @@ -0,0 +1,42 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.15 as Kirigami + +Kirigami.ApplicationWindow { + // ID provides unique identifier to reference this element + id: root + title: "EasyRPG Editor" + + property var actor + property var db + + // Initial page to be loaded on app load + pageStack.initialPage: Text { + text: `Hello World from ${db.actors.get(1).name}!` + } + + Component.onCompleted: { + db = project.database + + // Just some testing: + console.log("Hello World!") + + // Accessing a term + console.log(db.terms.actor_critical) + + // Setting an array element + db.actors.get(1).parameters.maxhp[3] = 100; + console.log(db.actors.get(1).parameters.maxhp) + + // Getting an actor and assigning it to a property + actor = db.actors.get(2) + actor.sayHello() + + // Accessing the parent of actor (database) + console.log(actor.parent.items.get(1).description) + + // Output the start map name + console.log(project.treemap.maps.get(project.treemap.start.party_map_id).name) + } +} diff --git a/src/ui/Ui.qrc b/src/ui/Ui.qrc new file mode 100644 index 00000000..1786556f --- /dev/null +++ b/src/ui/Ui.qrc @@ -0,0 +1,5 @@ + + + MainWindow.qml + +