Skip to content

Commit

Permalink
Merge branch 'NickHugi/master' into mdl-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
th3w1zard1 committed Mar 14, 2024
2 parents c833fa2 + aa9cd4e commit 0c32a32
Show file tree
Hide file tree
Showing 79 changed files with 15,364 additions and 13,704 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
exclude = [".git", "__pycache__", "dist"]
max-line-length = 130
max-line-length = 175
max-complexity = 25
347 changes: 265 additions & 82 deletions .github/workflows/publish_and_test_pykotor.yml

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"./Libraries/PyKotorFont/src",
"./Libraries/Utility/src",
],
"remote.WSL.fileWatcher.polling": true,
"files.watcherExclude": {
"**/.git/**": true
},
"python.testing.autoTestDiscoverOnSaveEnabled": true,
"python.testing.promptToConfigure": true,
"python.testing.pytestEnabled": false,
Expand All @@ -48,7 +52,7 @@
"python.analysis.diagnosticSeverityOverrides": {
"reportGeneralTypeIssues": "warning",
"reportOptionalMemberAccess": "warning", // Ignores issues with defs that can optionally be None.
"reportCallIssue": "none",
"reportCallIssue": "warning",
"reportAssignmentType": "warning",
"reportArgumentType": "warning",
},
Expand Down
24 changes: 18 additions & 6 deletions Libraries/PyKotor/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,42 @@ def main():
# Extract project metadata
project_metadata: dict[str, Any] = setup_params.get("project", {})
build_system: dict[str, dict] = setup_params.get("build-system", {})
AUTHORS: list[dict[str, str]] = project_metadata.get("authors", [{"name": ""}])
AUTHORS: list[dict[str, str]] = project_metadata.get("authors", [{"name": "", "email": ""}])
MAINTAINERS: list[dict[str, str]] = project_metadata.get("maintainers", [{"name": "", "email": ""}])
README: dict[str, str] = project_metadata.get("readme", {"file": "", "content-type": ""})

# Extract and extend requirements
REQUIREMENTS = {*build_system.get("requires", [])}
requirements_txt_path = HERE.joinpath("requirements.txt")
if requirements_txt_path.exists():
REQUIREMENTS.update(requirements_txt_path.read_text().splitlines())
REQUIREMENTS.update(project_metadata["dependencies"])

# Check if the installation is from PyPI or local source
if len(sys.argv) < 2:
sys.argv.append("install")

for key in ("authors", "readme"): # Remove keys that are not needed in setup()
for key in ["authors", "readme", "maintainers"]: # Remove keys that are not needed in setup()
if key in project_metadata:
project_metadata.pop(key)

EXTRA_PATHS = []
utility_path = HERE.joinpath("..", "Utility")
utility_src_path = utility_path / "src"
if utility_src_path.exists():
EXTRA_PATHS.append(str(utility_src_path))

EXTRAS_REQUIRE = project_metadata.get("optional-dependencies", {})

setup(
**project_metadata,
author=AUTHORS[0]["name"],
#author_email=AUTHORS[0]["email"],
maintainer=MAINTAINERS[0]["name"],
maintainer_email=MAINTAINERS[0]["email"],
install_requires=list(REQUIREMENTS),
extras_require=EXTRAS_REQUIRE,
long_description=README["file"],
long_description_content_type=README["content-type"],
include_dirs=[str(HERE)],
include_dirs=EXTRA_PATHS,
package_dir={"": "src"},
)


Expand Down
5 changes: 4 additions & 1 deletion Libraries/PyKotor/src/pykotor/common/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@

from __future__ import annotations

from collections.abc import Mapping
from enum import Enum, IntEnum
from typing import TYPE_CHECKING, ClassVar, Generic, ItemsView, Iterable, Iterator, Mapping, TypeVar, overload
from typing import TYPE_CHECKING, ClassVar, Generic, ItemsView, Iterable, Iterator, TypeVar, overload

from pykotor.common.geometry import Vector3

if TYPE_CHECKING:
import os

from collections.abc import ItemsView, Iterable, Iterator

T = TypeVar("T")
VT = TypeVar("VT")
_unique_sentinel = object()
Expand Down
6 changes: 6 additions & 0 deletions Libraries/PyKotor/src/pykotor/common/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def __init__(

self.reload_resources()

def get_id(self) -> str:
return self._root

@staticmethod
def get_root(
filepath: os.PathLike | str,
Expand Down Expand Up @@ -1121,6 +1124,9 @@ def restype(self) -> ResourceType:
"""
return self._restype

def identifier(self) -> ResourceIdentifier:
return self._identifier

def localized_name(self) -> str | None:
# sourcery skip: assign-if-exp, reintroduce-else
"""Returns a localized name for the resource.
Expand Down
4 changes: 2 additions & 2 deletions Libraries/PyKotor/src/pykotor/extract/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def _find_resource_folderpath(
self,
folder_names: tuple[str, ...] | str,
*,
optional: bool = False,
optional: bool = True,
) -> CaseAwarePath:
"""Finds the path to a resource folder.
Expand Down Expand Up @@ -373,7 +373,7 @@ def load_single_resource(

resname: str
restype: ResourceType
resname, restype = ResourceIdentifier.from_path(filepath)
resname, restype = ResourceIdentifier.from_path(filepath).unpack()
if restype.is_invalid:
return filepath, None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def compare(self, other: LIP, log_func=print) -> bool:

return ret

# mapping in LipSyncEditor is not very accurate. for example, shape 0 is neutral, not EE,
# mapping in LipSyncEditor is not very accurate. for example, shape 0 is neutral, not EE,
# meaning pauses are not properly represented when using LipSyncEditor. shape 15 is too open to be KG and etc.
# https://imgur.com/a/LIRZ8B1
class LIPShape(IntEnum):
Expand Down
10 changes: 6 additions & 4 deletions Libraries/PyKotor/src/pykotor/resource/formats/tpc/io_tga.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,10 @@ def load(
elif datatype_code == _DataTypes.UNCOMPRESSED_BLACK_WHITE:
data = bytearray()
for _ in range(width * height):
# Read the grayscale value (assuming 1 byte per pixel)
# Read the grayscale value (should be 1 byte per pixel)
gray_value = self._reader.read_uint8()

# Convert grayscale to RGBA (R=G=B=gray_value, A=255)
data.extend([gray_value, gray_value, gray_value, 255])
# Convert grayscale to RGB
data.extend([gray_value, gray_value, gray_value])
elif datatype_code == _DataTypes.RLE_COLOR_MAPPED:
if color_map is None:
msg = "Expected color map not found for RLE color-mapped data"
Expand All @@ -244,6 +243,9 @@ def load(
texture_format = TPCTextureFormat.RGBA if bits_per_pixel == 32 else TPCTextureFormat.RGB
self._tpc.set_data(width, height, [bytes(data)], texture_format)

datacode_name = next((c.name for c in _DataTypes if c.value == datatype_code), _DataTypes.NO_IMAGE_DATA.name)
print("tga datatype_code:", datacode_name, "y_flipped:", y_flipped, "bits_per_pixel:", bits_per_pixel)
self._tpc.original_datatype_code = _DataTypes.__members__[datacode_name]
return self._tpc


Expand Down
28 changes: 23 additions & 5 deletions Libraries/PyKotor/src/pykotor/resource/formats/tpc/tpc_auto.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import os

from typing import TYPE_CHECKING

from pykotor.common.stream import BinaryReader
Expand Down Expand Up @@ -73,6 +75,7 @@ def read_tpc(
source: SOURCE_TYPES,
offset: int = 0,
size: int | None = None,
txi_source: SOURCE_TYPES | None = None,
) -> TPC:
"""Returns an TPC instance from the source.
Expand All @@ -83,6 +86,7 @@ def read_tpc(
source: The source of the data.
offset: The byte offset of the file inside the data.
size: Number of bytes to allowed to read from the stream. If not specified, uses the whole stream.
txi_source: An optional source to the TXI data to use. If this is a filepath, it *must* exist on disk.
Raises:
------
Expand All @@ -98,11 +102,25 @@ def read_tpc(
file_format: ResourceType = detect_tpc(source, offset)

if file_format == ResourceType.TPC:
return TPCBinaryReader(source, offset, size or 0).load()
if file_format == ResourceType.TGA:
return TPCTGAReader(source, offset, size or 0).load()
msg = "Failed to determine the format of the GFF file."
raise ValueError(msg)
loaded_tpc = TPCBinaryReader(source, offset, size or 0).load()
elif file_format == ResourceType.TGA:
loaded_tpc = TPCTGAReader(source, offset, size or 0).load()
else:
msg = "Failed to determine the format of the TPC/TGA file."
raise ValueError(msg)
if txi_source is None and isinstance(source, (os.PathLike, str)):
txi_source = CaseAwarePath.pathify(source).with_suffix(".txi")
if not txi_source.safe_isfile():
return loaded_tpc

elif isinstance(txi_source, (os.PathLike, str)):
txi_source = CaseAwarePath.pathify(txi_source).with_suffix(".txi")

if txi_source is None:
return loaded_tpc
with BinaryReader.from_auto(txi_source) as f:
loaded_tpc.txi = f.read_all().decode(encoding="ascii", errors="ignore")
return loaded_tpc


def write_tpc(
Expand Down
63 changes: 57 additions & 6 deletions Libraries/PyKotor/src/pykotor/resource/formats/tpc/tpc_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TPCGetResult(NamedTuple):
width: int
height: int
texture_format: TPCTextureFormat
data: bytes | None
data: bytes


class TPCConvertResult(NamedTuple):
Expand All @@ -42,6 +42,8 @@ def __init__(
self._width: int = 4
self._height: int = 4
self.txi: str = ""
from pykotor.resource.formats.tpc.io_tga import _DataTypes
self.original_datatype_code: _DataTypes = _DataTypes.NO_IMAGE_DATA

# TODO: cube maps

Expand Down Expand Up @@ -99,6 +101,7 @@ def convert(
self,
convert_format: TPCTextureFormat,
mipmap: int = 0,
y_flip: bool | None = None,
) -> TPCConvertResult:
"""Returns a tuple containing the width, height and data of the specified mipmap where the data returned is in the texture format specified.
Expand All @@ -111,15 +114,22 @@ def convert(
-------
A tuple equal to (width, height, data)
"""
if convert_format in {TPCTextureFormat.DXT1, TPCTextureFormat.DXT5}:
msg = f"Conversion from {self._texture_format} to {convert_format} not implemented."
raise NotImplementedError(msg)

width, height = self._mipmap_size(mipmap)
raw_data: bytes = self._mipmaps[mipmap]
if self._texture_format == convert_format: # Is conversion needed?
if self._texture_format == convert_format and not y_flip: # Is conversion needed?
return TPCConvertResult(width, height, bytearray(raw_data))

if y_flip:
bytes_per_pixel = 0
if self._texture_format == TPCTextureFormat.Greyscale:
bytes_per_pixel = 1
elif self._texture_format in {TPCTextureFormat.RGB, TPCTextureFormat.RGBA}:
bytes_per_pixel = 4 if self._texture_format == TPCTextureFormat.RGBA else 3
# If the image needs to be flipped and it's an uncompressed format
if bytes_per_pixel > 0:
raw_data = bytearray(self.flip_image_data(raw_data, width, height, bytes_per_pixel))
y_flip = False

data: bytearray = bytearray(raw_data)
if convert_format == TPCTextureFormat.Greyscale:
if self._texture_format == TPCTextureFormat.DXT5:
Expand Down Expand Up @@ -155,8 +165,41 @@ def convert(
rgba_data = TPC._grey_to_rgba(raw_data, width, height)
data = TPC._rgba_to_rgb(rgba_data, width, height)

if y_flip:
bytes_per_pixel = 0
if convert_format == TPCTextureFormat.Greyscale:
bytes_per_pixel = 1
elif convert_format in {TPCTextureFormat.RGB, TPCTextureFormat.RGBA}:
bytes_per_pixel = 4 if convert_format == TPCTextureFormat.RGBA else 3

# If the image needs to be flipped and it's an uncompressed format
if bytes_per_pixel > 0:
data = bytearray(self.flip_image_data(data, width, height, bytes_per_pixel))

return TPCConvertResult(width, height, data)

def get_bytes_per_pixel(self):
bytes_per_pixel = 0
if self._texture_format == TPCTextureFormat.Greyscale:
bytes_per_pixel = 1
elif self._texture_format in {TPCTextureFormat.RGB, TPCTextureFormat.RGBA}:
bytes_per_pixel = 4 if self._texture_format == TPCTextureFormat.RGBA else 3
return bytes_per_pixel

@staticmethod
def flip_image_data(data: bytes | bytearray, width: int, height: int, bytes_per_pixel: int) -> bytes:
"""Flip the image data vertically."""
flipped_data = bytearray(len(data))
row_length = width * bytes_per_pixel

for row in range(height):
source_start = row * row_length
source_end = source_start + row_length
dest_start = (height - 1 - row) * row_length
flipped_data[dest_start:dest_start + row_length] = data[source_start:source_end]

return bytes(flipped_data)

def set_single(
self,
width: int,
Expand Down Expand Up @@ -736,3 +779,11 @@ class TPCTextureFormat(IntEnum):
RGBA = 2
DXT1 = 3
DXT5 = 4

def bytes_per_pixel(self):
bytes_per_pixel = 0
if self == TPCTextureFormat.Greyscale:
bytes_per_pixel = 1
elif self in {TPCTextureFormat.RGB, TPCTextureFormat.RGBA}:
bytes_per_pixel = 4 if self == TPCTextureFormat.RGBA else 3
return bytes_per_pixel
Loading

0 comments on commit 0c32a32

Please sign in to comment.