diff --git a/.pylintrc b/.pylintrc index dbd657d..28911a8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ [MESSAGES CONTROL] -disable=E1101,W0511,W0612,W0613,W0212,W0221,W0703,W0622,C,R +disable=E1101,W0511,W0612,W0613,W0212,W0221,W0703,W0622,W1113,C,R [BASIC] argument-rgx=[a-z_][a-z0-9_]{0,30}$ diff --git a/README.md b/README.md index d36eb4b..fab3b77 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,21 @@ functionality and make it easier to perform certain tasks. Let's look at an example by creating a new scene file for a Player: ```python - from godot_parser import GDScene, Node - - scene = GDScene() - res = scene.add_ext_resource("res://PlayerSprite.png", "PackedScene") - with scene.use_tree() as tree: - tree.root = Node("Player", type="KinematicBody2D") - tree.root.add_child( - Node( - "Sprite", - type="Sprite", - properties={"texture": res.reference}, - ) - ) - scene.write("Player.tscn") +from godot_parser import GDPackedScene, Node, VersionOutputFormat + +scene = GDPackedScene() +res = scene.add_ext_resource("res://PlayerSprite.png", "PackedScene") +with scene.use_tree() as tree: + tree.root = Node("Player", type="KinematicBody2D") + tree.root.add_child( + Node( + "Sprite", + type="Sprite", + properties={"texture": res.reference}, + ) + ) + +scene.write("Player.tscn", VersionOutputFormat("4.6")) ``` It's much easier to use the high-level API when it's available, but it doesn't diff --git a/godot_parser/__init__.py b/godot_parser/__init__.py index 9473177..0c2c6dd 100644 --- a/godot_parser/__init__.py +++ b/godot_parser/__init__.py @@ -1,5 +1,6 @@ from .files import * from .objects import * +from .output import * from .sections import * from .tree import * diff --git a/godot_parser/files.py b/godot_parser/files.py index b5344ae..f4af56a 100644 --- a/godot_parser/files.py +++ b/godot_parser/files.py @@ -1,29 +1,31 @@ import os +import re from contextlib import contextmanager -from typing import ( - Iterable, - Iterator, - List, - Optional, - Sequence, - Type, - TypeVar, - Union, - cast, +from typing import Any, Iterable, Iterator, List, Optional, Sequence, Type, Union, cast + +from .objects import ( + ExtResource, + GDIterable, + GDObject, + PackedByteArray, + PackedVector4Array, + ResourceReference, + SubResource, ) - -from .objects import ExtResource, GDObject, SubResource +from .output import Outputable, OutputFormat, VersionOutputFormat from .sections import ( + GDBaseResourceSection, GDExtResourceSection, + GDFileHeader, GDNodeSection, + GDResourceSection, GDSection, - GDSectionHeader, GDSubResourceSection, ) from .structure import scene_file from .util import find_project_root, gdpath_to_filepath -__all__ = ["GDFile", "GDScene", "GDResource"] +__all__ = ["GDFile", "GDCommonFile", "GDPackedScene", "GDResource"] # Scene and resource files seem to group the section types together and sort them. # This is the order I've observed @@ -38,14 +40,12 @@ "editable", ] -GDFileType = TypeVar("GDFileType", bound="GDFile") - class GodotFileException(Exception): """Thrown when there are errors in a Godot file""" -class GDFile(object): +class GDFile(Outputable): """Base class representing the contents of a Godot file""" project_root: Optional[str] = None @@ -86,10 +86,6 @@ def get_sections(self, name: Optional[str] = None) -> List[GDSection]: return self._sections return [s for s in self._sections if s.header.name == name] - def get_nodes(self) -> List[GDNodeSection]: - """Get all [node] sections""" - return cast(List[GDNodeSection], self.get_sections("node")) - def get_ext_resources(self) -> List[GDExtResourceSection]: """Get all [ext_resource] sections""" return cast(List[GDExtResourceSection], self.get_sections("ext_resource")) @@ -98,15 +94,6 @@ def get_sub_resources(self) -> List[GDSubResourceSection]: """Get all [sub_resource] sections""" return cast(List[GDSubResourceSection], self.get_sections("sub_resource")) - def find_node( - self, property_constraints: Optional[dict] = None, **constraints - ) -> Optional[GDNodeSection]: - """Find first [node] section that matches (see find_section)""" - return cast( - GDNodeSection, - self.find_section("node", property_constraints, **constraints), - ) - def find_ext_resource( self, property_constraints: Optional[dict] = None, **constraints ) -> Optional[GDExtResourceSection]: @@ -176,18 +163,330 @@ def find_all( def add_ext_resource(self, path: str, type: str) -> GDExtResourceSection: """Add an ext_resource""" - next_id = 1 + max([s.id for s in self.get_ext_resources()] + [0]) - section = GDExtResourceSection(path, type, next_id) + section = GDExtResourceSection(path, type) self.add_section(section) return section def add_sub_resource(self, type: str, **kwargs) -> GDSubResourceSection: """Add a sub_resource""" - next_id = 1 + max([s.id for s in self.get_sub_resources()] + [0]) - section = GDSubResourceSection(type, next_id, **kwargs) + section = GDSubResourceSection(type, **kwargs) self.add_section(section) return section + @classmethod + def parse(cls: Type["GDFile"], contents: str) -> "GDFile": + """Parse the contents of a Godot file""" + return cls.from_parser( + scene_file.parse_with_tabs().parse_string(contents, parse_all=True) + ) + + @classmethod + def load(cls: Type["GDFile"], filepath: str) -> "GDFile": + with open(filepath, "r", encoding="utf-8") as ifile: + try: + file = cls.parse(ifile.read()) + except UnicodeDecodeError: + raise NotImplementedError( # pylint: disable=W0707 + "Error loading %s: godot_parser does not support binary scenes" + % filepath + ) + file.project_root = find_project_root(filepath) + return file + + @classmethod + def from_parser(cls: Type["GDFile"], parse_result) -> "GDFile": + first_section = parse_result[0] + if first_section.header.name == "gd_scene": + scene = GDPackedScene.__new__(GDPackedScene) + scene._sections = list(parse_result) + return scene + elif first_section.header.name == "gd_resource": + resource = GDResource.__new__(GDResource) + resource._sections = list(parse_result) + return resource + return cls(*parse_result) + + def write(self, filename: str, output_format: Optional[OutputFormat] = None): + """Writes this to a file""" + os.makedirs(os.path.dirname(filename), exist_ok=True) + with open(filename, "w", encoding="utf-8") as ofile: + ofile.write(self.output_to_string(output_format)) + + __keep_together_sections = [ + "ext_resource", + "connection", + "editable", + ] + + def _output_to_string(self, output_format: OutputFormat) -> str: + output = "" + + last_section = None + + for cur_section in self._sections: + if ( + last_section is not None + and cur_section.header.name == last_section.header.name + and cur_section.header.name in self.__keep_together_sections + ): + output = output[:-1] + + output += cur_section.output_to_string(output_format) + "\n\n" + + last_section = cur_section + + return output.rstrip() + "\n" + + def __repr__(self) -> str: + return "%s(%s)" % (type(self).__name__, self.__str__()) + + def __eq__(self, other) -> bool: + if not isinstance(other, GDFile): + return False + return self._sections == other._sections + + def __ne__(self, other) -> bool: + return not self.__eq__(other) + + +class GDCommonFile(GDFile): + """Base class with common application logic for all Godot file types""" + + def __init__(self, name: str, *sections: GDSection) -> None: + super().__init__(GDSection(GDFileHeader(name)), *sections) + + def output_to_string(self, output_format: Optional[OutputFormat] = None) -> str: + if output_format is None: + output_format = VersionOutputFormat.guess_version(self) + return super().output_to_string(output_format) + + def _output_to_string(self, output_format: OutputFormat) -> str: + self.generate_resource_ids(output_format) + + header = self._sections[0].header + if "load_steps" in header: + del header["load_steps"] + if "format" in header: + del header["format"] + + if output_format.load_steps: + header["load_steps"] = ( + 1 + len(self.get_ext_resources()) + len(self.get_sub_resources()) + ) + + if output_format.resource_ids_as_strings: + header["format"] = 3 + for obj in self._iter_resource_references(): + if ( + isinstance(obj, PackedVector4Array) + and output_format.packed_vector4_array_support + ): + header["format"] = 4 + if ( + isinstance(obj, PackedByteArray) + and output_format.packed_byte_array_base64_support + ): + header["format"] = 4 + if output_format._force_format_4_if_available and ( + output_format.packed_byte_array_base64_support + or output_format.packed_vector4_array_support + ): + header["format"] = 4 + else: + header["format"] = 2 + + ret = super()._output_to_string(output_format) + + return ret + + def add_section(self, new_section: GDSection) -> int: + idx = super().add_section(new_section) + return idx + + def remove_at(self, index: int): + self._sections.pop(index) + + def remove_unused_resources(self): + self._remove_unused_resources(self.get_ext_resources(), ExtResource) + self._remove_unused_resources(self.get_sub_resources(), SubResource) + + def _remove_unused_resources( + self, + sections: Sequence[Union[GDExtResourceSection, GDSubResourceSection]], + reference_type: Type[Union[ExtResource, SubResource]], + ) -> None: + done_removing = False + while not done_removing: + done_removing = True + seen = set() + for ref in self._iter_resource_references(): + if isinstance(ref, reference_type): + seen.add(ref.id) + if len(seen) < len(sections): + to_remove = [s for s in sections if s.id not in seen] + for s in to_remove: + if self.remove_section(s): + done_removing = False + + def _iter_resource_references( + self, + ) -> Iterator[Union[ExtResource, SubResource]]: + def iter_resources(value): + if isinstance(value, ExtResource): + yield value + elif isinstance(value, SubResource): + yield value + sub_resource = self.find_sub_resource(id=value.id) + if sub_resource is not None: + yield from iter_resources(sub_resource.properties) + elif isinstance(value, list): + for v in value: + yield from iter_resources(v) + elif isinstance(value, dict): + for k in value.keys(): + yield from iter_resources(k) + for v in value.values(): + yield from iter_resources(v) + elif isinstance(value, GDIterable): + yield value + yield from value._iter_objects() + elif isinstance(value, GDObject): + yield value + for v in value.args: + yield from iter_resources(v) + + for reference in self._iter_references(): + yield from iter_resources(reference) + + def _iter_references(self) -> Iterator[Any]: + for resource in self.get_sections("resource"): + yield resource.properties + + def generate_resource_ids(self, output_format: OutputFormat = OutputFormat()): + self._generate_resource_ids( + self.get_ext_resources(), ExtResource, output_format + ) + self._generate_resource_ids( + self.get_sub_resources(), SubResource, output_format + ) + + __extract_int_re = re.compile(r"^(\d+)") + + def __extract_int_id(self, id_: Union[int, str, None]) -> Optional[int]: + if isinstance(id_, int) or id_ is None: + return id_ + match = self.__extract_int_re.match(id_) + if match: + return int(match.group(0)) + return None + + def _generate_resource_ids( + self, + sections: Sequence[GDBaseResourceSection], + reference_type: Type[ResourceReference], + output_format: OutputFormat, + ): + if output_format.resource_ids_as_strings: + ids = [self.__extract_int_id(s.id) for s in sections] + ids.append(1) + next_id = max([id for id in ids if id is not None]) + for section in sections: + if isinstance(section.id, int): + for ref in self._iter_resource_references(): + if isinstance(ref, reference_type) and ref.id == section.id: + ref.resource = section + section.id = str(section.id) + elif section.id is None: + section.id = "%s_%s" % ( + reference_type.get_id_key(next_id), + output_format.generate_id(section), + ) + next_id += 1 + else: + ids = [s.id for s in sections if isinstance(s.id, int)] + ids.append(1) + next_id = max([id for id in ids if id is not None]) + for section in sections: + if not isinstance(section.id, int): + if isinstance(section.id, str): + for ref in self._iter_resource_references(): + if isinstance(ref, reference_type) and ref.id == section.id: + ref.resource = section + section.id = next_id + next_id += 1 + + def renumber_resource_ids(self): + """Refactor all resource IDs to be sequential with no gaps""" + self._renumber_resource_ids(self.get_ext_resources(), ExtResource) + self._renumber_resource_ids(self.get_sub_resources(), SubResource) + + def _renumber_resource_ids( + self, + sections: Sequence[Union[GDExtResourceSection, GDSubResourceSection]], + reference_type: Type[Union[ExtResource, SubResource]], + ) -> None: + id_map = {} + # First we renumber all the resource IDs so there are no gaps + for i, section in enumerate([s for s in sections if isinstance(s.id, int)]): + id_map[section.id] = i + 1 + section.id = i + 1 + + # Now we update all references to use the new number + for ref in self._iter_resource_references(): + if isinstance(ref, reference_type): + if ref.id in id_map: + ref.id = id_map[ref.id] + + +class GDResource(GDCommonFile): + def __init__( + self, type: Optional[str] = None, *sections: GDSection, **attributes + ) -> None: + super().__init__("gd_resource", *sections) + + if type is not None: + self._sections[0].header["type"] = type + + self.add_section(GDResourceSection(**attributes)) + + @property + def resource_section(self): + return self.find_section("resource") + + def __contains__(self, k: str) -> bool: + return k in self.resource_section + + def __getitem__(self, k: str) -> Any: + return self.resource_section[k] + + def __setitem__(self, k: str, v: Any) -> None: + self.resource_section[k] = v + + def __delitem__(self, k: str) -> None: + del self.resource_section[k] + + def _iter_references(self) -> Iterator[Any]: + yield from super()._iter_references() + yield self.resource_section.properties + + +class GDPackedScene(GDCommonFile): + def __init__(self, *sections: GDSection) -> None: + super().__init__("gd_scene", *sections) + + def get_nodes(self) -> List[GDNodeSection]: + """Get all [node] sections""" + return cast(List[GDNodeSection], self.get_sections("node")) + + def find_node( + self, property_constraints: Optional[dict] = None, **constraints + ) -> Optional[GDNodeSection]: + """Find first [node] section that matches (see find_section)""" + return cast( + GDNodeSection, + self.find_section("node", property_constraints, **constraints), + ) + def add_node( self, name: str, @@ -245,7 +544,7 @@ def get_parent_scene(self) -> Optional[str]: return None return parent_res.path - def load_parent_scene(self) -> "GDScene": + def load_parent_scene(self) -> "GDPackedScene": if self.project_root is None: raise RuntimeError( "load_parent_scene() requires a project_root on the GDFile" @@ -256,9 +555,12 @@ def load_parent_scene(self) -> "GDScene": parent_res = self.find_ext_resource(id=root.instance) if parent_res is None: raise RuntimeError( - "Could not find parent scene resource id(%d)" % root.instance + "Could not find parent scene resource id(%s)" % root.instance ) - return GDScene.load(gdpath_to_filepath(self.project_root, parent_res.path)) + return cast( + GDPackedScene, + GDPackedScene.load(gdpath_to_filepath(self.project_root, parent_res.path)), + ) @contextmanager def use_tree(self): @@ -300,160 +602,8 @@ def get_node(self, path: str = ".") -> Optional[GDNodeSection]: node = tree.root.get_node(path) return node.section if node is not None else None - @classmethod - def parse(cls: Type[GDFileType], contents: str) -> GDFileType: - """Parse the contents of a Godot file""" - return cls.from_parser( - scene_file.parse_with_tabs().parse_string(contents, parse_all=True) - ) - - @classmethod - def load(cls: Type[GDFileType], filepath: str) -> GDFileType: - with open(filepath, "r", encoding="utf-8") as ifile: - try: - file = cls.parse(ifile.read()) - except UnicodeDecodeError: - raise NotImplementedError( # pylint: disable=W0707 - "Error loading %s: godot_parser does not support binary scenes" - % filepath - ) - file.project_root = find_project_root(filepath) - return file - - @classmethod - def from_parser(cls: Type[GDFileType], parse_result): - first_section = parse_result[0] - if first_section.header.name == "gd_scene": - scene = GDScene.__new__(GDScene) - scene._sections = list(parse_result) - return scene - elif first_section.header.name == "gd_resource": - resource = GDResource.__new__(GDResource) - resource._sections = list(parse_result) - return resource - return cls(*parse_result) - - def write(self, filename: str): - """Writes this to a file""" - os.makedirs(os.path.dirname(filename), exist_ok=True) - with open(filename, "w", encoding="utf-8") as ofile: - ofile.write(str(self)) - - def __str__(self) -> str: - return "\n\n".join([str(s) for s in self._sections]) + "\n" - - def __repr__(self) -> str: - return "%s(%s)" % (type(self).__name__, self.__str__()) - - def __eq__(self, other) -> bool: - if not isinstance(other, GDFile): - return False - return self._sections == other._sections - - def __ne__(self, other) -> bool: - return not self.__eq__(other) - - -class GDCommonFile(GDFile): - """Base class with common application logic for all Godot file types""" - - def __init__(self, name: str, *sections: GDSection) -> None: - super().__init__( - GDSection(GDSectionHeader(name, load_steps=1, format=2)), *sections - ) - self.load_steps = ( - 1 + len(self.get_ext_resources()) + len(self.get_sub_resources()) - ) - - @property - def load_steps(self) -> int: - return self._sections[0].header["load_steps"] - - @load_steps.setter - def load_steps(self, steps: int): - self._sections[0].header["load_steps"] = steps - - def add_section(self, new_section: GDSection) -> int: - idx = super().add_section(new_section) - if new_section.header.name in ["ext_resource", "sub_resource"]: - self.load_steps += 1 - return idx - - def remove_at(self, index: int): - section = self._sections.pop(index) - if section.header.name in ["ext_resource", "sub_resource"]: - self.load_steps -= 1 - - def remove_unused_resources(self): - self._remove_unused_resources(self.get_ext_resources(), ExtResource) - self._remove_unused_resources(self.get_sub_resources(), SubResource) - - def _remove_unused_resources( - self, - sections: Sequence[Union[GDExtResourceSection, GDSubResourceSection]], - reference_type: Type[Union[ExtResource, SubResource]], - ) -> None: - seen = set() - for ref in self._iter_node_resource_references(): - if isinstance(ref, reference_type): - seen.add(ref.id) - if len(seen) < len(sections): - to_remove = [s for s in sections if s.id not in seen] - for s in to_remove: - self.remove_section(s) - - def renumber_resource_ids(self): - """Refactor all resource IDs to be sequential with no gaps""" - self._renumber_resource_ids(self.get_ext_resources(), ExtResource) - self._renumber_resource_ids(self.get_sub_resources(), SubResource) - - def _iter_node_resource_references( - self, - ) -> Iterator[Union[ExtResource, SubResource]]: - def iter_resources(value): - if isinstance(value, (ExtResource, SubResource)): - yield value - elif isinstance(value, list): - for v in value: - yield from iter_resources(v) - elif isinstance(value, dict): - for v in value.values(): - yield from iter_resources(v) - elif isinstance(value, GDObject): - for v in value.args: - yield from iter_resources(v) - + def _iter_references(self) -> Iterator[Any]: + yield from super()._iter_references() for node in self.get_nodes(): - yield from iter_resources(node.header.attributes) - yield from iter_resources(node.properties) - for resource in self.get_sections("resource"): - yield from iter_resources(resource.properties) - - def _renumber_resource_ids( - self, - sections: Sequence[Union[GDExtResourceSection, GDSubResourceSection]], - reference_type: Type[Union[ExtResource, SubResource]], - ) -> None: - id_map = {} - # First we renumber all the resource IDs so there are no gaps - for i, section in enumerate(sections): - id_map[section.id] = i + 1 - section.id = i + 1 - - # Now we update all references to use the new number - for ref in self._iter_node_resource_references(): - if isinstance(ref, reference_type): - try: - ref.id = id_map[ref.id] - except KeyError as e: - raise GodotFileException("Unknown resource ID %d" % ref.id) from e - - -class GDScene(GDCommonFile): - def __init__(self, *sections: GDSection) -> None: - super().__init__("gd_scene", *sections) - - -class GDResource(GDCommonFile): - def __init__(self, *sections: GDSection) -> None: - super().__init__("gd_resource", *sections) + yield node.header.attributes + yield node.properties diff --git a/godot_parser/id_generator.py b/godot_parser/id_generator.py new file mode 100644 index 0000000..7fed62c --- /dev/null +++ b/godot_parser/id_generator.py @@ -0,0 +1,26 @@ +from random import choice +from string import ascii_lowercase, digits +from typing import Any + + +class BaseGenerator(object): + def generate(self, section: Any): + return "" + + +class RandomIdGenerator(BaseGenerator): + def __init__(self, length: int = 5, pool: str = ascii_lowercase + digits): + self.length = length + self.pool = pool + + def generate(self, section: Any): + return "".join((choice(self.pool) for _ in range(self.length))) + + +class SequentialHexGenerator(BaseGenerator): + def __init__(self): + self.counter = 0 + + def generate(self, section: Any): + self.counter += 1 + return "%x" % (self.counter) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index 0170cb6..94e3b06 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -1,9 +1,14 @@ """Wrappers for Godot's non-primitive object types""" +import base64 +import json +import re from functools import partial -from typing import Type, TypeVar +from math import floor +from typing import Any, Iterable, List, Optional, Type, TypeVar, Union -from .util import stringify_object +from .output import Outputable, OutputFormat +from .util import Identifiable, stringify_object __all__ = [ "GDObject", @@ -11,11 +16,15 @@ "Vector3", "Color", "NodePath", + "ResourceReference", "ExtResource", "SubResource", "StringName", "TypedArray", "TypedDictionary", + "PackedByteArray", + "PackedVector4Array", + "GDIterable", ] GD_OBJECT_REGISTRY = {} @@ -38,7 +47,7 @@ def __new__(cls, name, bases, dct): GDObjectType = TypeVar("GDObjectType", bound="GDObject") -class GDObject(metaclass=GDObjectMeta): +class GDObject(Outputable, metaclass=GDObjectMeta): """ Base class for all GD Object types @@ -49,7 +58,19 @@ class GDObject(metaclass=GDObjectMeta): def __init__(self, name, *args) -> None: self.name = name - self.args = list(args) + self.args: List[Any] = list(args) + + def __contains__(self, idx: int) -> bool: + return idx in self.args + + def __getitem__(self, idx: int) -> float: + return self.args[idx] + + def __setitem__(self, idx: int, value: float) -> None: + self.args[idx] = value + + def __delitem__(self, idx: int) -> None: + del self.args[idx] @classmethod def from_parser(cls: Type[GDObjectType], parse_result) -> GDObjectType: @@ -57,10 +78,17 @@ def from_parser(cls: Type[GDObjectType], parse_result) -> GDObjectType: factory = GD_OBJECT_REGISTRY.get(name, partial(GDObject, name)) return factory(*parse_result[1:]) - def __str__(self) -> str: - return "%s(%s)" % ( - self.name, - ", ".join([stringify_object(v) for v in self.args]), + __packed_array_re = re.compile(r"^(Packed|Pool)(?P[A-Z]\w+)Array$") + + def _output_to_string(self, output_format: OutputFormat) -> str: + name = self.name + + match = self.__packed_array_re.match(name) + if match: + name = output_format.packed_array_format % match.group("InnerType") + + return name + output_format.surround_parentheses( + ", ".join([stringify_object(v, output_format) for v in self.args]) ) def __repr__(self) -> str: @@ -82,12 +110,6 @@ class Vector2(GDObject): def __init__(self, x: float, y: float) -> None: super().__init__("Vector2", x, y) - def __getitem__(self, idx) -> float: - return self.args[idx] - - def __setitem__(self, idx: int, value: float): - self.args[idx] = value - @property def x(self) -> float: """Getter for x""" @@ -113,11 +135,40 @@ class Vector3(GDObject): def __init__(self, x: float, y: float, z: float) -> None: super().__init__("Vector3", x, y, z) - def __getitem__(self, idx: int) -> float: - return self.args[idx] + @property + def x(self) -> float: + """Getter for x""" + return self.args[0] - def __setitem__(self, idx: int, value: float) -> None: - self.args[idx] = value + @x.setter + def x(self, x: float) -> None: + """Setter for x""" + self.args[0] = x + + @property + def y(self) -> float: + """Getter for y""" + return self.args[1] + + @y.setter + def y(self, y: float) -> None: + """Setter for y""" + self.args[1] = y + + @property + def z(self) -> float: + """Getter for z""" + return self.args[2] + + @z.setter + def z(self, z: float) -> None: + """Setter for z""" + self.args[2] = z + + +class Vector4(GDObject): + def __init__(self, x: float, y: float, z: float, w: float) -> None: + super().__init__("Vector4", x, y, z, w) @property def x(self) -> float: @@ -149,6 +200,16 @@ def z(self, z: float) -> None: """Setter for z""" self.args[2] = z + @property + def w(self) -> float: + """Getter for w""" + return self.args[3] + + @w.setter + def w(self, w: float) -> None: + """Setter for w""" + self.args[3] = w + class Color(GDObject): def __init__(self, r: float, g: float, b: float, a: float) -> None: @@ -158,12 +219,6 @@ def __init__(self, r: float, g: float, b: float, a: float) -> None: assert 0 <= a <= 1 super().__init__("Color", r, g, b, a) - def __getitem__(self, idx: int) -> float: - return self.args[idx] - - def __setitem__(self, idx: int, value: float) -> None: - self.args[idx] = value - @property def r(self) -> float: """Getter for r""" @@ -205,6 +260,74 @@ def a(self, a: float) -> None: self.args[3] = a +class PackedVector4Array(GDObject): + def __init__(self, *args) -> None: + super().__init__("PackedVector4Array", *args) + + @classmethod + def FromList(cls, list_: List[Vector4]) -> "PackedVector4Array": + return cls(*sum([[v.x, v.y, v.z, v.w] for v in list_], [])) + + def get_vector4(self, idx: int) -> Vector4: + return Vector4( + self.args[idx * 4 + 0], + self.args[idx * 4 + 1], + self.args[idx * 4 + 2], + self.args[idx * 4 + 3], + ) + + def set_vector4(self, idx: int, value: Vector4) -> None: + self.args[idx * 4 + 0] = value.x + self.args[idx * 4 + 1] = value.y + self.args[idx * 4 + 2] = value.z + self.args[idx * 4 + 3] = value.w + + def remove_vector4_at(self, idx: int) -> None: + del self.args[idx * 4] + del self.args[idx * 4] + del self.args[idx * 4] + del self.args[idx * 4] + + def _output_to_string(self, output_format: OutputFormat) -> str: + if output_format.packed_vector4_array_support: + return super()._output_to_string(output_format) + else: + return TypedArray( + "Vector4", + [self.get_vector4(i) for i in range(floor(len(self.args) / 4))], + ).output_to_string(output_format) + + +class PackedByteArray(GDObject): + def __init__(self, *args) -> None: + super().__init__("PackedByteArray", *args) + + @classmethod + def FromBytes(cls, bytes_: bytes) -> "PackedByteArray": + return cls(*list(bytes_)) + + def _stored_as_base64(self) -> bool: + return len(self.args) == 1 and isinstance(self.args[0], str) + + @property + def bytes_(self) -> bytes: + if self._stored_as_base64(): + return base64.b64decode(self.args[0]) + return bytes(self.args) + + @bytes_.setter + def bytes_(self, bytes_: bytes) -> None: + self.args = list(bytes_) + + def _output_to_string(self, output_format: OutputFormat) -> str: + if output_format.packed_byte_array_base64_support: + if not self._stored_as_base64(): + self.args = [base64.b64encode(self.bytes_).decode("utf-8")] + elif self._stored_as_base64(): + self.bytes_ = self.bytes_ + return super()._output_to_string(output_format) + + class NodePath(GDObject): def __init__(self, path: str) -> None: super().__init__("NodePath", path) @@ -219,45 +342,72 @@ def path(self, path: str) -> None: """Setter for path""" self.args[0] = path - def __str__(self) -> str: - return '%s("%s")' % (self.name, self.path) + def _output_to_string(self, output_format: OutputFormat) -> str: + original_punctuation_spaces = output_format.punctuation_spaces + output_format.punctuation_spaces = False + ret = super()._output_to_string(output_format) + output_format.punctuation_spaces = original_punctuation_spaces + return ret -class ExtResource(GDObject): - def __init__(self, id: int) -> None: - super().__init__("ExtResource", id) +class ResourceReference(GDObject): + def __init__(self, name: str, resource: Union[int, str, Identifiable]): + self.resource = resource + if isinstance(resource, Identifiable): + super().__init__(name) + else: + super().__init__(name, resource) @property - def id(self) -> int: + def id(self) -> Optional[Union[int, str]]: """Getter for id""" - return self.args[0] + if isinstance(self.resource, Identifiable): + return self.resource.get_id() + else: + return self.resource @id.setter - def id(self, id: int) -> None: + def id(self, id: Union[int, str]) -> None: """Setter for id""" - self.args[0] = id + self.resource = id + self.args = [id] + @classmethod + def get_id_key(cls, index: Optional[int] = None) -> str: + return "Resource" -class SubResource(GDObject): - def __init__(self, id: int) -> None: - super().__init__("SubResource", id) + def _output_to_string(self, output_format: OutputFormat) -> str: + if isinstance(self.resource, Identifiable): + id = self.resource.get_id() + if id is not None: + self.id = id + return super()._output_to_string(output_format) - @property - def id(self) -> int: - """Getter for id""" - return self.args[0] - @id.setter - def id(self, id: int) -> None: - """Setter for id""" - self.args[0] = id +class ExtResource(ResourceReference): + def __init__(self, resource: Union[int, str, Identifiable]) -> None: + super().__init__("ExtResource", resource) + + @classmethod + def get_id_key(cls, index: Optional[int] = None) -> str: + return str(index) + + +class SubResource(ResourceReference): + def __init__(self, resource: Union[int, str, Identifiable]) -> None: + super().__init__("SubResource", resource) -class TypedArray: +class GDIterable: + def _iter_objects(self) -> Iterable[Any]: + return iter([]) + + +class TypedArray(GDIterable, Outputable): def __init__(self, type, list_) -> None: self.name = "Array" self.type = type - self.list_ = list_ + self.list_: list = list_ @classmethod def WithCustomName(cls: Type["TypedArray"], name, type, list_) -> "TypedArray": @@ -269,8 +419,21 @@ def WithCustomName(cls: Type["TypedArray"], name, type, list_) -> "TypedArray": def from_parser(cls: Type["TypedArray"], parse_result) -> "TypedArray": return TypedArray.WithCustomName(*parse_result) - def __str__(self) -> str: - return "%s[%s](%s)" % (self.name, self.type, stringify_object(self.list_)) + def _output_to_string(self, output_format: OutputFormat) -> str: + if output_format.typed_array_support: + return ( + self.name + + output_format.surround_brackets( + self.type.output_to_string(output_format) + if isinstance(self.type, Outputable) + else self.type + ) + + output_format.surround_parentheses( + stringify_object(self.list_, output_format) + ) + ) + else: + return stringify_object(self.list_, output_format) def __repr__(self) -> str: return self.__str__() @@ -290,13 +453,17 @@ def __ne__(self, other) -> bool: def __hash__(self): return hash(frozenset((self.name, self.type, self.list_))) + def _iter_objects(self) -> Iterable[Any]: + yield self.type + yield from self.list_ + -class TypedDictionary: +class TypedDictionary(GDIterable, Outputable): def __init__(self, key_type, value_type, dict_) -> None: self.name = "Dictionary" self.key_type = key_type self.value_type = value_type - self.dict_ = dict_ + self.dict_: dict = dict_ @classmethod def WithCustomName( @@ -310,13 +477,31 @@ def WithCustomName( def from_parser(cls: Type["TypedDictionary"], parse_result) -> "TypedDictionary": return TypedDictionary.WithCustomName(*parse_result) - def __str__(self) -> str: - return "%s[%s, %s](%s)" % ( - self.name, - self.key_type, - self.value_type, - stringify_object(self.dict_), - ) + def _output_to_string(self, output_format: OutputFormat) -> str: + if output_format.typed_dictionary_support: + return ( + self.name + + output_format.surround_brackets( + "%s, %s" + % ( + ( + self.key_type.output_to_string(output_format) + if isinstance(self.key_type, Outputable) + else self.key_type + ), + ( + self.value_type.output_to_string(output_format) + if isinstance(self.value_type, Outputable) + else self.value_type + ), + ) + ) + + output_format.surround_parentheses( + stringify_object(self.dict_, output_format) + ) + ) + else: + return stringify_object(self.dict_, output_format) def __repr__(self) -> str: return self.__str__() @@ -337,8 +522,14 @@ def __ne__(self, other) -> bool: def __hash__(self): return hash(frozenset((self.name, self.key_type, self.value_type, self.dict_))) + def _iter_objects(self) -> Iterable[Any]: + yield self.key_type + yield self.value_type + yield from self.dict_.keys() + yield from self.dict_.values() + -class StringName: +class StringName(Outputable): def __init__(self, str) -> None: self.str = str @@ -346,8 +537,11 @@ def __init__(self, str) -> None: def from_parser(cls: Type["StringName"], parse_result) -> "StringName": return StringName(parse_result[0]) - def __str__(self) -> str: - return "&" + stringify_object(self.str) + def _output_to_string(self, output_format: OutputFormat) -> str: + if output_format.string_name_support: + return "&" + json.dumps(self.str, ensure_ascii=False).replace("'", "\\'") + else: + return stringify_object(self.str, output_format) def __repr__(self) -> str: return self.__str__() diff --git a/godot_parser/output.py b/godot_parser/output.py new file mode 100644 index 0000000..d542ea2 --- /dev/null +++ b/godot_parser/output.py @@ -0,0 +1,182 @@ +from typing import Any, Optional, Tuple, Union + +from packaging.version import Version + +from .id_generator import RandomIdGenerator + + +class OutputFormat(object): + """Controls how Godot files are generated + + Attributes: + punctuation_spaces: + Determines if spaces should be added between puncuation. + ex: Vector2( 1, 2 ) vs Vector2(1, 2); + single_line_on_empty_dict: + Determines if empty dicts should be written on a single line or not. + ex: "{}" vs "{\\n}"; + resource_ids_as_strings: + Determines if SubResources and ExtResources should be identified with string over int ids. + ex: ExtResource(1) vs ExtResource("1_n2jr1"); + ex: SubResource(1) vs SubResource("Resource_b1ij2"); + typed_array_support: + Determines if TypedArrays are supported. + If unsupported, will be output as regular arrays. + ex: Array[int]([1]) vs [1]; + packed_byte_array_base64_support: + Determines if PackedByteArrays should be output as base64. + ex: PackedByteArray("1qr3") vs PackedByteArray(1, 2, 3); + packed_vector4_array_support: + Determines if PackedVector4Arrays are supported. + If unsupported, will be output as a Typed array. May fallback to untyped array. + ex: PackedVector4Array(1, 2, 3, 4) vs Array[Vector4]([Vector4(1, 2, 3, 4)]); + typed_dictionary_support: + Determines if TypedDictionaries are supported. + If unsupported, will be output as regular dict. + ex: Dictionary[int,String]({\\n1: "One"\\n}) vs {\\n1: "One"\\n}; + string_name_support: + Determines if StringNames are supported. + If unsupported, will be output as a regular string. + ex: &"foo" vs "foo"; + load_steps: + Determines if load_steps will be output on GDFile header. + packed_array_format: + Determines type to be used on Packed arrays. + ex: Packed%sArray for Godot 4.x; + ex: Pool%sArray for Godot 3.x; + """ + + def __init__( + self, + punctuation_spaces: bool = False, + single_line_on_empty_dict: bool = True, + resource_ids_as_strings: bool = True, + typed_array_support: bool = True, + packed_byte_array_base64_support: bool = True, + packed_vector4_array_support: bool = True, + typed_dictionary_support: bool = True, + string_name_support=True, + load_steps: bool = False, + packed_array_format="Packed%sArray", + ): + self.punctuation_spaces = punctuation_spaces + self.single_line_on_empty_dict = single_line_on_empty_dict + self.resource_ids_as_strings = resource_ids_as_strings + self.typed_array_support = typed_array_support + self.packed_byte_array_base64_support = packed_byte_array_base64_support + self.packed_vector4_array_support = packed_vector4_array_support + self.typed_dictionary_support = typed_dictionary_support + self.string_name_support = string_name_support + self.load_steps = load_steps + self.packed_array_format = packed_array_format + + self._force_format_4_if_available = False + self._id_generator = RandomIdGenerator() + + def surround_string( + self, punctuation: Union[str, Tuple[str, str]], content: str + ) -> str: + if isinstance(punctuation, str): + right = left = punctuation + else: + left = punctuation[0] + right = punctuation[1] + + if self.punctuation_spaces: + left += " " + right = " " + right + + return left + content + right + + def surround_parentheses(self, content: str) -> str: + return self.surround_string(("(", ")"), content) + + def surround_brackets(self, content: str) -> str: + return self.surround_string(("[", "]"), content) + + def generate_id(self, section: Any) -> str: + return self._id_generator.generate(section) + + +class VersionOutputFormat(OutputFormat): + __V36 = Version("3.6") + __V40 = Version("4.0") + __V43 = Version("4.3") + __V44 = Version("4.4") + __V46 = Version("4.6") + + def __init__(self, version: Union[str, Version]): + if not isinstance(version, Version): + version = Version(version) + + self.version = version + + super().__init__( + punctuation_spaces=version < self.__V40, + single_line_on_empty_dict=version >= self.__V40, + resource_ids_as_strings=version >= self.__V40, + typed_array_support=version >= self.__V40, + packed_byte_array_base64_support=version >= self.__V43, + packed_vector4_array_support=version >= self.__V43, + typed_dictionary_support=version >= self.__V44, + string_name_support=version >= self.__V40, + load_steps=version < self.__V46, + packed_array_format=( + "Pool%sArray" if version < self.__V40 else "Packed%sArray" + ), + ) + + @classmethod + def guess_version(cls, gd_common_file) -> "VersionOutputFormat": + from .objects import PackedByteArray, PackedVector4Array, TypedDictionary + + format = None + + if "format" in gd_common_file._sections[0].header: + format = gd_common_file._sections[0].header["format"] + + version = cls.__V40 + + if format == 2: + version = cls.__V36 + return cls(version) + if not "load_steps" in gd_common_file._sections[0].header: + version = cls.__V46 + + force_format_4 = False + + if format == 4: + version = max(version, cls.__V43) + force_format_4 = True + + for reference in gd_common_file._iter_resource_references(): + if isinstance(reference, TypedDictionary): + version = max(version, cls.__V44) + elif isinstance(reference, PackedVector4Array): + version = max(version, cls.__V43) + elif ( + isinstance(reference, PackedByteArray) and reference._stored_as_base64() + ): + version = max(version, cls.__V43) + + output_format = cls(version) + + if force_format_4: + output_format._force_format_4_if_available = True + + return output_format + + +class Outputable(object): + def _output_to_string(self, output_format: OutputFormat) -> str: + raise NotImplementedError( + "output_to_string method not defined in %s" % type(self) + ) + + def output_to_string(self, output_format: Optional[OutputFormat] = None) -> str: + if output_format is None: + output_format = OutputFormat() + return self._output_to_string(output_format) + + def __str__(self) -> str: + return self.output_to_string() diff --git a/godot_parser/sections.py b/godot_parser/sections.py index 5c6f152..39573af 100644 --- a/godot_parser/sections.py +++ b/godot_parser/sections.py @@ -1,23 +1,27 @@ import re from collections import OrderedDict -from typing import Any, List, Optional, Type, TypeVar +from typing import Any, List, Optional, Type, TypeVar, Union -from .objects import ExtResource, SubResource -from .util import stringify_object +from .objects import ExtResource, StringName, SubResource +from .output import Outputable, OutputFormat +from .util import Identifiable, stringify_object __all__ = [ "GDSectionHeader", "GDSection", "GDNodeSection", + "GDBaseResourceSection", "GDExtResourceSection", "GDSubResourceSection", "GDResourceSection", + "GDFileHeader", ] + GD_SECTION_REGISTRY = {} -class GDSectionHeader(object): +class GDSectionHeader(Outputable): """ Represents the header for a section @@ -32,6 +36,9 @@ def __init__(self, _name: str, **kwargs) -> None: for k, v in kwargs.items(): self.attributes[k] = v + def __contains__(self, k: str) -> bool: + return k in self.attributes + def __getitem__(self, k: str) -> Any: return self.attributes[k] @@ -39,26 +46,50 @@ def __setitem__(self, k: str, v: Any) -> None: self.attributes[k] = v def __delitem__(self, k: str): - try: - del self.attributes[k] - except KeyError: - pass + del self.attributes[k] def get(self, k: str, default: Any = None) -> Any: return self.attributes.get(k, default) @classmethod def from_parser(cls: Type["GDSectionHeader"], parse_result) -> "GDSectionHeader": - header = cls(parse_result[0]) + factory = cls + + if parse_result[0] in ["gd_resource", "gd_scene"]: + factory = GDFileHeader + + header = factory(parse_result[0]) for attribute in parse_result[1:]: header.attributes[attribute[0]] = attribute[1] return header - def __str__(self) -> str: + def _get_key_priority(self, key: str) -> int: + return 0 + + def _output_to_string(self, output_format: OutputFormat) -> str: attribute_str = "" if self.attributes: + keys = sorted( + self.attributes.keys(), key=self._get_key_priority, reverse=True + ) + attribute_str = " " + " ".join( - ["%s=%s" % (k, stringify_object(v)) for k, v in self.attributes.items()] + [ + "%s=%s" + % ( + k, + ( + # Bizarre but consistent edge case as far as I could tell. + # Would be more than happy to just live with it though + " " + if isinstance(self.attributes[k], list) + and isinstance(self.attributes[k][0], StringName) + else "" + ) + + stringify_object(self.attributes[k], output_format), + ) + for k in keys + ] ) return "[" + self.name + attribute_str + "]" @@ -74,6 +105,13 @@ def __ne__(self, other: Any) -> bool: return not self.__eq__(other) +class GDFileHeader(GDSectionHeader): + def _get_key_priority(self, key: str) -> int: + if key == "uid": + return -10 + return super()._get_key_priority(key) + + class GDSectionMeta(type): """Still trying to be too clever""" @@ -88,7 +126,7 @@ def __new__(cls, name, bases, dct): GDSectionType = TypeVar("GDSectionType", bound="GDSection") -class GDSection(metaclass=GDSectionMeta): +class GDSection(Outputable, metaclass=GDSectionMeta): """ Represents a full section of a GD file @@ -105,6 +143,9 @@ def __init__(self, header: GDSectionHeader, **kwargs) -> None: for k, v in kwargs.items(): self.properties[k] = v + def __contains__(self, k: str) -> bool: + return k in self.properties + def __getitem__(self, k: str) -> Any: return self.properties[k] @@ -112,10 +153,7 @@ def __setitem__(self, k: str, v: Any) -> None: self.properties[k] = v def __delitem__(self, k: str) -> None: - try: - del self.properties[k] - except KeyError: - pass + del self.properties[k] def get(self, k: str, default: Any = None) -> Any: return self.properties.get(k, default) @@ -131,12 +169,16 @@ def from_parser(cls: Type[GDSectionType], parse_result) -> GDSectionType: section[k] = v return section - def __str__(self) -> str: - ret = str(self.header) + def _output_to_string(self, output_format: OutputFormat) -> str: + ret = self.header.output_to_string(output_format) if self.properties: ret += "\n" + "\n".join( [ - "%s = %s" % ('"' + k + '"' if " " in k else k, stringify_object(v)) + "%s = %s" + % ( + '"' + k + '"' if " " in k else k, + stringify_object(v, output_format), + ) for k, v in self.properties.items() ] ) @@ -154,20 +196,7 @@ def __ne__(self, other: Any) -> bool: return not self.__eq__(other) -class GDExtResourceSection(GDSection): - """Section representing an [ext_resource]""" - - def __init__(self, path: str, type: str, id: int): - super().__init__(GDSectionHeader("ext_resource", path=path, type=type, id=id)) - - @property - def path(self) -> str: - return self.header["path"] - - @path.setter - def path(self, path: str) -> None: - self.header["path"] = path - +class GDBaseResourceSection(GDSection, Identifiable): @property def type(self) -> str: return self.header["type"] @@ -177,43 +206,47 @@ def type(self, type: str) -> None: self.header["type"] = type @property - def id(self) -> int: - return self.header["id"] + def id(self) -> Optional[Union[int, str]]: + if "id" in self.header: + return self.header["id"] + return None @id.setter - def id(self, id: int) -> None: + def id(self, id: Union[int, str]) -> None: self.header["id"] = id - @property - def reference(self) -> ExtResource: - return ExtResource(self.id) + def get_id(self) -> Optional[Union[int, str]]: + return self.id -class GDSubResourceSection(GDSection): - """Section representing a [sub_resource]""" +class GDExtResourceSection(GDBaseResourceSection): + """Section representing an [ext_resource]""" - def __init__(self, type: str, id: int, **kwargs): - super().__init__(GDSectionHeader("sub_resource", type=type, id=id), **kwargs) + def __init__(self, path: str, type_: str, id_: Optional[Union[int, str]] = None): + super().__init__(GDSectionHeader("ext_resource", path=path, type=type_, id=id_)) @property - def type(self) -> str: - return self.header["type"] + def path(self) -> str: + return self.header["path"] - @type.setter - def type(self, type: str) -> None: - self.header["type"] = type + @path.setter + def path(self, path: str) -> None: + self.header["path"] = path @property - def id(self) -> int: - return self.header["id"] + def reference(self) -> ExtResource: + return ExtResource(self) - @id.setter - def id(self, id: int) -> None: - self.header["id"] = id + +class GDSubResourceSection(GDBaseResourceSection): + """Section representing a [sub_resource]""" + + def __init__(self, type_: str, id_: Optional[Union[int, str]] = None, **kwargs): + super().__init__(GDSectionHeader("sub_resource", type=type_, id=id_), **kwargs) @property def reference(self) -> SubResource: - return SubResource(self.id) + return SubResource(self) class GDNodeSection(GDSection): @@ -269,7 +302,8 @@ def type(self) -> Optional[str]: @type.setter def type(self, type: Optional[str]) -> None: if type is None: - del self.header["type"] + if "type" in self.header: + del self.header["type"] else: self.header["type"] = type self.instance = None @@ -281,7 +315,8 @@ def parent(self) -> Optional[str]: @parent.setter def parent(self, parent: Optional[str]) -> None: if parent is None: - del self.header["parent"] + if "parent" in self.header: + del self.header["parent"] else: self.header["parent"] = parent @@ -295,7 +330,8 @@ def instance(self) -> Optional[int]: @instance.setter def instance(self, instance: Optional[int]) -> None: if instance is None: - del self.header["instance"] + if "instance" in self.header: + del self.header["instance"] else: self.header["instance"] = ExtResource(instance) self.type = None @@ -310,7 +346,8 @@ def index(self) -> Optional[int]: @index.setter def index(self, index: Optional[int]) -> None: if index is None: - del self.header["index"] + if "index" in self.header: + del self.header["index"] else: self.header["index"] = str(index) @@ -321,7 +358,8 @@ def groups(self) -> Optional[List[str]]: @groups.setter def groups(self, groups: Optional[List[str]]) -> None: if groups is None: - del self.header["groups"] + if "groups" in self.header: + del self.header["groups"] else: self.header["groups"] = groups diff --git a/godot_parser/structure.py b/godot_parser/structure.py index 2281850..b77b49e 100644 --- a/godot_parser/structure.py +++ b/godot_parser/structure.py @@ -15,7 +15,7 @@ from .sections import GDSection, GDSectionHeader from .values import value -key = QuotedString('"', escChar="\\", multiline=False).set_name("key") | Word( +key = QuotedString('"', esc_char="\\", multiline=False).set_name("key") | Word( alphanums + "_/:" ).set_name("key") var = Word(alphanums + "_").set_name("variable") diff --git a/godot_parser/tree.py b/godot_parser/tree.py index 9ef48c8..e624632 100644 --- a/godot_parser/tree.py +++ b/godot_parser/tree.py @@ -3,11 +3,10 @@ from collections import OrderedDict from typing import Any, List, Optional, Union -from .files import GDFile +from .files import GDPackedScene from .sections import GDNodeSection __all__ = ["Node", "TreeMutationException"] -SENTINEL = object() class TreeMutationException(Exception): @@ -106,35 +105,38 @@ def instance(self, new_instance: Optional[int]) -> None: self._type = None self._instance = new_instance + def __contains__(self, k: str) -> bool: + if k in self.properties: + return True + if self._inherited_node != None: + return k in self._inherited_node + return False + def __getitem__(self, k: str) -> Any: - v = self.properties.get(k, SENTINEL) - if v is SENTINEL: - if self._inherited_node is not None: - return self._inherited_node[k] - raise KeyError("No property %s found on node %s" % (k, self.name)) - return v + if k in self.properties: + return self.properties[k] + if self._inherited_node is not None: + return self._inherited_node[k] + raise KeyError("No property %s found on node %s" % (k, self.name)) def __setitem__(self, k: str, v: Any) -> None: - if self._inherited_node is not None and v == self._inherited_node.get( - k, SENTINEL + if ( + self._inherited_node is not None + and k in self._inherited_node + and v == self._inherited_node[k] ): del self[k] else: self.properties[k] = v def __delitem__(self, k: str) -> None: - try: - del self.properties[k] - except KeyError: - pass + del self.properties[k] def get(self, k: str, default: Any = None) -> Any: - v = self.properties.get(k, SENTINEL) - if v is SENTINEL: - if self._inherited_node is not None: - return self._inherited_node.get(k, default) + try: + return self[k] + except KeyError: return default - return v @classmethod def from_section(cls, section: GDNodeSection): @@ -294,7 +296,7 @@ def get_node(self, path: str) -> Optional[Node]: return self.root.get_node(path) @classmethod - def build(cls, file: GDFile): + def build(cls, file: GDPackedScene): """Build the Tree from a flat list of [node]'s""" tree = cls() # Makes assumptions that the nodes are well-ordered @@ -326,8 +328,8 @@ def flatten(self) -> List[GDNodeSection]: return ret -def _load_parent_scene(root: Node, file: GDFile): - parent_file: GDFile = file.load_parent_scene() +def _load_parent_scene(root: Node, file: GDPackedScene): + parent_file = file.load_parent_scene() parent_tree = Tree.build(parent_file) # Transfer parent scene's children to this scene for child in parent_tree.root.get_children(): diff --git a/godot_parser/util.py b/godot_parser/util.py index 5728969..a297df3 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -1,33 +1,45 @@ """Utils""" -import json import os -from typing import Optional +from typing import Optional, Union +from godot_parser.output import Outputable, OutputFormat -def stringify_object(value): + +def stringify_object(value, output_format: OutputFormat = OutputFormat()): """Serialize a value to the godot file format""" if value is None: return "null" elif isinstance(value, str): - return json.dumps(value, ensure_ascii=False) + return '"%s"' % value.replace("\\", "\\\\").replace('"', '\\"') elif isinstance(value, bool): return "true" if value else "false" elif isinstance(value, dict): - if len(value) == 0: - return "{}" + if len(value.values()) == 0: + if output_format.single_line_on_empty_dict: + return "{}" + else: + return "{\n}" return ( "{\n" + ",\n".join( [ - "%s: %s" % (stringify_object(k), stringify_object(v)) + "%s: %s" + % ( + stringify_object(k, output_format), + stringify_object(v, output_format), + ) for k, v in value.items() ] ) + "\n}" ) elif isinstance(value, list): - return "[" + ", ".join([stringify_object(v) for v in value]) + "]" + return output_format.surround_brackets( + ", ".join([stringify_object(v, output_format) for v in value]) + ) + elif isinstance(value, Outputable): + return value.output_to_string(output_format) else: return str(value) @@ -39,14 +51,14 @@ def find_project_root(start: str) -> Optional[str]: while True: if os.path.isfile(os.path.join(curdir, "project.godot")): return curdir - next_dir = os.path.realpath(os.path.join(curdir, os.pardir)) + next_dir = os.path.dirname(curdir) if next_dir == curdir: return None curdir = next_dir def gdpath_to_filepath(root: str, path: str) -> str: - if not path.startswith("res://"): + if not is_gd_path(path): raise ValueError("'%s' is not a godot resource path" % path) pieces = path[6:].split("/") return os.path.join(root, *pieces) @@ -58,3 +70,8 @@ def filepath_to_gdpath(root: str, path: str) -> str: def is_gd_path(path: str) -> bool: return path.startswith("res://") + + +class Identifiable(object): + def get_id(self) -> Optional[Union[int, str]]: + return None diff --git a/godot_parser/values.py b/godot_parser/values.py index ab81265..9fabc42 100644 --- a/godot_parser/values.py +++ b/godot_parser/values.py @@ -24,7 +24,7 @@ null = Keyword("null").set_parse_action(lambda _: [None]) -_string = QuotedString('"', escChar="\\", multiline=True).set_name("string") +_string = QuotedString('"', esc_char="\\", multiline=True).set_name("string") _string_name = ( (Suppress("&") + _string) diff --git a/requirements_dev.txt b/requirements_dev.txt index 64e77bb..adb303f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,3 +4,4 @@ tox twine bumpversion build +packaging \ No newline at end of file diff --git a/test_parse_files.py b/test_parse_files.py index bccb7b0..6016924 100755 --- a/test_parse_files.py +++ b/test_parse_files.py @@ -1,65 +1,80 @@ #!/usr/bin/env python import argparse +import difflib +import io import os import sys -from itertools import zip_longest +import traceback -from godot_parser import load, parse +from godot_parser import parse +from godot_parser.output import VersionOutputFormat -def _parse_and_test_file(filename: str) -> bool: - print("Parsing %s" % filename) - with open(filename, "r") as ifile: - contents = ifile.read() +def _parse_and_test_file(filename: str, verbose: bool) -> bool: + if verbose: + print("Parsing %s" % filename) + with open(filename, "r", encoding="utf-8") as ifile: + original_file = ifile.read() try: - data = parse(contents) + parsed_file = parse(original_file) + output_format = VersionOutputFormat.guess_version(parsed_file) + output_file = parsed_file.output_to_string(output_format) except Exception: - print(" Parsing error!") - import traceback - - traceback.print_exc() + print("! Parsing error on %s" % filename, file=sys.stderr) + traceback.print_exc(file=sys.stderr) return False - f = load(filename) - with f.use_tree() as tree: - pass + diff = list( + difflib.context_diff( + io.StringIO(original_file).readlines(), + io.StringIO(str(output_file)).readlines(), + fromfile=filename, + tofile="PARSED FILE", + ) + ) + diff = [" " + "\n ".join(l.strip().split("\n")) + "\n" for l in diff] - data_lines = [l for l in str(data).split("\n") if l] - content_lines = [l for l in contents.split("\n") if l] - if data_lines != content_lines: - print(" Error!") - max_len = max([len(l) for l in content_lines]) - if max_len < 100: - for orig, parsed in zip_longest(content_lines, data_lines, fillvalue=""): - c = " " if orig == parsed else "x" - print("%s <%s> %s" % (orig.ljust(max_len), c, parsed)) - else: - for orig, parsed in zip_longest( - content_lines, data_lines, fillvalue="----EMPTY----" - ): - c = " " if orig == parsed else "XXX)" - print("%s\n%s%s" % (orig, c, parsed)) - return False - return True + if len(diff) == 0: + return True + + print("! Difference detected on %s" % filename) + print(" Version detected: %s" % output_format.version) + sys.stdout.writelines(diff) + return False def main(): """Test the parsing of one tscn file or all files in directory""" parser = argparse.ArgumentParser(description=main.__doc__) parser.add_argument("file_or_dir", help="Parse file or files under this directory") + parser.add_argument( + "--all", action="store_true", help="Tests all files even if one fails" + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Prints all file paths as they're parsed", + ) args = parser.parse_args() if os.path.isfile(args.file_or_dir): - _parse_and_test_file(args.file_or_dir) + _parse_and_test_file(args.file_or_dir, args.verbose) else: + all_passed = True for root, _dirs, files in os.walk(args.file_or_dir, topdown=False): for file in files: ext = os.path.splitext(file)[1] if ext not in [".tscn", ".tres"]: continue filepath = os.path.join(root, file) - if not _parse_and_test_file(filepath): - sys.exit(1) + if not _parse_and_test_file(filepath, args.verbose): + all_passed = False + if not args.all: + return 1 + + if all_passed: + print("All tests passed!") if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tests/projects/3.3/custom_node.gd b/tests/projects/3.3/custom_node.gd new file mode 100644 index 0000000..8f96e74 --- /dev/null +++ b/tests/projects/3.3/custom_node.gd @@ -0,0 +1,7 @@ +extends Node +class_name CustomNode + +export var godot_version : String; +export var internalResource : Resource; +export var externalResource : Resource; +export(NodePath) var remoteNode; diff --git a/tests/projects/3.3/custom_resource.gd b/tests/projects/3.3/custom_resource.gd new file mode 100644 index 0000000..d3523ac --- /dev/null +++ b/tests/projects/3.3/custom_resource.gd @@ -0,0 +1,11 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +export(String, MULTILINE) var test_string : String; +export var test_vector3 : Vector3; +export(CustomEnum) var test_enum; +export(Array, Vector3) var test_array_vector3 : Array; +export(Dictionary) var test_dict : Dictionary; +export var test_array_pool : PoolVector2Array; diff --git a/tests/projects/3.3/fresh_resource.tres b/tests/projects/3.3/fresh_resource.tres new file mode 100644 index 0000000..3e0f964 --- /dev/null +++ b/tests/projects/3.3/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.3/project.godot b/tests/projects/3.3/project.godot new file mode 100644 index 0000000..0e81eff --- /dev/null +++ b/tests/projects/3.3/project.godot @@ -0,0 +1,37 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Node", +"class": "CustomNode", +"language": "GDScript", +"path": "res://custom_node.gd" +}, { +"base": "Resource", +"class": "CustomResource", +"language": "GDScript", +"path": "res://custom_resource.gd" +} ] +_global_script_class_icons={ +"CustomNode": "", +"CustomResource": "" +} + +[application] + +config/name="SerializationTest" + +[global] + +env=false + +[physics] + +common/enable_pause_aware_picking=true diff --git a/tests/projects/3.3/resource.tres b/tests/projects/3.3/resource.tres new file mode 100644 index 0000000..278fc49 --- /dev/null +++ b/tests/projects/3.3/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "test" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.3/scene.tscn b/tests/projects/3.3/scene.tscn new file mode 100644 index 0000000..db108df --- /dev/null +++ b/tests/projects/3.3/scene.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://custom_node.gd" type="Script" id=1] +[ext_resource path="res://custom_resource.gd" type="Script" id=2] +[ext_resource path="res://resource.tres" type="Resource" id=3] + +[sub_resource type="Resource" id=1] +script = ExtResource( 2 ) +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3( 9, 0, 0 ) +test_enum = 1 +test_array_vector3 = [ Vector3( 0, 1, 0 ), Vector3( 2, 0, 2 ) ] +test_dict = { +1: "One", +1.1: "One dot One", +PoolIntArray( 2, 3 ): PoolByteArray( 255, 1, 1 ) +} +test_array_pool = PoolVector2Array( 50, 50, 20, 20 ) + +[node name="CustomNode" type="Node"] +script = ExtResource( 1 ) +godot_version = "3.3" +internalResource = SubResource( 1 ) +externalResource = ExtResource( 3 ) +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/3.4/custom_node.gd b/tests/projects/3.4/custom_node.gd new file mode 100644 index 0000000..8f96e74 --- /dev/null +++ b/tests/projects/3.4/custom_node.gd @@ -0,0 +1,7 @@ +extends Node +class_name CustomNode + +export var godot_version : String; +export var internalResource : Resource; +export var externalResource : Resource; +export(NodePath) var remoteNode; diff --git a/tests/projects/3.4/custom_resource.gd b/tests/projects/3.4/custom_resource.gd new file mode 100644 index 0000000..d3523ac --- /dev/null +++ b/tests/projects/3.4/custom_resource.gd @@ -0,0 +1,11 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +export(String, MULTILINE) var test_string : String; +export var test_vector3 : Vector3; +export(CustomEnum) var test_enum; +export(Array, Vector3) var test_array_vector3 : Array; +export(Dictionary) var test_dict : Dictionary; +export var test_array_pool : PoolVector2Array; diff --git a/tests/projects/3.4/fresh_resource.tres b/tests/projects/3.4/fresh_resource.tres new file mode 100644 index 0000000..3e0f964 --- /dev/null +++ b/tests/projects/3.4/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.4/project.godot b/tests/projects/3.4/project.godot new file mode 100644 index 0000000..0e81eff --- /dev/null +++ b/tests/projects/3.4/project.godot @@ -0,0 +1,37 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Node", +"class": "CustomNode", +"language": "GDScript", +"path": "res://custom_node.gd" +}, { +"base": "Resource", +"class": "CustomResource", +"language": "GDScript", +"path": "res://custom_resource.gd" +} ] +_global_script_class_icons={ +"CustomNode": "", +"CustomResource": "" +} + +[application] + +config/name="SerializationTest" + +[global] + +env=false + +[physics] + +common/enable_pause_aware_picking=true diff --git a/tests/projects/3.4/resource.tres b/tests/projects/3.4/resource.tres new file mode 100644 index 0000000..278fc49 --- /dev/null +++ b/tests/projects/3.4/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "test" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.4/scene.tscn b/tests/projects/3.4/scene.tscn new file mode 100644 index 0000000..b98007a --- /dev/null +++ b/tests/projects/3.4/scene.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://custom_node.gd" type="Script" id=1] +[ext_resource path="res://custom_resource.gd" type="Script" id=2] +[ext_resource path="res://resource.tres" type="Resource" id=3] + +[sub_resource type="Resource" id=1] +script = ExtResource( 2 ) +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3( 9, 0, 0 ) +test_enum = 1 +test_array_vector3 = [ Vector3( 0, 1, 0 ), Vector3( 2, 0, 2 ) ] +test_dict = { +1: "One", +1.1: "One dot One", +PoolIntArray( 2, 3 ): PoolByteArray( 255, 1, 1 ) +} +test_array_pool = PoolVector2Array( 50, 50, 20, 20 ) + +[node name="CustomNode" type="Node"] +script = ExtResource( 1 ) +godot_version = "3.4" +internalResource = SubResource( 1 ) +externalResource = ExtResource( 3 ) +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/3.5/custom_node.gd b/tests/projects/3.5/custom_node.gd new file mode 100644 index 0000000..8f96e74 --- /dev/null +++ b/tests/projects/3.5/custom_node.gd @@ -0,0 +1,7 @@ +extends Node +class_name CustomNode + +export var godot_version : String; +export var internalResource : Resource; +export var externalResource : Resource; +export(NodePath) var remoteNode; diff --git a/tests/projects/3.5/custom_resource.gd b/tests/projects/3.5/custom_resource.gd new file mode 100644 index 0000000..d3523ac --- /dev/null +++ b/tests/projects/3.5/custom_resource.gd @@ -0,0 +1,11 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +export(String, MULTILINE) var test_string : String; +export var test_vector3 : Vector3; +export(CustomEnum) var test_enum; +export(Array, Vector3) var test_array_vector3 : Array; +export(Dictionary) var test_dict : Dictionary; +export var test_array_pool : PoolVector2Array; diff --git a/tests/projects/3.5/fresh_resource.tres b/tests/projects/3.5/fresh_resource.tres new file mode 100644 index 0000000..3e0f964 --- /dev/null +++ b/tests/projects/3.5/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.5/project.godot b/tests/projects/3.5/project.godot new file mode 100644 index 0000000..0e81eff --- /dev/null +++ b/tests/projects/3.5/project.godot @@ -0,0 +1,37 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Node", +"class": "CustomNode", +"language": "GDScript", +"path": "res://custom_node.gd" +}, { +"base": "Resource", +"class": "CustomResource", +"language": "GDScript", +"path": "res://custom_resource.gd" +} ] +_global_script_class_icons={ +"CustomNode": "", +"CustomResource": "" +} + +[application] + +config/name="SerializationTest" + +[global] + +env=false + +[physics] + +common/enable_pause_aware_picking=true diff --git a/tests/projects/3.5/resource.tres b/tests/projects/3.5/resource.tres new file mode 100644 index 0000000..278fc49 --- /dev/null +++ b/tests/projects/3.5/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "test" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.5/scene.tscn b/tests/projects/3.5/scene.tscn new file mode 100644 index 0000000..e0591e1 --- /dev/null +++ b/tests/projects/3.5/scene.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://custom_node.gd" type="Script" id=1] +[ext_resource path="res://custom_resource.gd" type="Script" id=2] +[ext_resource path="res://resource.tres" type="Resource" id=3] + +[sub_resource type="Resource" id=1] +script = ExtResource( 2 ) +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3( 9, 0, 0 ) +test_enum = 1 +test_array_vector3 = [ Vector3( 0, 1, 0 ), Vector3( 2, 0, 2 ) ] +test_dict = { +1: "One", +1.1: "One dot One", +PoolIntArray( 2, 3 ): PoolByteArray( 255, 1, 1 ) +} +test_array_pool = PoolVector2Array( 50, 50, 20, 20 ) + +[node name="CustomNode" type="Node"] +script = ExtResource( 1 ) +godot_version = "3.5" +internalResource = SubResource( 1 ) +externalResource = ExtResource( 3 ) +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/3.6/custom_node.gd b/tests/projects/3.6/custom_node.gd new file mode 100644 index 0000000..8f96e74 --- /dev/null +++ b/tests/projects/3.6/custom_node.gd @@ -0,0 +1,7 @@ +extends Node +class_name CustomNode + +export var godot_version : String; +export var internalResource : Resource; +export var externalResource : Resource; +export(NodePath) var remoteNode; diff --git a/tests/projects/3.6/custom_resource.gd b/tests/projects/3.6/custom_resource.gd new file mode 100644 index 0000000..d3523ac --- /dev/null +++ b/tests/projects/3.6/custom_resource.gd @@ -0,0 +1,11 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +export(String, MULTILINE) var test_string : String; +export var test_vector3 : Vector3; +export(CustomEnum) var test_enum; +export(Array, Vector3) var test_array_vector3 : Array; +export(Dictionary) var test_dict : Dictionary; +export var test_array_pool : PoolVector2Array; diff --git a/tests/projects/3.6/fresh_resource.tres b/tests/projects/3.6/fresh_resource.tres new file mode 100644 index 0000000..3e0f964 --- /dev/null +++ b/tests/projects/3.6/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.6/project.godot b/tests/projects/3.6/project.godot new file mode 100644 index 0000000..0e81eff --- /dev/null +++ b/tests/projects/3.6/project.godot @@ -0,0 +1,37 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Node", +"class": "CustomNode", +"language": "GDScript", +"path": "res://custom_node.gd" +}, { +"base": "Resource", +"class": "CustomResource", +"language": "GDScript", +"path": "res://custom_resource.gd" +} ] +_global_script_class_icons={ +"CustomNode": "", +"CustomResource": "" +} + +[application] + +config/name="SerializationTest" + +[global] + +env=false + +[physics] + +common/enable_pause_aware_picking=true diff --git a/tests/projects/3.6/resource.tres b/tests/projects/3.6/resource.tres new file mode 100644 index 0000000..278fc49 --- /dev/null +++ b/tests/projects/3.6/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" load_steps=2 format=2] + +[ext_resource path="res://custom_resource.gd" type="Script" id=1] + +[resource] +script = ExtResource( 1 ) +test_string = "test" +test_vector3 = Vector3( 0, 0, 0 ) +test_enum = 0 +test_array_vector3 = [ ] +test_dict = { +} +test_array_pool = PoolVector2Array( ) diff --git a/tests/projects/3.6/scene.tscn b/tests/projects/3.6/scene.tscn new file mode 100644 index 0000000..5ad3a3e --- /dev/null +++ b/tests/projects/3.6/scene.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://custom_node.gd" type="Script" id=1] +[ext_resource path="res://custom_resource.gd" type="Script" id=2] +[ext_resource path="res://resource.tres" type="Resource" id=3] + +[sub_resource type="Resource" id=1] +script = ExtResource( 2 ) +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3( 9, 0, 0 ) +test_enum = 1 +test_array_vector3 = [ Vector3( 0, 1, 0 ), Vector3( 2, 0, 2 ) ] +test_dict = { +1: "One", +1.1: "One dot One", +PoolIntArray( 2, 3 ): PoolByteArray( 255, 1, 1 ) +} +test_array_pool = PoolVector2Array( 50, 50, 20, 20 ) + +[node name="CustomNode" type="Node"] +script = ExtResource( 1 ) +godot_version = "3.6" +internalResource = SubResource( 1 ) +externalResource = ExtResource( 3 ) +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.0/custom_node.gd b/tests/projects/4.0/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.0/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.0/custom_resource.gd b/tests/projects/4.0/custom_resource.gd new file mode 100644 index 0000000..6835c68 --- /dev/null +++ b/tests/projects/4.0/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +# @export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +# @export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.0/fresh_resource.tres b/tests/projects/4.0/fresh_resource.tres new file mode 100644 index 0000000..9642f94 --- /dev/null +++ b/tests/projects/4.0/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=3 uid="uid://bqjtw6e2p8hxm"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1_f8pg4"] + +[resource] +script = ExtResource("1_f8pg4") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() diff --git a/tests/projects/4.0/project.godot b/tests/projects/4.0/project.godot new file mode 100644 index 0000000..3828d54 --- /dev/null +++ b/tests/projects/4.0/project.godot @@ -0,0 +1,15 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="SerializationTest" +config/features=PackedStringArray("4.0", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.0/resource.tres b/tests/projects/4.0/resource.tres new file mode 100644 index 0000000..17bbd40 --- /dev/null +++ b/tests/projects/4.0/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=3 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string_name = &"" +test_string = "test" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() diff --git a/tests/projects/4.0/scene.tscn b/tests/projects/4.0/scene.tscn new file mode 100644 index 0000000..f3b2440 --- /dev/null +++ b/tests/projects/4.0/scene.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=6 format=3 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_pqy25"] +script = ExtResource("2") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"stringName" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray(255, 1, 1) +} +test_array_pool = PackedVector2Array(50, 50, 20, 20) +child_resource = SubResource("Resource_pqy25") + +[node name="CustomNode" type="Node"] +script = ExtResource("1") +godot_version = "4.0" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.1/custom_node.gd b/tests/projects/4.1/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.1/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.1/custom_resource.gd b/tests/projects/4.1/custom_resource.gd new file mode 100644 index 0000000..6835c68 --- /dev/null +++ b/tests/projects/4.1/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +# @export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +# @export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.1/fresh_resource.tres b/tests/projects/4.1/fresh_resource.tres new file mode 100644 index 0000000..19004db --- /dev/null +++ b/tests/projects/4.1/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=3 uid="uid://bqjtw6e2p8hxm"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1_8knv5"] + +[resource] +script = ExtResource("1_8knv5") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() diff --git a/tests/projects/4.1/project.godot b/tests/projects/4.1/project.godot new file mode 100644 index 0000000..6f4e5d1 --- /dev/null +++ b/tests/projects/4.1/project.godot @@ -0,0 +1,15 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Preload" +config/features=PackedStringArray("4.1", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.1/resource.tres b/tests/projects/4.1/resource.tres new file mode 100644 index 0000000..17bbd40 --- /dev/null +++ b/tests/projects/4.1/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=3 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string_name = &"" +test_string = "test" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() diff --git a/tests/projects/4.1/scene.tscn b/tests/projects/4.1/scene.tscn new file mode 100644 index 0000000..33cb7af --- /dev/null +++ b/tests/projects/4.1/scene.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=6 format=3 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" uid="uid://c2aqhm1bli2jd" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_6ce7h"] +script = ExtResource("2") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"stringName" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray(255, 1, 1) +} +test_array_pool = PackedVector2Array(50, 50, 20, 20) +child_resource = SubResource("Resource_6ce7h") + +[node name="CustomNode" type="Node"] +script = ExtResource("1") +godot_version = "4.1" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.2/custom_node.gd b/tests/projects/4.2/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.2/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.2/custom_resource.gd b/tests/projects/4.2/custom_resource.gd new file mode 100644 index 0000000..6835c68 --- /dev/null +++ b/tests/projects/4.2/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +# @export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +# @export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.2/fresh_resource.tres b/tests/projects/4.2/fresh_resource.tres new file mode 100644 index 0000000..9c5e858 --- /dev/null +++ b/tests/projects/4.2/fresh_resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=3 uid="uid://bqjtw6e2p8hxm"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1_v7iwk"] + +[resource] +script = ExtResource("1_v7iwk") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() diff --git a/tests/projects/4.2/project.godot b/tests/projects/4.2/project.godot new file mode 100644 index 0000000..c61ddd6 --- /dev/null +++ b/tests/projects/4.2/project.godot @@ -0,0 +1,15 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Preload" +config/features=PackedStringArray("4.2", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.2/resource.tres b/tests/projects/4.2/resource.tres new file mode 100644 index 0000000..17bbd40 --- /dev/null +++ b/tests/projects/4.2/resource.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=3 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string_name = &"" +test_string = "test" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() diff --git a/tests/projects/4.2/scene.tscn b/tests/projects/4.2/scene.tscn new file mode 100644 index 0000000..5e02c68 --- /dev/null +++ b/tests/projects/4.2/scene.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=6 format=3 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" uid="uid://c2aqhm1bli2jd" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_l04e0"] +script = ExtResource("2") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"stringName" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray(255, 1, 1) +} +test_array_pool = PackedVector2Array(50, 50, 20, 20) +child_resource = SubResource("Resource_l04e0") + +[node name="CustomNode" type="Node"] +script = ExtResource("1") +godot_version = "4.2" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.3/custom_node.gd b/tests/projects/4.3/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.3/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.3/custom_resource.gd b/tests/projects/4.3/custom_resource.gd new file mode 100644 index 0000000..73b4ae2 --- /dev/null +++ b/tests/projects/4.3/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +# @export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +@export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.3/fresh_resource.tres b/tests/projects/4.3/fresh_resource.tres new file mode 100644 index 0000000..32764c2 --- /dev/null +++ b/tests/projects/4.3/fresh_resource.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=4 uid="uid://bqjtw6e2p8hxm"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1_eat75"] + +[resource] +script = ExtResource("1_eat75") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() +test_array_pool2 = PackedVector4Array() diff --git a/tests/projects/4.3/project.godot b/tests/projects/4.3/project.godot new file mode 100644 index 0000000..aafeaa1 --- /dev/null +++ b/tests/projects/4.3/project.godot @@ -0,0 +1,15 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Preload" +config/features=PackedStringArray("4.3", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.3/resource.tres b/tests/projects/4.3/resource.tres new file mode 100644 index 0000000..231c10f --- /dev/null +++ b/tests/projects/4.3/resource.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=4 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string_name = &"" +test_string = "test" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() +test_array_pool2 = PackedVector4Array() diff --git a/tests/projects/4.3/scene.tscn b/tests/projects/4.3/scene.tscn new file mode 100644 index 0000000..44f5265 --- /dev/null +++ b/tests/projects/4.3/scene.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=6 format=4 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" uid="uid://c2aqhm1bli2jd" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_40tgu"] +script = ExtResource("2") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_array_pool = PackedVector2Array() +test_array_pool2 = PackedVector4Array() + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"stringName" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray("/wEB") +} +test_array_pool = PackedVector2Array(50, 50, 20, 20) +test_array_pool2 = PackedVector4Array(1, 2, 4, 8) +child_resource = SubResource("Resource_40tgu") + +[node name="CustomNode" type="Node"] +script = ExtResource("1") +godot_version = "4.3" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.4/custom_node.gd b/tests/projects/4.4/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.4/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.4/custom_node.gd.uid b/tests/projects/4.4/custom_node.gd.uid new file mode 100644 index 0000000..684b47a --- /dev/null +++ b/tests/projects/4.4/custom_node.gd.uid @@ -0,0 +1 @@ +uid://bvr5pt47ijphu diff --git a/tests/projects/4.4/custom_resource.gd b/tests/projects/4.4/custom_resource.gd new file mode 100644 index 0000000..67d5bc6 --- /dev/null +++ b/tests/projects/4.4/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +@export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +@export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.4/custom_resource.gd.uid b/tests/projects/4.4/custom_resource.gd.uid new file mode 100644 index 0000000..9399612 --- /dev/null +++ b/tests/projects/4.4/custom_resource.gd.uid @@ -0,0 +1 @@ +uid://d2fhcaebxigkv diff --git a/tests/projects/4.4/fresh_resource.tres b/tests/projects/4.4/fresh_resource.tres new file mode 100644 index 0000000..98dd895 --- /dev/null +++ b/tests/projects/4.4/fresh_resource.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=4 uid="uid://bqjtw6e2p8hxm"] + +[ext_resource type="Script" uid="uid://dwc8swu8kdiuo" path="res://custom_resource.gd" id="1_2f2vh"] + +[resource] +script = ExtResource("1_2f2vh") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_dict_int_str = Dictionary[int, String]({}) +test_array_pool = PackedVector2Array() +test_array_pool2 = PackedVector4Array() +metadata/_custom_type_script = "uid://dwc8swu8kdiuo" diff --git a/tests/projects/4.4/project.godot b/tests/projects/4.4/project.godot new file mode 100644 index 0000000..3aeb29c --- /dev/null +++ b/tests/projects/4.4/project.godot @@ -0,0 +1,15 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Preload" +config/features=PackedStringArray("4.4", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.4/resource.tres b/tests/projects/4.4/resource.tres new file mode 100644 index 0000000..ef86f72 --- /dev/null +++ b/tests/projects/4.4/resource.tres @@ -0,0 +1,15 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=4 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string_name = &"" +test_string = "test" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_dict_int_str = Dictionary[int, String]({}) +test_array_pool = PackedVector2Array() +test_array_pool2 = PackedVector4Array() diff --git a/tests/projects/4.4/scene.tscn b/tests/projects/4.4/scene.tscn new file mode 100644 index 0000000..4546133 --- /dev/null +++ b/tests/projects/4.4/scene.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=6 format=4 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" uid="uid://bvr5pt47ijphu" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" uid="uid://c2aqhm1bli2jd" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_3253y"] +script = ExtResource("2") +test_string_name = &"" +test_string = "" +test_vector3 = Vector3(0, 0, 0) +test_enum = 0 +test_array_vector3 = Array[Vector3]([]) +test_dict = {} +test_dict_int_str = Dictionary[int, String]({}) +test_array_pool = PackedVector2Array() +test_array_pool2 = PackedVector4Array() +metadata/_custom_type_script = "uid://d2fhcaebxigkv" + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"stringName" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray("/wEB") +} +test_dict_int_str = Dictionary[int, String]({ +5: "five" +}) +test_array_pool = PackedVector2Array(50, 50, 20, 20) +test_array_pool2 = PackedVector4Array(1, 2, 4, 8) +child_resource = SubResource("Resource_3253y") + +[node name="CustomNode" type="Node"] +script = ExtResource("1") +godot_version = "4.4" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.5/custom_node.gd b/tests/projects/4.5/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.5/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.5/custom_node.gd.uid b/tests/projects/4.5/custom_node.gd.uid new file mode 100644 index 0000000..684b47a --- /dev/null +++ b/tests/projects/4.5/custom_node.gd.uid @@ -0,0 +1 @@ +uid://bvr5pt47ijphu diff --git a/tests/projects/4.5/custom_resource.gd b/tests/projects/4.5/custom_resource.gd new file mode 100644 index 0000000..67d5bc6 --- /dev/null +++ b/tests/projects/4.5/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +@export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +@export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.5/custom_resource.gd.uid b/tests/projects/4.5/custom_resource.gd.uid new file mode 100644 index 0000000..9399612 --- /dev/null +++ b/tests/projects/4.5/custom_resource.gd.uid @@ -0,0 +1 @@ +uid://d2fhcaebxigkv diff --git a/tests/projects/4.5/fresh_resource.tres b/tests/projects/4.5/fresh_resource.tres new file mode 100644 index 0000000..f053083 --- /dev/null +++ b/tests/projects/4.5/fresh_resource.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=4 uid="uid://cmqyvug1htvo6"] + +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="1_odbn7"] + +[resource] +script = ExtResource("1_odbn7") +metadata/_custom_type_script = "uid://d2fhcaebxigkv" diff --git a/tests/projects/4.5/project.godot b/tests/projects/4.5/project.godot new file mode 100644 index 0000000..4fa9330 --- /dev/null +++ b/tests/projects/4.5/project.godot @@ -0,0 +1,15 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Preload" +config/features=PackedStringArray("4.5", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.5/resource.tres b/tests/projects/4.5/resource.tres new file mode 100644 index 0000000..1593cb5 --- /dev/null +++ b/tests/projects/4.5/resource.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="CustomResource" load_steps=2 format=4 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string = "test" diff --git a/tests/projects/4.5/scene.tscn b/tests/projects/4.5/scene.tscn new file mode 100644 index 0000000..90eca5a --- /dev/null +++ b/tests/projects/4.5/scene.tscn @@ -0,0 +1,39 @@ +[gd_scene load_steps=6 format=4 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" uid="uid://bvr5pt47ijphu" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" uid="uid://c2aqhm1bli2jd" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_3253y"] +script = ExtResource("2") +metadata/_custom_type_script = "uid://d2fhcaebxigkv" + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"stringName" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray("/wEB") +} +test_dict_int_str = Dictionary[int, String]({ +5: "five" +}) +test_array_pool = PackedVector2Array(50, 50, 20, 20) +test_array_pool2 = PackedVector4Array(1, 2, 4, 8) +child_resource = SubResource("Resource_3253y") + +[node name="CustomNode" type="Node"] +script = ExtResource("1") +godot_version = "4.5" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="."] diff --git a/tests/projects/4.6/custom_node.gd b/tests/projects/4.6/custom_node.gd new file mode 100644 index 0000000..dad20a5 --- /dev/null +++ b/tests/projects/4.6/custom_node.gd @@ -0,0 +1,13 @@ +@tool + +extends Node +class_name CustomNode + +@export var godot_version : String; +@export var internalResource : CustomResource; +@export var externalResource : CustomResource; +@export_node_path var remoteNode; + +func _ready(): + var file = FileAccess.open("res://resource.tres", FileAccess.READ) + print(file.get_as_text()) diff --git a/tests/projects/4.6/custom_node.gd.uid b/tests/projects/4.6/custom_node.gd.uid new file mode 100644 index 0000000..684b47a --- /dev/null +++ b/tests/projects/4.6/custom_node.gd.uid @@ -0,0 +1 @@ +uid://bvr5pt47ijphu diff --git a/tests/projects/4.6/custom_resource.gd b/tests/projects/4.6/custom_resource.gd new file mode 100644 index 0000000..67d5bc6 --- /dev/null +++ b/tests/projects/4.6/custom_resource.gd @@ -0,0 +1,15 @@ +extends Resource +class_name CustomResource + +enum CustomEnum{A, B, C} + +@export var test_string_name : StringName; +@export_multiline var test_string : String; +@export var test_vector3 : Vector3; +@export var test_enum : CustomEnum; +@export var test_array_vector3 : Array[Vector3]; +@export var test_dict : Dictionary; +@export var test_dict_int_str : Dictionary[int,String]; +@export var test_array_pool : PackedVector2Array; +@export var test_array_pool2 : PackedVector4Array; +@export var child_resource : CustomResource; diff --git a/tests/projects/4.6/custom_resource.gd.uid b/tests/projects/4.6/custom_resource.gd.uid new file mode 100644 index 0000000..9399612 --- /dev/null +++ b/tests/projects/4.6/custom_resource.gd.uid @@ -0,0 +1 @@ +uid://d2fhcaebxigkv diff --git a/tests/projects/4.6/fresh_resource.tres b/tests/projects/4.6/fresh_resource.tres new file mode 100644 index 0000000..36c32eb --- /dev/null +++ b/tests/projects/4.6/fresh_resource.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="CustomResource" format=4 uid="uid://cmqyvug1htvo6"] + +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="1_odbn7"] + +[resource] +script = ExtResource("1_odbn7") +metadata/_custom_type_script = "uid://d2fhcaebxigkv" diff --git a/tests/projects/4.6/project.godot b/tests/projects/4.6/project.godot new file mode 100644 index 0000000..6a15816 --- /dev/null +++ b/tests/projects/4.6/project.godot @@ -0,0 +1,19 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[animation] + +compatibility/default_parent_skeleton_in_mesh_instance_3d=true + +[application] + +config/name="Preload" +config/features=PackedStringArray("4.6", "GL Compatibility") +config/icon="res://icon.svg" diff --git a/tests/projects/4.6/resource.tres b/tests/projects/4.6/resource.tres new file mode 100644 index 0000000..a540712 --- /dev/null +++ b/tests/projects/4.6/resource.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="CustomResource" format=4 uid="uid://c2aqhm1bli2jd"] + +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="1"] + +[resource] +script = ExtResource("1") +test_string = "test" diff --git a/tests/projects/4.6/scene.tscn b/tests/projects/4.6/scene.tscn new file mode 100644 index 0000000..83efa33 --- /dev/null +++ b/tests/projects/4.6/scene.tscn @@ -0,0 +1,39 @@ +[gd_scene format=4 uid="uid://cqxcqnxlyn3vl"] + +[ext_resource type="Script" uid="uid://bvr5pt47ijphu" path="res://custom_node.gd" id="1"] +[ext_resource type="Script" uid="uid://d2fhcaebxigkv" path="res://custom_resource.gd" id="2"] +[ext_resource type="Resource" uid="uid://c2aqhm1bli2jd" path="res://resource.tres" id="3"] + +[sub_resource type="Resource" id="Resource_3253y"] +script = ExtResource("2") +metadata/_custom_type_script = "uid://d2fhcaebxigkv" + +[sub_resource type="Resource" id="1"] +script = ExtResource("2") +test_string_name = &"string\'Name" +test_string = "asd\\\\/\\'\" + +ç" +test_vector3 = Vector3(9, 0, 0) +test_enum = 1 +test_array_vector3 = Array[Vector3]([Vector3(0, 1, 0), Vector3(2, 0, 2)]) +test_dict = { +1: "One", +1.1: "One dot One", +PackedInt32Array(2, 3): PackedByteArray("/wEB") +} +test_dict_int_str = Dictionary[int, String]({ +5: "five" +}) +test_array_pool = PackedVector2Array(50, 50, 20, 20) +test_array_pool2 = PackedVector4Array(1, 2, 4, 8) +child_resource = SubResource("Resource_3253y") + +[node name="CustomNode" type="Node" unique_id=293065304] +script = ExtResource("1") +godot_version = "4.6" +internalResource = SubResource("1") +externalResource = ExtResource("3") +remoteNode = NodePath("ChildNode") + +[node name="ChildNode" type="Node" parent="." unique_id=1415315168] diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index 8715664..d43ee59 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -1,30 +1,43 @@ import tempfile import unittest -from godot_parser import GDFile, GDObject, GDResource, GDResourceSection, GDScene, Node +from godot_parser import ( + GDFile, + GDObject, + GDPackedScene, + GDResource, + GDResourceSection, + Node, + StringName, + TypedArray, + TypedDictionary, +) +from godot_parser.id_generator import SequentialHexGenerator +from godot_parser.output import OutputFormat class TestGDFile(unittest.TestCase): """Tests for GDFile""" + def setUp(self): + self.test_output_format = OutputFormat() + self.test_output_format._id_generator = SequentialHexGenerator() + def test_basic_scene(self): """Run the parsing test cases""" - self.assertEqual(str(GDScene()), "[gd_scene load_steps=1 format=2]\n") + self.assertEqual(str(GDPackedScene()), "[gd_scene format=3]\n") def test_all_data_types(self): """Run the parsing test cases""" - res = GDResource() - res.add_section( - GDResourceSection( - list=[1, 2.0, "string"], - map={"key": ["nested", GDObject("Vector2", 1, 1)]}, - empty=None, - escaped='foo("bar")', - ) + res = GDResource( + list=[1, 2.0, "string"], + map={"key": ["nested", GDObject("Vector2", 1, 1)]}, + empty=None, + escaped='foo("bar")', ) self.assertEqual( str(res), - """[gd_resource load_steps=1 format=2] + """[gd_resource format=3] [resource] list = [1, 2.0, "string"] @@ -38,36 +51,36 @@ def test_all_data_types(self): def test_ext_resource(self): """Test serializing a scene with an ext_resource""" - scene = GDScene() + scene = GDPackedScene() scene.add_ext_resource("res://Other.tscn", "PackedScene") self.assertEqual( - str(scene), - """[gd_scene load_steps=2 format=2] + scene.output_to_string(self.test_output_format), + """[gd_scene format=3] -[ext_resource path="res://Other.tscn" type="PackedScene" id=1] +[ext_resource path="res://Other.tscn" type="PackedScene" id="1_1"] """, ) def test_sub_resource(self): """Test serializing a scene with an sub_resource""" - scene = GDScene() + scene = GDPackedScene() scene.add_sub_resource("Animation") self.assertEqual( - str(scene), - """[gd_scene load_steps=2 format=2] + scene.output_to_string(self.test_output_format), + """[gd_scene format=3] -[sub_resource type="Animation" id=1] +[sub_resource type="Animation" id="Resource_1"] """, ) def test_node(self): """Test serializing a scene with a node""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode", type="Node2D") scene.add_node("Child", type="Area2D", parent=".") self.assertEqual( str(scene), - """[gd_scene load_steps=1 format=2] + """[gd_scene format=3] [node name="RootNode" type="Node2D"] @@ -77,7 +90,7 @@ def test_node(self): def test_tree_create(self): """Test creating a scene with the tree API""" - scene = GDScene() + scene = GDPackedScene() with scene.use_tree() as tree: tree.root = Node("RootNode", type="Node2D") tree.root.add_child( @@ -85,7 +98,7 @@ def test_tree_create(self): ) self.assertEqual( str(scene), - """[gd_scene load_steps=1 format=2] + """[gd_scene format=3] [node name="RootNode" type="Node2D"] @@ -96,7 +109,7 @@ def test_tree_create(self): def test_tree_deep_create(self): """Test creating a scene with nested children using the tree API""" - scene = GDScene() + scene = GDPackedScene() with scene.use_tree() as tree: tree.root = Node("RootNode", type="Node2D") child = Node("Child", type="Node") @@ -105,7 +118,7 @@ def test_tree_deep_create(self): child.add_child(Node("ChildChild2", type="Node")) self.assertEqual( str(scene), - """[gd_scene load_steps=1 format=2] + """[gd_scene format=3] [node name="RootNode" type="Node2D"] @@ -130,7 +143,7 @@ def test_remove_section(self): def test_section_ordering(self): """Sections maintain an ordering""" - scene = GDScene() + scene = GDPackedScene() node = scene.add_node("RootNode") scene.add_ext_resource("res://Other.tscn", "PackedScene") res = scene.find_section("ext_resource") @@ -138,34 +151,37 @@ def test_section_ordering(self): def test_add_ext_node(self): """Test GDScene.add_ext_node""" - scene = GDScene() + scene = GDPackedScene() res = scene.add_ext_resource("res://Other.tscn", "PackedScene") + scene.generate_resource_ids() node = scene.add_ext_node("Root", res.id) self.assertEqual(node.name, "Root") self.assertEqual(node.instance, res.id) + self.assertTrue(scene.is_inherited) def test_write(self): """Test writing scene out to a file""" - scene = GDScene() + scene = GDPackedScene() outfile = tempfile.mkstemp()[1] scene.write(outfile) with open(outfile, "r", encoding="utf-8") as ifile: - gen_scene = GDScene.parse(ifile.read()) + gen_scene = GDPackedScene.parse(ifile.read()) self.assertEqual(scene, gen_scene) def test_get_node_none(self): """get_node() works with no nodes""" - scene = GDScene() + scene = GDPackedScene() n = scene.get_node() self.assertIsNone(n) def test_addremove_ext_res(self): """Test adding and removing an ext_resource""" - scene = GDScene() + scene = GDPackedScene() res = scene.add_ext_resource("res://Res.tscn", "PackedScene") - self.assertEqual(res.id, 1) res2 = scene.add_ext_resource("res://Sprite.png", "Texture") - self.assertEqual(res2.id, 2) + scene.generate_resource_ids(self.test_output_format) + self.assertEqual(res.id, "1_1") + self.assertEqual(res2.id, "2_2") node = scene.add_node("Sprite", "Sprite") node["texture"] = res2.reference node["textures"] = [res2.reference] @@ -174,10 +190,9 @@ def test_addremove_ext_res(self): s = scene.find_section(path="res://Res.tscn") scene.remove_section(s) - scene.renumber_resource_ids() s = scene.find_section("ext_resource") - self.assertEqual(s.id, 1) + self.assertEqual(s.id, "2_2") self.assertEqual(node["texture"], s.reference) self.assertEqual(node["textures"][0], s.reference) self.assertEqual(node["texture_map"]["tex"], s.reference) @@ -185,7 +200,7 @@ def test_addremove_ext_res(self): def test_remove_unused_resource(self): """Can remove unused resources""" - scene = GDScene() + scene = GDPackedScene() res = scene.add_ext_resource("res://Res.tscn", "PackedScene") scene.remove_unused_resources() resources = scene.get_sections("ext_resource") @@ -195,25 +210,80 @@ def test_addremove_sub_res(self): """Test adding and removing a sub_resource""" scene = GDResource() res = scene.add_sub_resource("CircleShape2D") - self.assertEqual(res.id, 1) res2 = scene.add_sub_resource("AnimationNodeAnimation") - self.assertEqual(res2.id, 2) + scene.generate_resource_ids(self.test_output_format) + self.assertEqual(res.id, "Resource_1") + self.assertEqual(res2.id, "Resource_2") resource = GDResourceSection(shape=res2.reference) scene.add_section(resource) s = scene.find_sub_resource(type="CircleShape2D") scene.remove_section(s) - scene.renumber_resource_ids() s = scene.find_section("sub_resource") - self.assertEqual(s.id, 1) + self.assertEqual(s.id, "Resource_2") self.assertEqual(resource["shape"], s.reference) + def test_remove_unused_nested(self): + res = GDResource("CustomResource") + + res1 = res.add_sub_resource("CustomResource") + res["child_resource"] = res1.reference + + res2 = res.add_sub_resource("CustomResource") + res1["child_resource"] = res2.reference + + res3 = res.add_sub_resource("CustomResource") + res2["child_resource"] = res3.reference + + res.generate_resource_ids(self.test_output_format) + + self.assertEqual(len(res._sections), 5) + self.assertIn(res1, res._sections) + self.assertIn(res2, res._sections) + self.assertIn(res3, res._sections) + + del res1["child_resource"] + res.remove_unused_resources() + + self.assertEqual(len(res._sections), 3) + self.assertIn(res1, res._sections) + self.assertNotIn(res2, res._sections) + self.assertNotIn(res3, res._sections) + + def test_remove_unused_typed(self): + """Won't remove used types""" + resource = GDResource() + + script1 = resource.add_ext_resource("res://custom_resource.gd", "Script") + sub_res1 = resource.add_sub_resource("Resource") + + script2 = resource.add_ext_resource("res://custom_resource_2.gd", "Script") + sub_res2 = resource.add_sub_resource("Resource") + + resource.add_ext_resource("res://custom_resource_3.gd", "Script") + + resource["typedArray"] = TypedArray(script1.reference, [sub_res1.reference]) + resource["typedDict"] = TypedDictionary( + script2.reference, "String", {sub_res2.reference: "Cool"} + ) + + resource.generate_resource_ids() + + self.assertEqual(len(resource.get_sub_resources()), 2) + self.assertEqual(len(resource.get_ext_resources()), 3) + + resource.remove_unused_resources() + + self.assertEqual(len(resource.get_sub_resources()), 2) + self.assertEqual(len(resource.get_ext_resources()), 2) + def test_find_constraints(self): """Test for the find_section constraints""" - scene = GDScene() + scene = GDPackedScene() res1 = scene.add_sub_resource("CircleShape2D", radius=1) res2 = scene.add_sub_resource("CircleShape2D", radius=2) + scene.generate_resource_ids() found = list(scene.find_all("sub_resource")) self.assertCountEqual(found, [res1, res2]) @@ -226,7 +296,7 @@ def test_find_constraints(self): def test_find_node(self): """Test GDScene.find_node""" - scene = GDScene() + scene = GDPackedScene() n1 = scene.add_node("Root", "Node") n2 = scene.add_node("Child", "Node", parent=".") node = scene.find_node(name="Root") @@ -236,9 +306,108 @@ def test_find_node(self): def test_file_equality(self): """Tests for GDFile == GDFile""" - s1 = GDScene(GDResourceSection()) - s2 = GDScene(GDResourceSection()) + s1 = GDPackedScene(GDResourceSection()) + s2 = GDPackedScene(GDResourceSection()) self.assertEqual(s1, s2) resource = s1.find_section("resource") resource["key"] = "value" self.assertNotEqual(s1, s2) + + def test_renumber_ids(self): + output_format = OutputFormat(resource_ids_as_strings=False) + + res = GDResource() + res1 = res.add_sub_resource("CircleShape2D") + res2 = res.add_sub_resource("AnimationNodeAnimation") + res.generate_resource_ids(output_format) + + self.assertEqual(res1.id, 1) + self.assertEqual(res2.id, 2) + + res.remove_section(res1) + res.renumber_resource_ids() + + self.assertEqual(res2.id, 1) + + def test_string_special_characters(self): + input = "".join( + [ + " ", + '"', + "a", + " ", + "'", + "'", + "\\", + "\n", + "\t", + "\n", + "\\", + "n", + "\n", + ] + ) + + res = GDResource() + res["str_value"] = input + res["str_name"] = StringName(input) + + print(str(res)) + + self.assertEqual( + str(res), + """[gd_resource format=3] + +[resource] +str_value = "%s" +str_name = &"%s" +""" + % ( + "".join( + [ + " ", + "\\", + '"', + "a", + " ", + "'", + "'", + "\\", + "\\", + "\n", + "\t", + "\n", + "\\", + "\\", + "n", + "\n", + ] + ), + "".join( + [ + " ", + "\\", + '"', + "a", + " ", + "\\", + "'", + "\\", + "'", + "\\", + "\\", + "\\", + "n", + "\\", + "t", + "\\", + "n", + "\\", + "\\", + "n", + "\\", + "n", + ] + ), + ), + ) diff --git a/tests/test_objects.py b/tests/test_objects.py index 969f38e..d3d04f9 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -12,6 +12,7 @@ Vector2, Vector3, ) +from godot_parser.objects import PackedByteArray, PackedVector4Array, Vector4 class TestGDObjects(unittest.TestCase): @@ -57,6 +58,55 @@ def test_vector3(self): self.assertEqual(v[1], 4) self.assertEqual(v[2], 5) + def test_vector4(self): + """Test for Vector4""" + v = Vector4(1, 2, 3, 4) + self.assertEqual(v[0], 1) + self.assertEqual(v[1], 2) + self.assertEqual(v[2], 3) + self.assertEqual(v[3], 4) + self.assertEqual(v.x, 1) + self.assertEqual(v.y, 2) + self.assertEqual(v.z, 3) + self.assertEqual(v.w, 4) + self.assertEqual(str(v), "Vector4(1, 2, 3, 4)") + v.x = 2 + v.y = 3 + v.z = 4 + v.w = 5 + self.assertEqual(v.x, 2) + self.assertEqual(v.y, 3) + self.assertEqual(v.z, 4) + self.assertEqual(v.w, 5) + v[0] = 3 + v[1] = 4 + v[2] = 5 + v[3] = 6 + self.assertEqual(v[0], 3) + self.assertEqual(v[1], 4) + self.assertEqual(v[2], 5) + self.assertEqual(v[3], 6) + + def test_packed_vector4_array(self): + """Test for PackedVector4Array""" + array = PackedVector4Array.FromList( + [Vector4(i, i * 2, i * 3, i * 4) for i in range(3)] + ) + self.assertEqual(array.get_vector4(0), Vector4(0, 0, 0, 0)) + self.assertEqual(array.get_vector4(1), Vector4(1, 2, 3, 4)) + self.assertEqual(array.get_vector4(2), Vector4(2, 4, 6, 8)) + + array.remove_vector4_at(1) + + self.assertEqual(array.get_vector4(1), Vector4(2, 4, 6, 8)) + + def test_packed_byte_array(self): + """Test for PackedVector4Array""" + array = PackedByteArray.FromBytes(bytes(range(3))) + self.assertEqual(array.bytes_[0], 0) + self.assertEqual(array.bytes_[1], 1) + self.assertEqual(array.bytes_[2], 2) + def test_color(self): """Test for Color""" c = Color(0.1, 0.2, 0.3, 0.4) diff --git a/tests/test_output.py b/tests/test_output.py new file mode 100644 index 0000000..d068d57 --- /dev/null +++ b/tests/test_output.py @@ -0,0 +1,441 @@ +import base64 +import unittest + +from godot_parser import ( + GDExtResourceSection, + GDObject, + GDResource, + GDSubResourceSection, + StringName, + TypedArray, + Vector3, +) +from godot_parser.id_generator import SequentialHexGenerator +from godot_parser.objects import ( + PackedByteArray, + PackedVector4Array, + TypedDictionary, + Vector4, +) +from godot_parser.output import OutputFormat, VersionOutputFormat + + +class TestOutputFormat(unittest.TestCase): + def test_version_output_format(self): + version_output_format = VersionOutputFormat("3.6") + self.assertTrue(version_output_format.punctuation_spaces) + self.assertFalse(version_output_format.resource_ids_as_strings) + self.assertFalse(version_output_format.typed_array_support) + self.assertFalse(version_output_format.packed_byte_array_base64_support) + self.assertFalse(version_output_format.typed_dictionary_support) + self.assertTrue(version_output_format.load_steps) + + version_output_format = VersionOutputFormat("4.0") + self.assertFalse(version_output_format.punctuation_spaces) + self.assertTrue(version_output_format.resource_ids_as_strings) + self.assertTrue(version_output_format.typed_array_support) + self.assertFalse(version_output_format.packed_byte_array_base64_support) + self.assertFalse(version_output_format.typed_dictionary_support) + self.assertTrue(version_output_format.load_steps) + + version_output_format = VersionOutputFormat("4.1") + self.assertFalse(version_output_format.punctuation_spaces) + self.assertTrue(version_output_format.resource_ids_as_strings) + self.assertTrue(version_output_format.typed_array_support) + self.assertFalse(version_output_format.packed_byte_array_base64_support) + self.assertFalse(version_output_format.typed_dictionary_support) + self.assertTrue(version_output_format.load_steps) + + version_output_format = VersionOutputFormat("4.3") + self.assertFalse(version_output_format.punctuation_spaces) + self.assertTrue(version_output_format.resource_ids_as_strings) + self.assertTrue(version_output_format.typed_array_support) + self.assertTrue(version_output_format.packed_byte_array_base64_support) + self.assertFalse(version_output_format.typed_dictionary_support) + self.assertTrue(version_output_format.load_steps) + + version_output_format = VersionOutputFormat("4.4") + self.assertFalse(version_output_format.punctuation_spaces) + self.assertTrue(version_output_format.resource_ids_as_strings) + self.assertTrue(version_output_format.typed_array_support) + self.assertTrue(version_output_format.packed_byte_array_base64_support) + self.assertTrue(version_output_format.typed_dictionary_support) + self.assertTrue(version_output_format.load_steps) + + version_output_format = VersionOutputFormat("4.5") + self.assertFalse(version_output_format.punctuation_spaces) + self.assertTrue(version_output_format.resource_ids_as_strings) + self.assertTrue(version_output_format.typed_array_support) + self.assertTrue(version_output_format.packed_byte_array_base64_support) + self.assertTrue(version_output_format.typed_dictionary_support) + self.assertTrue(version_output_format.load_steps) + + version_output_format = VersionOutputFormat("4.6") + self.assertFalse(version_output_format.punctuation_spaces) + self.assertTrue(version_output_format.resource_ids_as_strings) + self.assertTrue(version_output_format.typed_array_support) + self.assertTrue(version_output_format.packed_byte_array_base64_support) + self.assertTrue(version_output_format.typed_dictionary_support) + self.assertFalse(version_output_format.load_steps) + + def test_punctuation_spaces(self): + resource = GDResource() + resource["array"] = [Vector3(1, 2, 3)] + + self.assertEqual( + resource.output_to_string(OutputFormat(punctuation_spaces=False)), + """[gd_resource format=3] + +[resource] +array = [Vector3(1, 2, 3)]\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(punctuation_spaces=True)), + """[gd_resource format=3] + +[resource] +array = [ Vector3( 1, 2, 3 ) ]\n""", + ) + + def test_single_line_dict(self): + resource = GDResource() + resource["dict"] = {} + + self.assertEqual( + resource.output_to_string(OutputFormat(single_line_on_empty_dict=True)), + """[gd_resource format=3] + +[resource] +dict = {}\n""", + ) + self.assertEqual( + resource.output_to_string(OutputFormat(single_line_on_empty_dict=False)), + """[gd_resource format=3] + +[resource] +dict = { +}\n""", + ) + + def test_load_steps(self): + resource = GDResource() + resource["toggle"] = True + + true_output_format = OutputFormat(load_steps=True) + true_output_format._id_generator = SequentialHexGenerator() + + false_output_format = OutputFormat(load_steps=False) + false_output_format._id_generator = SequentialHexGenerator() + + self.assertEqual( + resource.output_to_string(false_output_format), + """[gd_resource format=3] + +[resource] +toggle = true\n""", + ) + self.assertEqual( + resource.output_to_string(true_output_format), + """[gd_resource load_steps=1 format=3] + +[resource] +toggle = true\n""", + ) + + resource.add_ext_resource("res://a.tres", "CustomResource") + + self.assertEqual( + resource.output_to_string(false_output_format), + """[gd_resource format=3] + +[ext_resource path="res://a.tres" type="CustomResource" id="1_1"] + +[resource] +toggle = true\n""", + ) + self.assertEqual( + resource.output_to_string(true_output_format), + """[gd_resource load_steps=2 format=3] + +[ext_resource path="res://a.tres" type="CustomResource" id="1_1"] + +[resource] +toggle = true\n""", + ) + + resource.add_sub_resource("CustomResource") + + self.assertEqual( + resource.output_to_string(false_output_format), + """[gd_resource format=3] + +[ext_resource path="res://a.tres" type="CustomResource" id="1_1"] + +[sub_resource type="CustomResource" id="Resource_2"] + +[resource] +toggle = true\n""", + ) + self.assertEqual( + resource.output_to_string(true_output_format), + """[gd_resource load_steps=3 format=3] + +[ext_resource path="res://a.tres" type="CustomResource" id="1_1"] + +[sub_resource type="CustomResource" id="Resource_2"] + +[resource] +toggle = true\n""", + ) + + def test_resource_ids_as_string(self): + resource = GDResource() + resource["toggle"] = True + resource.add_ext_resource("res://a.tres", "CustomResource") + resource.add_sub_resource("CustomResource") + + false_output_format = OutputFormat(resource_ids_as_strings=False) + + self.assertEqual( + resource.output_to_string(false_output_format), + """[gd_resource format=2] + +[ext_resource path="res://a.tres" type="CustomResource" id=1] + +[sub_resource type="CustomResource" id=1] + +[resource] +toggle = true\n""", + ) + + resource = GDResource() + resource["toggle"] = True + resource.add_ext_resource("res://a.tres", "CustomResource") + resource.add_sub_resource("CustomResource") + + true_output_format = OutputFormat(resource_ids_as_strings=True) + true_output_format._id_generator = SequentialHexGenerator() + + self.assertEqual( + resource.output_to_string(true_output_format), + """[gd_resource format=3] + +[ext_resource path="res://a.tres" type="CustomResource" id="1_1"] + +[sub_resource type="CustomResource" id="Resource_2"] + +[resource] +toggle = true\n""", + ) + + def test_resource_ids_as_string_migration(self): + resource = GDResource() + ext = GDExtResourceSection("res://a.tres", "CustomResource", 1) + sub = GDSubResourceSection("CustomResource", 1) + + resource.add_section(ext) + resource.add_section(sub) + + resource["ext"] = ext.reference + resource["sub"] = sub.reference + + self.assertEqual( + resource.output_to_string(OutputFormat(resource_ids_as_strings=False)), + """[gd_resource format=2] + +[ext_resource path="res://a.tres" type="CustomResource" id=1] + +[sub_resource type="CustomResource" id=1] + +[resource] +ext = ExtResource(1) +sub = SubResource(1)\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(resource_ids_as_strings=True)), + """[gd_resource format=3] + +[ext_resource path="res://a.tres" type="CustomResource" id="1"] + +[sub_resource type="CustomResource" id="1"] + +[resource] +ext = ExtResource("1") +sub = SubResource("1")\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(resource_ids_as_strings=False)), + """[gd_resource format=2] + +[ext_resource path="res://a.tres" type="CustomResource" id=1] + +[sub_resource type="CustomResource" id=1] + +[resource] +ext = ExtResource(1) +sub = SubResource(1)\n""", + ) + + def test_typed_array_support(self): + resource = GDResource() + resource["test"] = TypedArray("int", [3, 1, 2]) + + self.assertEqual( + resource.output_to_string(OutputFormat(typed_array_support=True)), + """[gd_resource format=3] + +[resource] +test = Array[int]([3, 1, 2])\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(typed_array_support=False)), + """[gd_resource format=3] + +[resource] +test = [3, 1, 2]\n""", + ) + + def test_typed_dictionary_support(self): + resource = GDResource() + resource["test"] = TypedDictionary( + "int", + "String", + { + 1: "One", + 2: "Two", + }, + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(typed_dictionary_support=True)), + """[gd_resource format=3] + +[resource] +test = Dictionary[int, String]({ +1: "One", +2: "Two" +})\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(typed_dictionary_support=False)), + """[gd_resource format=3] + +[resource] +test = { +1: "One", +2: "Two" +}\n""", + ) + + def test_packed_vector4_array_support(self): + resource = GDResource() + resource["test"] = PackedVector4Array.FromList([Vector4(1, 2, 3, 4)]) + + self.assertEqual( + resource.output_to_string(OutputFormat(packed_vector4_array_support=True)), + """[gd_resource format=4] + +[resource] +test = PackedVector4Array(1, 2, 3, 4)\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(packed_vector4_array_support=False)), + """[gd_resource format=3] + +[resource] +test = Array[Vector4]([Vector4(1, 2, 3, 4)])\n""", + ) + + self.assertEqual( + resource.output_to_string( + OutputFormat( + packed_vector4_array_support=False, typed_array_support=False + ) + ), + """[gd_resource format=3] + +[resource] +test = [Vector4(1, 2, 3, 4)]\n""", + ) + + def test_packed_byte_array_base64_support(self): + bytes_ = bytes([5, 88, 10]) + + resource = GDResource() + resource["test"] = PackedByteArray.FromBytes(bytes_) + + self.assertEqual( + resource.output_to_string( + OutputFormat(packed_byte_array_base64_support=True) + ), + """[gd_resource format=4] + +[resource] +test = PackedByteArray("%s")\n""" % base64.b64encode(bytes_).decode("utf-8"), + ) + + self.assertEqual( + resource.output_to_string( + OutputFormat(packed_byte_array_base64_support=False) + ), + """[gd_resource format=3] + +[resource] +test = PackedByteArray(5, 88, 10)\n""", + ) + + def test_packed_array_format(self): + resource = GDResource() + resource["test1"] = PackedByteArray.FromBytes(bytes(range(4))) + resource["test2"] = GDObject("PackedVector2Array", *range(4)) + + self.assertEqual( + resource.output_to_string( + OutputFormat(packed_byte_array_base64_support=False) + ), + """[gd_resource format=3] + +[resource] +test1 = PackedByteArray(0, 1, 2, 3) +test2 = PackedVector2Array(0, 1, 2, 3)\n""", + ) + + self.assertEqual( + resource.output_to_string( + OutputFormat( + packed_array_format="Pool%sArray", + packed_byte_array_base64_support=False, + ) + ), + """[gd_resource format=3] + +[resource] +test1 = PoolByteArray(0, 1, 2, 3) +test2 = PoolVector2Array(0, 1, 2, 3)\n""", + ) + + def test_string_name(self): + resource = GDResource() + resource["strName"] = StringName("StringName") + + self.assertEqual( + resource.output_to_string(OutputFormat(string_name_support=True)), + """[gd_resource format=3] + +[resource] +strName = &"StringName"\n""", + ) + + self.assertEqual( + resource.output_to_string(OutputFormat(string_name_support=False)), + """[gd_resource format=3] + +[resource] +strName = "StringName"\n""", + ) diff --git a/tests/test_parser.py b/tests/test_parser.py index 575ac5b..457f33a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -126,13 +126,13 @@ ), ( """[sub_resource type="CustomType" id=1] - string_value = "String" - string_name_value = &"StringName" - string_quote = "\\"String\\"" - string_name_quote = &"\\"StringName\\"" - string_single_quote = "\'String\'" - string_name_single_quote = &"\'StringName\'" - """, + string_value = "String" + string_name_value = &"StringName" + string_quote = "\\"String\\"" + string_name_quote = &"\\"StringName\\"" + string_single_quote = "\'String\'" + string_name_single_quote = &"\'StringName\'" + """, GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), @@ -149,13 +149,13 @@ ), ( """[sub_resource type="CustomType" id=1] - typed_dict_1 = Dictionary[StringName, ExtResource("1_testt")]({ - &"key": ExtResource("2_testt") - }) - typed_dict_2 = Dictionary[ExtResource("1_testt"), StringName]({ - ExtResource("2_testt"): &"key" - }) - """, + typed_dict_1 = Dictionary[StringName, ExtResource("1_testt")]({ + &"key": ExtResource("2_testt") + }) + typed_dict_2 = Dictionary[ExtResource("1_testt"), StringName]({ + ExtResource("2_testt"): &"key" + }) + """, GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), @@ -176,9 +176,9 @@ ), ( """[sub_resource type="CustomType" id=1] - typed_array_1 = Array[StringName]([&"a", &"b", &"c"]) - typed_array_2 = Array[ExtResource("1_typee")]([ExtResource("1_qwert"), ExtResource("2_testt")]) - """, + typed_array_1 = Array[StringName]([&"a", &"b", &"c"]) + typed_array_2 = Array[ExtResource("1_typee")]([ExtResource("1_qwert"), ExtResource("2_testt")]) + """, GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), @@ -204,8 +204,8 @@ ), ( """[node name="Label" type="Label" parent="." unique_id=1387035530] - text = "\ta\\\"q\\'é'd\\\"\n\n\\\\" - """, + text = "\ta\\\"q\\'é'd\\\"\n\n\\\\" + """, GDFile( GDSection( GDSectionHeader( @@ -247,4 +247,5 @@ def _run_test(self, string: str, expected): def test_cases(self): """Run the parsing test cases""" for string, expected in TEST_CASES: - self._run_test(string, expected) + with self.subTest(string): + self._run_test(string, expected) diff --git a/tests/test_sections.py b/tests/test_sections.py index 542e454..b8ae8cd 100644 --- a/tests/test_sections.py +++ b/tests/test_sections.py @@ -39,11 +39,10 @@ def test_section_dunder(self): del s["vframes"] self.assertEqual(s.get("vframes"), None) - del s["vframes"] def test_ext_resource(self): """Test for GDExtResourceSection""" - s = GDExtResourceSection("res://Other.tscn", type="PackedScene", id=1) + s = GDExtResourceSection("res://Other.tscn", type_="PackedScene", id_=1) self.assertEqual(s.path, "res://Other.tscn") self.assertEqual(s.type, "PackedScene") self.assertEqual(s.id, 1) @@ -56,7 +55,7 @@ def test_ext_resource(self): def test_sub_resource(self): """Test for GDSubResourceSection""" - s = GDSubResourceSection(type="CircleShape2D", id=1) + s = GDSubResourceSection(type_="CircleShape2D", id_=1) self.assertEqual(s.type, "CircleShape2D") self.assertEqual(s.id, 1) s.type = "Animation" diff --git a/tests/test_tree.py b/tests/test_tree.py index d99ac7e..dc56791 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -3,7 +3,7 @@ import tempfile import unittest -from godot_parser import GDScene, Node, SubResource, TreeMutationException +from godot_parser import GDPackedScene, Node, SubResource, TreeMutationException from godot_parser.util import find_project_root, gdpath_to_filepath @@ -12,7 +12,7 @@ class TestTree(unittest.TestCase): def test_get_node(self): """Test for get_node()""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode") scene.add_node("Child", parent=".") child = scene.add_node("Child2", parent="Child") @@ -21,7 +21,7 @@ def test_get_node(self): def test_remove_node(self): """Test for remove_node()""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode") scene.add_node("Child", parent=".") node = scene.find_section("node", name="Child") @@ -58,7 +58,7 @@ def test_remove_node(self): def test_insert_child(self): """Test for insert_child()""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode") scene.add_node("Child1", parent=".") with scene.use_tree() as tree: @@ -72,29 +72,28 @@ def test_insert_child(self): def test_empty_scene(self): """Empty scenes should not crash""" - scene = GDScene() + scene = GDPackedScene() with scene.use_tree() as tree: n = tree.get_node("Any") self.assertIsNone(n) def test_get_missing_node(self): """get_node on missing node should return None""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode") node = scene.get_node("Foo/Bar/Baz") self.assertIsNone(node) def test_properties(self): """Test for changing properties on a node""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode") with scene.use_tree() as tree: tree.root["vframes"] = 10 self.assertEqual(tree.root["vframes"], 10) tree.root["hframes"] = 10 del tree.root["hframes"] - del tree.root["hframes"] - self.assertIsNone(tree.root.get("hframes")) + self.assertNotIn("hframes", tree.root) child = scene.find_section("node") self.assertEqual(child["vframes"], 10) @@ -124,7 +123,7 @@ def setUpClass(cls): cls.root_scene = os.path.join(cls.project_dir, "Root.tscn") cls.mid_scene = os.path.join(cls.project_dir, "Mid.tscn") cls.leaf_scene = os.path.join(cls.project_dir, "Leaf.tscn") - scene = GDScene.parse(""" + scene = GDPackedScene.parse(""" [gd_scene load_steps=1 format=2] [node name="Root" type="KinematicBody2D"] collision_layer = 3 @@ -137,7 +136,7 @@ def setUpClass(cls): """) scene.write(cls.root_scene) - scene = GDScene.parse(""" + scene = GDPackedScene.parse(""" [gd_scene load_steps=2 format=2] [ext_resource path="res://Root.tscn" type="PackedScene" id=1] [node name="Mid" instance=ExtResource( 1 )] @@ -147,7 +146,7 @@ def setUpClass(cls): """) scene.write(cls.mid_scene) - scene = GDScene.parse(""" + scene = GDPackedScene.parse(""" [gd_scene load_steps=2 format=2] [ext_resource path="res://Mid.tscn" type="PackedScene" id=1] [sub_resource type="CircleShape2D" id=1] @@ -166,7 +165,7 @@ def tearDownClass(cls): def test_load_inherited(self): """Can load an inherited scene and read the nodes""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) with scene.use_tree() as tree: node = tree.get_node("Health/LifeBar") self.assertIsNotNone(node) @@ -174,7 +173,7 @@ def test_load_inherited(self): def test_add_new_nodes(self): """Can add new nodes to an inherited scene""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) with scene.use_tree() as tree: tree.get_node("Health/LifeBar") node = Node("NewChild", type="Control") @@ -191,7 +190,7 @@ def test_add_new_nodes(self): def test_cannot_remove(self): """Cannot remove inherited nodes""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) with scene.use_tree() as tree: node = tree.get_node("Health") self.assertRaises(TreeMutationException, node.remove_from_parent) @@ -202,7 +201,7 @@ def test_cannot_remove(self): def test_cannot_mutate(self): """Cannot change the name/type/instance of inherited nodes""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) def change_name(x): x.name = "foo" @@ -221,7 +220,7 @@ def change_instance(x): def test_inherit_properties(self): """Inherited nodes inherit properties""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) with scene.use_tree() as tree: self.assertEqual(tree.root["shape"], SubResource(1)) self.assertEqual(tree.root["collision_layer"], 4) @@ -231,7 +230,7 @@ def test_inherit_properties(self): def test_unchanged_sections(self): """Inherited nodes do not appear in sections""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) num_nodes = len(scene.get_nodes()) self.assertEqual(num_nodes, 2) with scene.use_tree() as tree: @@ -243,7 +242,7 @@ def test_unchanged_sections(self): def test_overwrite_sections(self): """Inherited nodes appear in sections if we change their configuration""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) with scene.use_tree() as tree: node = tree.get_node("Health/LifeBar") node["pause_mode"] = 2 @@ -254,7 +253,7 @@ def test_overwrite_sections(self): def test_disappear_sections(self): """Inherited nodes are removed from sections if we change their configuration to match parent""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) with scene.use_tree() as tree: sprite = tree.get_node("Sprite") sprite["flip_h"] = False @@ -272,20 +271,20 @@ def test_find_project_root(self): def test_invalid_tree(self): """Raise exception when tree is invalid""" - scene = GDScene() + scene = GDPackedScene() scene.add_node("RootNode") scene.add_node("Child", parent="Missing") self.assertRaises(TreeMutationException, lambda: scene.get_node("Child")) def test_missing_root(self): """Raise exception when GDScene is inherited but missing project_root""" - scene = GDScene() + scene = GDPackedScene() scene.add_ext_node("Root", 1) self.assertRaises(RuntimeError, lambda: scene.get_node("Root")) def test_missing_ext_resource(self): """Raise exception when GDScene is inherited but ext_resource is missing""" - scene = GDScene.load(self.leaf_scene) + scene = GDPackedScene.load(self.leaf_scene) for section in scene.get_ext_resources(): scene.remove_section(section) self.assertRaises(RuntimeError, lambda: scene.get_node("Root")) diff --git a/tests/test_versions.py b/tests/test_versions.py new file mode 100644 index 0000000..2112d49 --- /dev/null +++ b/tests/test_versions.py @@ -0,0 +1,279 @@ +import base64 +import os +import unittest + +import tests +from godot_parser import GDPackedScene, NodePath, StringName, TypedArray, parse +from godot_parser.id_generator import SequentialHexGenerator +from godot_parser.objects import ( + PackedByteArray, + PackedVector4Array, + TypedDictionary, + Vector4, +) +from godot_parser.output import VersionOutputFormat + + +class TestVersionOutput(unittest.TestCase): + def setUp(self): + self.scene = GDPackedScene() + root = self.scene.add_node("Root", "Node3D") + child = self.scene.add_node("Child", "Node", ".") + + ext1 = self.scene.add_ext_resource("res://external.tres", "CustomResource1") + root["resource1"] = ext1.reference + + extScript = self.scene.add_ext_resource("res://custom_resource.gd", "Script") + extScript2 = self.scene.add_ext_resource("res://custom_resource_2.gd", "Script") + + sub1 = self.scene.add_sub_resource("Resource") + sub1["script"] = extScript.reference + + sub2 = self.scene.add_sub_resource("Resource") + sub2["script"] = extScript.reference + + sub3 = self.scene.add_sub_resource("Resource") + sub3["script"] = extScript2.reference + + root["resourceArray"] = TypedArray( + extScript.reference, [sub1.reference, sub2.reference] + ) + root["typedDict"] = TypedDictionary( + "StringName", + extScript2.reference, + {StringName("Two"): sub3.reference}, + ) + + byte_array = bytes([10, 20, 15]) + self.byte_array_base64 = base64.b64encode(byte_array).decode("utf-8") + + child["str"] = "'hello\\me'\n\"HI\"" + child["string name"] = StringName("StringName") + child["packedByte"] = PackedByteArray.FromBytes(byte_array) + child["packedVector4"] = PackedVector4Array.FromList([Vector4(1, 3, 5, 7)]) + child["nodepath"] = NodePath(".") + + def test_godot_3(self): + self.assertEqual( + self.scene.output_to_string(VersionOutputFormat("3.6")), + """[gd_scene load_steps=7 format=2] + +[ext_resource path="res://external.tres" type="CustomResource1" id=1] +[ext_resource path="res://custom_resource.gd" type="Script" id=2] +[ext_resource path="res://custom_resource_2.gd" type="Script" id=3] + +[sub_resource type="Resource" id=1] +script = ExtResource( 2 ) + +[sub_resource type="Resource" id=2] +script = ExtResource( 2 ) + +[sub_resource type="Resource" id=3] +script = ExtResource( 3 ) + +[node name="Root" type="Node3D"] +resource1 = ExtResource( 1 ) +resourceArray = [ SubResource( 1 ), SubResource( 2 ) ] +typedDict = { +"Two": SubResource( 3 ) +} + +[node name="Child" type="Node" parent="."] +str = "'hello\\\\me' +\\\"HI\\\"" +"string name" = "StringName" +packedByte = PoolByteArray( 10, 20, 15 ) +packedVector4 = [ Vector4( 1, 3, 5, 7 ) ] +nodepath = NodePath(".") +""", + ) + + def test_godot_4_0(self): + output_format = VersionOutputFormat("4.0") + output_format._id_generator = SequentialHexGenerator() + + self.assertEqual( + self.scene.output_to_string(output_format), + """[gd_scene load_steps=7 format=3] + +[ext_resource path="res://external.tres" type="CustomResource1" id="1_1"] +[ext_resource path="res://custom_resource.gd" type="Script" id="2_2"] +[ext_resource path="res://custom_resource_2.gd" type="Script" id="3_3"] + +[sub_resource type="Resource" id="Resource_4"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_5"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_6"] +script = ExtResource("3_3") + +[node name="Root" type="Node3D"] +resource1 = ExtResource("1_1") +resourceArray = Array[ExtResource("2_2")]([SubResource("Resource_4"), SubResource("Resource_5")]) +typedDict = { +&"Two": SubResource("Resource_6") +} + +[node name="Child" type="Node" parent="."] +str = "'hello\\\\me' +\\\"HI\\\"" +"string name" = &"StringName" +packedByte = PackedByteArray(10, 20, 15) +packedVector4 = Array[Vector4]([Vector4(1, 3, 5, 7)]) +nodepath = NodePath(".") +""", + ) + + def test_godot_4_3(self): + output_format = VersionOutputFormat("4.3") + output_format._id_generator = SequentialHexGenerator() + + self.assertEqual( + self.scene.output_to_string(output_format), + """[gd_scene load_steps=7 format=4] + +[ext_resource path="res://external.tres" type="CustomResource1" id="1_1"] +[ext_resource path="res://custom_resource.gd" type="Script" id="2_2"] +[ext_resource path="res://custom_resource_2.gd" type="Script" id="3_3"] + +[sub_resource type="Resource" id="Resource_4"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_5"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_6"] +script = ExtResource("3_3") + +[node name="Root" type="Node3D"] +resource1 = ExtResource("1_1") +resourceArray = Array[ExtResource("2_2")]([SubResource("Resource_4"), SubResource("Resource_5")]) +typedDict = { +&"Two": SubResource("Resource_6") +} + +[node name="Child" type="Node" parent="."] +str = "'hello\\\\me' +\\\"HI\\\"" +"string name" = &"StringName" +packedByte = PackedByteArray("%s") +packedVector4 = PackedVector4Array(1, 3, 5, 7) +nodepath = NodePath(".") +""" % self.byte_array_base64, + ) + + def test_godot_4_4(self): + output_format = VersionOutputFormat("4.4") + output_format._id_generator = SequentialHexGenerator() + + self.assertEqual( + self.scene.output_to_string(output_format), + """[gd_scene load_steps=7 format=4] + +[ext_resource path="res://external.tres" type="CustomResource1" id="1_1"] +[ext_resource path="res://custom_resource.gd" type="Script" id="2_2"] +[ext_resource path="res://custom_resource_2.gd" type="Script" id="3_3"] + +[sub_resource type="Resource" id="Resource_4"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_5"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_6"] +script = ExtResource("3_3") + +[node name="Root" type="Node3D"] +resource1 = ExtResource("1_1") +resourceArray = Array[ExtResource("2_2")]([SubResource("Resource_4"), SubResource("Resource_5")]) +typedDict = Dictionary[StringName, ExtResource("3_3")]({ +&"Two": SubResource("Resource_6") +}) + +[node name="Child" type="Node" parent="."] +str = "'hello\\\\me' +\\\"HI\\\"" +"string name" = &"StringName" +packedByte = PackedByteArray("%s") +packedVector4 = PackedVector4Array(1, 3, 5, 7) +nodepath = NodePath(".") +""" % self.byte_array_base64, + ) + + def test_godot_4_6(self): + output_format = VersionOutputFormat("4.6") + output_format._id_generator = SequentialHexGenerator() + + self.assertEqual( + self.scene.output_to_string(output_format), + """[gd_scene format=4] + +[ext_resource path="res://external.tres" type="CustomResource1" id="1_1"] +[ext_resource path="res://custom_resource.gd" type="Script" id="2_2"] +[ext_resource path="res://custom_resource_2.gd" type="Script" id="3_3"] + +[sub_resource type="Resource" id="Resource_4"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_5"] +script = ExtResource("2_2") + +[sub_resource type="Resource" id="Resource_6"] +script = ExtResource("3_3") + +[node name="Root" type="Node3D"] +resource1 = ExtResource("1_1") +resourceArray = Array[ExtResource("2_2")]([SubResource("Resource_4"), SubResource("Resource_5")]) +typedDict = Dictionary[StringName, ExtResource("3_3")]({ +&"Two": SubResource("Resource_6") +}) + +[node name="Child" type="Node" parent="."] +str = "'hello\\\\me' +\\\"HI\\\"" +"string name" = &"StringName" +packedByte = PackedByteArray("%s") +packedVector4 = PackedVector4Array(1, 3, 5, 7) +nodepath = NodePath(".") +""" % self.byte_array_base64, + ) + + +class TestRealProject(unittest.TestCase): + def test_projects(self): + project_folder = os.path.join(os.path.dirname(tests.__file__), "projects") + for root, _dirs, files in os.walk(project_folder, topdown=False): + for file in files: + ext = os.path.splitext(file)[1] + if ext not in [".tscn", ".tres"]: + continue + filepath = os.path.join(root, file) + version, filename = os.path.split( + os.path.relpath(filepath, project_folder) + ) + + with self.subTest("%s - %s" % (version, filename)): + output_format = VersionOutputFormat(version) + + # Required as Godot uses format=4 not if the tres contains a PackedVector4Array or + # base64 PackedByteArray, but if the originating script contains any such properties + # + # Since we have no access to the original script, we hack this with the prior knowledge that the + # files used for this test contained those types + output_format._force_format_4_if_available = True + + with open(filepath, "r", encoding="utf-8") as input_file: + file_content = input_file.read() + parsed_file = parse(file_content) + self.assertEqual( + file_content, + parsed_file.output_to_string(output_format), + ) + self.assertEqual( + file_content, + parsed_file.output_to_string( + VersionOutputFormat.guess_version(parsed_file) + ), + )