diff --git a/mos6502/bitarray_factory.py b/mos6502/bitarray_factory.py index 476e58e..c60daa7 100644 --- a/mos6502/bitarray_factory.py +++ b/mos6502/bitarray_factory.py @@ -22,7 +22,7 @@ import os import sys -from typing import Literal +from mos6502.compat import Literal, Union # Auto-detect PyPy and force pure-Python mode # PyPy's JIT compiler works much better with pure Python than with C extensions @@ -30,7 +30,7 @@ os.environ.setdefault("MOS6502_PURE_PYTHON", "1") # Configuration - can be overridden before first use -_USE_NATIVE: bool | None = None # None means auto-detect +_USE_NATIVE: Union[bool, None]= None # None means auto-detect _NATIVE_AVAILABLE: bool = False # Try to import native bitarray @@ -71,7 +71,7 @@ def _should_use_native() -> bool: return _NATIVE_AVAILABLE -def configure(*, use_native: bool | None = None) -> None: +def configure(use_native: Union[bool, None]= None) -> None: """Configure which bitarray implementation to use. Args: @@ -131,10 +131,10 @@ def is_bitarray(obj: object) -> bool: class _LazyBitarray: """Lazy loader that selects implementation on first use.""" - def __new__(cls, *args, **kwargs): # noqa: ANN204, ANN002, ANN003 + def __new__(cls, initializer=0, length=None, endian="little"): # noqa: ANN204 if _should_use_native(): - return _native_bitarray(*args, **kwargs) - return _pybitarray(*args, **kwargs) + return _native_bitarray(initializer, length, endian) + return _pybitarray(initializer, length, endian) def int2ba( @@ -156,8 +156,8 @@ def int2ba( """ if _should_use_native(): - return _native_int2ba(value, length=length, endian=endian) - return _py_int2ba(value, length=length, endian=endian) + return _native_int2ba(value, length, endian) + return _py_int2ba(value, length, endian) def ba2int(ba) -> int: # noqa: ANN001 diff --git a/mos6502/compat.py b/mos6502/compat.py new file mode 100644 index 0000000..d77be96 --- /dev/null +++ b/mos6502/compat.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python3 +"""Compatibility module for CPython and MicroPython. + +This module provides compatibility shims for modules that are available +in CPython but not in MicroPython. +""" + +# typing compatibility - must come first as other modules may need it +try: + from typing import ( + TYPE_CHECKING, + Optional, + List, + Dict, + Tuple, + Union, + Callable, + Protocol, + NamedTuple, + Literal, + NoReturn, + Any, + ) +except ImportError: + # MicroPython stub for typing + # TYPE_CHECKING is False at runtime, so conditional imports won't execute + TYPE_CHECKING = False + + # Generic type hints need to support [] subscript syntax + # We create a class that returns itself when subscripted + class _TypingStub: + """A type stub that can be subscripted with [].""" + def __getitem__(self, item): + return self + + def __call__(self, *args): + return self + + # Create instances for each typing construct + Optional = _TypingStub() + List = _TypingStub() + Dict = _TypingStub() + Tuple = _TypingStub() + Union = _TypingStub() + Callable = _TypingStub() + Literal = _TypingStub() + NoReturn = _TypingStub() + Any = _TypingStub() + + # Protocol is just a base class for structural subtyping + class Protocol: + pass + + # NamedTuple - For MicroPython, create actual namedtuples using collections.namedtuple + # The class-based syntax (class Foo(NamedTuple): field: type) doesn't work in MicroPython, + # so we provide a simple tuple base class as a fallback + try: + from collections import namedtuple as _namedtuple + # MicroPython has namedtuple, but the class inheritance syntax doesn't work + # Just use tuple as base - classes will work but lose field names + NamedTuple = tuple + except ImportError: + # No namedtuple available - just use tuple + NamedTuple = tuple + + +# contextlib compatibility +try: + import contextlib # CPython +except ImportError: + import ucontextlib as contextlib # MicroPython + +# logging compatibility +try: + import logging +except ImportError: + # MicroPython stub for logging using print + import sys as _sys + + # Global log level - default to WARNING to suppress DEBUG/INFO spam + _LOG_LEVEL = 30 # WARNING + + class _PrintLogger: + """Simple logger that uses print() for MicroPython.""" + + def __init__(self, name=None): + self._name = name or "root" + self._level = None # Use global level if not set + + def setLevel(self, level): + """Set logger-specific level.""" + self._level = level + + def _get_level(self): + """Get effective log level.""" + return self._level if self._level is not None else _LOG_LEVEL + + def debug(self, msg, *args): + if self._get_level() <= 10: + print(f"[DEBUG] {self._name}: {msg}") + + def info(self, msg, *args): + if self._get_level() <= 20: + print(f"[INFO] {self._name}: {msg}") + + def warning(self, msg, *args): + if self._get_level() <= 30: + print(f"[WARN] {self._name}: {msg}") + + def error(self, msg, *args): + if self._get_level() <= 40: + print(f"[ERROR] {self._name}: {msg}") + + def critical(self, msg, *args): + if self._get_level() <= 50: + print(f"[CRIT] {self._name}: {msg}") + + class logging: # noqa: N801 + """Stub logging module for MicroPython using print.""" + + # Log level constants + DEBUG = 10 + INFO = 20 + WARNING = 30 + ERROR = 40 + CRITICAL = 50 + + Logger = _PrintLogger + + @staticmethod + def getLogger(name=None): + return _PrintLogger(name) + + @staticmethod + def basicConfig(level=None): + """Set global log level for MicroPython.""" + global _LOG_LEVEL + if level is not None: + _LOG_LEVEL = level + + +# enum compatibility +try: + import enum + from enum import Enum, IntEnum, auto +except ImportError: + # MicroPython stub for enum - minimal implementation + # Enum members are just their raw values (int or str) + # This is simpler but loses .name/.value properties + + _auto_value = 0 + + def auto(): + """Return an auto-incrementing integer value.""" + global _auto_value + _auto_value += 1 + return _auto_value + + class Enum: + """Minimal Enum base class for MicroPython. + + Subclass attributes become enum members as their raw values. + """ + pass + + class IntEnum(int): + """Minimal IntEnum base class for MicroPython. + + Subclass attributes become enum members as integers. + """ + pass + + # Create a stub enum module + class enum: # noqa: N801 + Enum = Enum + IntEnum = IntEnum + auto = staticmethod(auto) + + +# dataclasses compatibility +try: + from dataclasses import dataclass, field + + def make_dataclass(frozen, slots): + """Create a dataclass decorator with positional args (works on both CPython and MicroPython). + + Usage: @make_dataclass(True, True) instead of @dataclass(frozen=True, slots=True) + This syntax avoids kwargs which don't work in MicroPython frozen modules. + """ + def decorator(cls): + return dataclass(cls, frozen=frozen, slots=slots) + return decorator + +except ImportError: + # MicroPython stub for dataclasses - generates __init__ from __annotations__ + + class _MISSING: + """Sentinel for missing default values.""" + pass + + MISSING = _MISSING() + + class _Field: + """Represents a dataclass field with default value info.""" + __slots__ = ('default', 'default_factory') + + def __init__(self, default=MISSING, default_factory=MISSING): + self.default = default + self.default_factory = default_factory + + def field(default=MISSING, default_factory=MISSING): # noqa: N802 + """Create a field descriptor for dataclasses.""" + return _Field(default, default_factory) + + def make_dataclass(frozen, slots): # noqa: N802 + """Create a dataclass decorator with positional args (for MicroPython frozen modules). + + Usage: @make_dataclass(True, True) instead of @dataclass(frozen=True, slots=True) + """ + def decorator(cls): + return dataclass(cls, frozen, slots) + return decorator + + def dataclass(cls=None, frozen=False, slots=False): # noqa: N802 + """Minimal dataclass decorator for MicroPython. + + Generates __init__ from __annotations__. Supports default values. + Note: MicroPython may not fully support __annotations__, so we also + check __slots__ as a fallback for field names. + """ + def wrapper(cls): + # Get field names - try multiple sources for MicroPython compatibility + # Priority: _field_order_ (explicit) > __annotations__ > __slots__ + if hasattr(cls, '_field_order_'): + field_names = list(cls._field_order_) + else: + annotations = getattr(cls, '__annotations__', {}) + field_names = list(annotations.keys()) + # MicroPython fallback: if no annotations, try __slots__ + if not field_names and hasattr(cls, '__slots__'): + field_names = list(cls.__slots__) + + # Get defaults - try _field_defaults_ first, then class attributes + defaults = {} + if hasattr(cls, '_field_defaults_'): + defaults.update(cls._field_defaults_) + + for name in field_names: + if name not in defaults and hasattr(cls, name): + val = getattr(cls, name) + if isinstance(val, _Field): + if val.default is not MISSING: + defaults[name] = val.default + elif val.default_factory is not MISSING: + defaults[name] = val.default_factory + elif not callable(val) and not isinstance(val, (tuple, type)): + # Skip class methods, tuples (like _field_order_), and types + defaults[name] = val + + # Build __init__ method - positional args only for MicroPython frozen compatibility + def make_init(field_names, defaults): + def __init__(self, *args): + # Assign positional args first + for i, val in enumerate(args): + if i < len(field_names): + setattr(self, field_names[i], val) + + # Fill in defaults for remaining fields + for i in range(len(args), len(field_names)): + name = field_names[i] + if name in defaults: + default = defaults[name] + if callable(default): + setattr(self, name, default()) + else: + setattr(self, name, default) + + # Call __post_init__ if it exists + if hasattr(self, '__post_init__'): + self.__post_init__() + + return __init__ + + cls.__init__ = make_init(field_names, defaults) + + # Add __slots__ if requested (won't work retroactively, but marks intent) + if slots and not hasattr(cls, '__slots__'): + cls.__slots__ = tuple(field_names) + + return cls + + if cls is None: + return wrapper + return wrapper(cls) + + +# collections.abc compatibility +try: + from collections.abc import MutableSequence +except ImportError: + # MicroPython stub - just use list as base + MutableSequence = list + + +def import_module(name, package=None): + """Import a module by name, compatible with both CPython and MicroPython. + + Arguments: + --------- + name: Module name (can be relative like ".module_name") + package: Package name for relative imports + + Returns: + ------- + The imported module + """ + if name.startswith('.'): + # Relative import - construct full module path + full_name = package + name + else: + full_name = name + + # Use __import__ which works on both CPython and MicroPython + # For "a.b.c", __import__ returns "a", so we need to traverse + parts = full_name.split('.') + module = __import__(full_name) + for part in parts[1:]: + module = getattr(module, part) + return module + + +# abc compatibility +try: + from abc import ABC, abstractmethod +except ImportError: + # MicroPython stub for abc + class ABC: + """Abstract Base Class stub for MicroPython.""" + pass + + def abstractmethod(func): + """Abstract method decorator stub - just returns the function.""" + return func + + +# pathlib compatibility +try: + from pathlib import Path +except ImportError: + # Minimal Path stub for MicroPython + import os + + class Path: + """Minimal pathlib.Path implementation for MicroPython.""" + + __slots__ = ('_path',) + + def __init__(self, path): + self._path = str(path) + + def __str__(self): + return self._path + + def __repr__(self): + return f"Path({self._path!r})" + + def __truediv__(self, other): + return Path(self._path + "/" + str(other)) + + def __eq__(self, other): + if isinstance(other, Path): + return self._path == other._path + return self._path == str(other) + + def __hash__(self): + return hash(self._path) + + def exists(self): + try: + os.stat(self._path) + return True + except OSError: + return False + + def is_file(self): + try: + return (os.stat(self._path)[0] & 0x8000) != 0 + except OSError: + return False + + def is_dir(self): + try: + return (os.stat(self._path)[0] & 0x4000) != 0 + except OSError: + return False + + def read_bytes(self): + with open(self._path, "rb") as f: + return f.read() + + def read_text(self, encoding="utf-8"): + with open(self._path, "r") as f: + return f.read() + + def write_bytes(self, data): + with open(self._path, "wb") as f: + f.write(data) + + def write_text(self, data, encoding="utf-8"): + with open(self._path, "w") as f: + f.write(data) + + @property + def name(self): + return self._path.rstrip("/").split("/")[-1] + + @property + def stem(self): + name = self.name + if "." in name: + return name.rsplit(".", 1)[0] + return name + + @property + def suffix(self): + name = self.name + if "." in name: + return "." + name.rsplit(".", 1)[1] + return "" + + @property + def parent(self): + parts = self._path.rstrip("/").split("/") + if len(parts) <= 1: + return Path(".") + return Path("/".join(parts[:-1])) + + def joinpath(self, *args): + result = self + for arg in args: + result = result / arg + return result + + def iterdir(self): + for name in os.listdir(self._path): + yield Path(self._path + "/" + name) + + def glob(self, pattern): + # Very basic glob - only supports "*" at the end + if pattern.endswith("*"): + prefix = pattern[:-1] + for item in self.iterdir(): + if item.name.startswith(prefix): + yield item + else: + # Exact match + child = self / pattern + if child.exists(): + yield child diff --git a/mos6502/core.py b/mos6502/core.py index f68017f..bcf0f97 100755 --- a/mos6502/core.py +++ b/mos6502/core.py @@ -1,10 +1,7 @@ #!/usr/bin/env python3 """CPU core for the mos6502.""" -import contextlib -import importlib -import logging -from typing import TYPE_CHECKING, Callable, Literal, Self +from mos6502.compat import contextlib, logging, import_module, TYPE_CHECKING, Callable, Literal, Union, Dict, Tuple from mos6502.bitarray_factory import ba2int @@ -51,6 +48,7 @@ class MOS6502CPU(flags.ProcessorStatusFlagsInterface): __slots__ = ( '_variant', + '_preallocated_ram', 'verbose_cycles', 'endianness', '_registers', @@ -79,10 +77,11 @@ class MOS6502CPU(flags.ProcessorStatusFlagsInterface): log: logging.Logger = logging.getLogger("mos6502.cpu") def __init__( - self: Self, - cpu_variant: str | variants.CPUVariant = variants.CPUVariant.NMOS_6502, + self, + cpu_variant: Union[str, variants.CPUVariant]= variants.CPUVariant.NMOS_6502, verbose_cycles: bool = False, - ) -> Self: + preallocated_ram: bytearray = None, + ) -> "MOS6502CPU": """Instantiate a mos6502 CPU core. Arguments: @@ -93,7 +92,11 @@ def __init__( Defaults to NMOS 6502 for backward compatibility. verbose_cycles: If True, emit per-cycle log messages (slow). Defaults to False for performance. + preallocated_ram: Optional pre-allocated 64KB bytearray to use for RAM. + Useful on memory-constrained systems like Pico where we need to + allocate before imports fragment the heap. """ + self._preallocated_ram = preallocated_ram super().__init__() # Handle variant parameter @@ -104,9 +107,11 @@ def __init__( # Configuration for unstable/highly-unstable illegal opcodes # Users can override this after creation to customize chip behavior + # Use getattr for MicroPython compatibility (Enum stub returns strings, not objects with .value) + variant_value = getattr(self._variant, 'value', self._variant) self.unstable_config: variants.UnstableOpcodeConfig = variants.UNSTABLE_OPCODE_DEFAULTS.get( - self._variant.value, - variants.UnstableOpcodeConfig(ane_const=0xFF, unstable_stores_enabled=True) + variant_value, + variants.UnstableOpcodeConfig(0xFF, True) ) self.verbose_cycles: bool = verbose_cycles @@ -116,10 +121,11 @@ def __init__( # for newly created Byte/Word/etc... objects here memory.ENDIANNESS = self.endianness - self._registers: Registers = registers.Registers(endianness=self.endianness) + self._registers: "Registers" = registers.Registers(self.endianness) from mos6502.flags import FlagsRegister self._flags: FlagsRegister = FlagsRegister() - self.ram: RAM = RAM(endianness=self.endianness) + # RAM(endianness, save_state, preallocated_buffer) + self.ram: RAM = RAM(self.endianness, None, self._preallocated_ram) self.cycles = 0 self.cycles_executed: Literal[0] = 0 self.instructions_executed: int = 0 # Total instructions executed @@ -174,23 +180,23 @@ def __init__( self._opcode_handler_cache: list = self._build_opcode_handler_table() @property - def variant(self: Self) -> variants.CPUVariant: + def variant(self) -> variants.CPUVariant: """Return the CPU variant being emulated.""" return self._variant @property - def variant_name(self: Self) -> str: + def variant_name(self) -> str: """Return the CPU variant name as a string.""" return str(self._variant) # Variant handler cache: {(instruction_package_name, function_name, variant): handler} - _variant_handler_cache: dict[tuple[str, str, variants.CPUVariant], Callable[[Self], None]] = {} + _variant_handler_cache: Dict[Tuple[str, str, variants.CPUVariant], Callable[["MOS6502CPU"], None]] = {} def _load_variant_handler( - self: Self, + self, instruction_package: str, function_name: str, - ) -> Callable[[Self], None]: + ) -> Callable[["MOS6502CPU"], None]: """Dynamically load variant-specific handler for an instruction. Tries to load _.py, falls back to _6502.py if not found. @@ -216,22 +222,22 @@ def _load_variant_handler( # Try to load variant-specific module try: - module = importlib.import_module( + module = import_module( f".{instruction_name}_{variant_name}", - package=instruction_package, + instruction_package, ) except ImportError: # Fall back to _6502 (default implementation) - module = importlib.import_module( + module = import_module( f".{instruction_name}_6502", - package=instruction_package, + instruction_package, ) handler = getattr(module, function_name) self._variant_handler_cache[cache_key] = handler return handler - def _build_opcode_handler_table(self: Self) -> list: + def _build_opcode_handler_table(self) -> list: """Build a 256-entry opcode handler table for fast dispatch. Pre-populates handlers for all legal opcodes using OPCODE_LOOKUP. @@ -249,14 +255,14 @@ def _build_opcode_handler_table(self: Self) -> list: return table - def __enter__(self: Self) -> Self: + def __enter__(self) -> "MOS6502CPU": """With entrypoint.""" return self - def __exit__(self: Self, *args: list, **kwargs: dict) -> None: + def __exit__(self, exc_type, exc_val, exc_tb) -> None: """With exitpoint.""" - def tick(self: Self, cycles: int) -> int: + def tick(self, cycles: int) -> int: """ Tick {cycles} cycles. @@ -312,7 +318,7 @@ def tick(self: Self, cycles: int) -> int: return self.cycles - def spend_cpu_cycles(self: Self, cost: int) -> None: + def spend_cpu_cycles(self, cost: int) -> None: """ Tick the CPU and spend {cost} cycles. @@ -330,7 +336,7 @@ def spend_cpu_cycles(self: Self, cost: int) -> None: self.log.info("*" * cost) self.tick(cost) - def fetch_byte(self: Self) -> int: + def fetch_byte(self) -> int: """ Fetch a byte from RAM[self.PC]. @@ -347,7 +353,7 @@ def fetch_byte(self: Self) -> int: # Use explicit assignment to trigger PC setter (for pc_callback) # Setter handles 16-bit wrap via & 0xFFFF masking self.PC = self.PC + 1 - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) if self.verbose_cycles: addr = self.PC - 1 @@ -360,7 +366,7 @@ def fetch_byte(self: Self) -> int: return byte - def fetch_word(self: Self) -> int: + def fetch_word(self) -> int: """ Fetch a word from RAM[self.PC]. @@ -374,12 +380,12 @@ def fetch_word(self: Self) -> int: """ addr1: int = self.PC lowbyte: int = self.ram[self.PC] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) self.PC = self.PC + 1 addr2: int = self.PC highbyte: int = self.ram[self.PC] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) self.PC = self.PC + 1 @@ -397,7 +403,7 @@ def fetch_word(self: Self) -> int: return word - def read_byte(self: Self, address: int) -> int: + def read_byte(self, address: int) -> int: """ Read a byte from RAM at location RAM[address]. @@ -412,14 +418,14 @@ def read_byte(self: Self, address: int) -> int: int: the byte value located in memory at RAM[address] """ data: int = self.ram[address] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) if self.verbose_cycles: - memory_section = self.ram.memory_section(address=address) + memory_section = self.ram.memory_section(address) self.log.info("r") self.log.debug(f"read_byte({memory_section}[0x{address:02x}]): {data}") return data - def peek_byte(self: Self, address: Word) -> Byte: + def peek_byte(self, address: Word) -> Byte: """ Read a Byte() from RAM at location RAM[address]. @@ -436,7 +442,7 @@ def peek_byte(self: Self, address: Word) -> Byte: data: Byte = self.ram[int(address)] return data - def write_byte(self: Self, address: Word, data: Byte) -> Byte: + def write_byte(self, address: Word, data: Byte) -> Byte: """ Write a Byte() to RAM at location RAM[address]. @@ -452,7 +458,7 @@ def write_byte(self: Self, address: Word, data: Byte) -> Byte: None """ self.ram[address] = data & 0xFF - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) if self.verbose_cycles: # DEBUG: Log screen writes addr_int = int(address) if hasattr(address, '__int__') else address @@ -460,7 +466,7 @@ def write_byte(self: Self, address: Word, data: Byte) -> Byte: self.log.warning(f"*** write_byte SCREEN: addr=${addr_int:04X}, data=${data & 0xFF:02X} ***") self.log.info("w") - def read_word(self: Self, address: Word) -> int: + def read_word(self, address: Word) -> int: """ Read a 16-bit word from RAM at location RAM[address]. @@ -475,17 +481,17 @@ def read_word(self: Self, address: Word) -> int: int: the 16-bit value located in memory at RAM[address:address+1] """ lowbyte: int = self.ram[int(address)] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) highbyte: int = self.ram[int(address) + 1] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) data = (highbyte << 8) + lowbyte if self.verbose_cycles: - memory_section = self.ram.memory_section(address=address) + memory_section = self.ram.memory_section(address) self.log.info("rr") self.log.debug(f"read_word({memory_section}[0x{address:02x}]): 0x{data:04X} ({data})") return data - def read_word_zeropage(self: Self, address: Byte) -> int: + def read_word_zeropage(self, address: Byte) -> int: """ Read a 16-bit word from zero page at location RAM[address]. @@ -506,16 +512,16 @@ def read_word_zeropage(self: Self, address: Byte) -> int: int: the 16-bit value located in memory at RAM[address:address+1] """ lowbyte: int = self.ram[int(address) & 0xFF] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) highbyte: int = self.ram[(int(address) + 1) & 0xFF] - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) data = (highbyte << 8) + lowbyte if self.verbose_cycles: self.log.info("rr") self.log.debug(f"read_word(zeropage[0x{address & 0xFF:02x}]): 0x{data:04X} ({data})") return data - def peek_word(self: Self, address: Word) -> int: + def peek_word(self, address: Word) -> int: """ Read a 16-bit word from RAM at location RAM[address]. @@ -533,7 +539,7 @@ def peek_word(self: Self, address: Word) -> int: highbyte: int = self.ram[int(address) + 1] return (highbyte << 8) + lowbyte - def write_word(self: Self, address: Word, data: Word) -> None: + def write_word(self, address: Word, data: Word) -> None: """ Write a Word() to RAM at location RAM[address]. @@ -555,13 +561,13 @@ def write_word(self: Self, address: Word, data: Word) -> None: lowbyte: int = ba2int(data.lowbyte_bits) highbyte: int = ba2int(data.highbyte_bits) self.ram[address] = lowbyte - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) self.ram[address + 1] = highbyte - self.spend_cpu_cycles(cost=1) + self.spend_cpu_cycles(1) if self.verbose_cycles: self.log.info("ww") - def read_register(self: Self, register_name: str) -> Byte | Word: + def read_register(self, register_name: str) -> Union[Byte, Word]: """ Read the value of a register. @@ -580,7 +586,7 @@ def read_register(self: Self, register_name: str) -> Byte | Word: return getattr(self, register_name) & 0xFF - def write_register(self: Self, register_name: str, data: int) -> None: + def write_register(self, register_name: str, data: int) -> None: """ Write a value to a register. @@ -600,7 +606,7 @@ def write_register(self: Self, register_name: str, data: int) -> None: setattr(self, register_name, data & 0xFF) - def write_register_to_ram(self: Self, register_name: str, address: int) -> None: + def write_register_to_ram(self, register_name: str, address: int) -> None: """ Write a register value to ram. @@ -617,17 +623,17 @@ def write_register_to_ram(self: Self, register_name: str, address: int) -> None: """ if register_name == "PC": self.write_byte( - address=address, - data=Byte(getattr(self, register_name)).lowbyte & 0xFF, + address, + Byte(getattr(self, register_name)).lowbyte & 0xFF, ) self.write_byte( - address=address, - data=Byte(getattr(self, register_name)).highbyte & 0xFF, + address, + Byte(getattr(self, register_name)).highbyte & 0xFF, ) - self.write_byte(address=address, data=getattr(self, register_name) & 0xFF) + self.write_byte(address,getattr(self, register_name) & 0xFF) - def write_ram_to_register(self: Self, address: int, register_name: str) -> None: + def write_ram_to_register(self, address: int, register_name: str) -> None: """ Write a ram value to a register. @@ -646,14 +652,13 @@ def write_ram_to_register(self: Self, address: int, register_name: str) -> None: setattr( self, register_name, - data=Word( - self.read_byte(address + 1) << 8 + self.read_byte(address), - ), + Word(self.read_byte(address + 1) << 8 + self.read_byte(address)), ) + return - setattr(self, register_name, self.fetch_byte(address=address) & 0xFF) + setattr(self, register_name, self.read_byte(address) & 0xFF) - def set_store_status_flags(self: Self, register_name: str) -> None: + def set_store_status_flags(self, register_name: str) -> None: """ Set the status flags for store operations. @@ -663,7 +668,7 @@ def set_store_status_flags(self: Self, register_name: str) -> None: """ # No flag modifications for store instructions - def set_load_status_flags(self: Self, register_name: str) -> None: + def set_load_status_flags(self, register_name: str) -> None: """ Set the status flags for load operations. @@ -679,7 +684,7 @@ def set_load_status_flags(self: Self, register_name: str) -> None: self.N = flags.ProcessorStatusFlags.N[flags.N] \ if (register & 128) else not flags.ProcessorStatusFlags.N[flags.N] - def fetch_zeropage_mode_address(self: Self, offset_register_name: str) -> int: + def fetch_zeropage_mode_address(self, offset_register_name: str) -> int: """ Read from RAM @ RAM:ZEROPAGE[PC]. @@ -704,7 +709,7 @@ def fetch_zeropage_mode_address(self: Self, offset_register_name: str) -> int: return zeropage_address - def fetch_immediate_mode_address(self: Self) -> int: + def fetch_immediate_mode_address(self) -> int: """ Read from RAM @ RAM[PC]. @@ -720,7 +725,7 @@ def fetch_immediate_mode_address(self: Self) -> int: return data - def fetch_absolute_mode_address(self: Self, offset_register_name: str) -> int: + def fetch_absolute_mode_address(self, offset_register_name: str) -> int: """ Read from RAM @ RAM[(PC:PC + 1)]. @@ -751,7 +756,7 @@ def fetch_absolute_mode_address(self: Self, offset_register_name: str) -> int: return address - def fetch_indexed_indirect_mode_address(self: Self) -> int: + def fetch_indexed_indirect_mode_address(self) -> int: """ Read from RAM @ RAM[(PC:PC + 1) + X]. @@ -775,7 +780,7 @@ def fetch_indexed_indirect_mode_address(self: Self) -> int: return address - def fetch_indirect_indexed_mode_address(self: Self) -> int: + def fetch_indirect_indexed_mode_address(self) -> int: """ Read from RAM @ RAM:[ZEROPAGE[PC] << 8 + ZEROPAGE[PC + 1] + Y. @@ -808,7 +813,7 @@ def fetch_indirect_indexed_mode_address(self: Self) -> int: return address - def execute_load_immediate(self: Self, instruction: instructions.InstructionSet, + def execute_load_immediate(self, instruction: instructions.InstructionSet, register_name: str) -> None: """ Instruction execution for "immediate LD[A, X, Y] #oper". @@ -835,14 +840,14 @@ def execute_load_immediate(self: Self, instruction: instructions.InstructionSet, data: Byte = self.fetch_immediate_mode_address() setattr(self, register_name, data) - self.set_load_status_flags(register_name=register_name) + self.set_load_status_flags(register_name) self.log.debug( f"{instructions.InstructionSet(int(instruction)).name}: " - f"{Byte(value=getattr(self, register_name))}", + f"{Byte(getattr(self, register_name))}", ) - def execute_store_zeropage(self: Self, instruction: instructions.InstructionSet, + def execute_store_zeropage(self, instruction: instructions.InstructionSet, register_name: str, offset_register_name: str) -> None: """ Instruction execution for store zeropage. @@ -869,17 +874,17 @@ def execute_store_zeropage(self: Self, instruction: instructions.InstructionSet, # types. # # Just remember to pass register names and dereference in methods if necessary. - address: Byte = self.fetch_zeropage_mode_address(offset_register_name=offset_register_name) + address: Byte = self.fetch_zeropage_mode_address(offset_register_name) data: Byte = getattr(self, register_name) - self.write_byte(address=address, data=data) - setattr(self, register_name, self.read_byte(address=address)) + self.write_byte(address,data) + setattr(self, register_name, self.read_byte(address)) - self.set_store_status_flags(register_name=register_name) + self.set_store_status_flags(register_name) - self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(value=data)}") + self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(data)}") - def execute_load_zeropage(self: Self, instruction: instructions.InstructionSet, + def execute_load_zeropage(self, instruction: instructions.InstructionSet, register_name: str, offset_register_name: str) -> None: """ Instruction execution for load zeropage. @@ -906,16 +911,16 @@ def execute_load_zeropage(self: Self, instruction: instructions.InstructionSet, # types. # # Just remember to pass register names and dereference in methods if necessary. - address: Byte = self.fetch_zeropage_mode_address(offset_register_name=offset_register_name) + address: Byte = self.fetch_zeropage_mode_address(offset_register_name) register: Byte = getattr(self, register_name) - setattr(self, register_name, self.read_byte(address=address)) + setattr(self, register_name, self.read_byte(address)) - self.set_load_status_flags(register_name=register_name) + self.set_load_status_flags(register_name) - self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(value=register)}") + self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(register)}") - def execute_store_absolute(self: Self, instruction: instructions.InstructionSet, + def execute_store_absolute(self, instruction: instructions.InstructionSet, register_name: str, offset_register_name: str) -> None: """ Instruction execution for store absolute. @@ -934,15 +939,15 @@ def execute_store_absolute(self: Self, instruction: instructions.InstructionSet, ------- None """ - address: Word = self.fetch_absolute_mode_address(offset_register_name=offset_register_name) + address: Word = self.fetch_absolute_mode_address(offset_register_name) register: Byte = getattr(self, register_name) - self.write_byte(address=address & 0xFFFF, data=register) - self.set_store_status_flags(register_name=register_name) + self.write_byte(address & 0xFFFF, register) + self.set_store_status_flags(register_name) - self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(value=register)}") + self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(register)}") - def execute_load_absolute(self: Self, instruction: instructions.InstructionSet, + def execute_load_absolute(self, instruction: instructions.InstructionSet, register_name: str, offset_register_name: str) -> None: """ Instruction execution for load absolute. @@ -969,15 +974,15 @@ def execute_load_absolute(self: Self, instruction: instructions.InstructionSet, # types. # # Just remember to pass register names and dereference in methods if necessary. - address: Word = self.fetch_absolute_mode_address(offset_register_name=offset_register_name) + address: Word = self.fetch_absolute_mode_address(offset_register_name) register: Byte = getattr(self, register_name) - setattr(self, register_name, self.read_byte(address=address)) - self.set_load_status_flags(register_name=register_name) + setattr(self, register_name, self.read_byte(address)) + self.set_load_status_flags(register_name) - self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(value=register)}") + self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(register)}") - def execute_store_indexed_indirect(self: Self, instruction: instructions.InstructionSet, + def execute_store_indexed_indirect(self, instruction: instructions.InstructionSet, register_name: str) -> None: """ Instruction execution for "(indirect,X) ST[A, X, Y] (oper,X)". @@ -1002,17 +1007,17 @@ def execute_store_indexed_indirect(self: Self, instruction: instructions.Instruc # # Just remember to pass register names and dereference in methods if necessary. address: Word = self.fetch_indexed_indirect_mode_address() & 0xFFFF - data: Byte = self.read_byte(address=address) + data: Byte = self.read_byte(address) setattr(self, register_name, data) - self.set_store_status_flags(register_name=register_name) + self.set_store_status_flags(register_name) self.log.debug( f"{instructions.InstructionSet(int(instruction)).name}: " - f"{Byte(value=self.ram[address])}", + f"{Byte(self.ram[address])}", ) - def execute_load_indexed_indirect(self: Self, instruction: instructions.InstructionSet, + def execute_load_indexed_indirect(self, instruction: instructions.InstructionSet, register_name: str) -> None: """ Instruction execution for "(indirect,X) LD[A, X, Y] (oper,X)". @@ -1039,15 +1044,15 @@ def execute_load_indexed_indirect(self: Self, instruction: instructions.Instruct address: Word = self.fetch_indexed_indirect_mode_address() & 0xFFFF register: Byte = getattr(self, register_name) - setattr(self, register_name, self.read_byte(address=address)) - self.set_load_status_flags(register_name=register_name) + setattr(self, register_name, self.read_byte(address)) + self.set_load_status_flags(register_name) self.log.debug( f"{instructions.InstructionSet(int(instruction)).name}: " - f"{Byte(value=register)}", + f"{Byte(register)}", ) - def execute_store_indirect_indexed(self: Self, instruction: instructions.InstructionSet, + def execute_store_indirect_indexed(self, instruction: instructions.InstructionSet, register_name: str) -> None: """ Instruction execution for "(indirect),Y LD[A, X, Y] (oper),Y". @@ -1073,15 +1078,15 @@ def execute_store_indirect_indexed(self: Self, instruction: instructions.Instruc # Just remember to pass register names and dereference in methods if necessary. address: Word = self.fetch_indirect_indexed_mode_address() - setattr(self, register_name, self.read_byte(address=address)) - self.set_store_status_flags(register_name=register_name) + setattr(self, register_name, self.read_byte(address)) + self.set_store_status_flags(register_name) self.log.debug( f"{instructions.InstructionSet(int(instruction)).name}: " - f"{Byte(value=self.ram[address & 0xFFFF])}", + f"{Byte(self.ram[address & 0xFFFF])}", ) - def execute_load_indirect_indexed(self: Self, instruction: instructions.InstructionSet, + def execute_load_indirect_indexed(self, instruction: instructions.InstructionSet, register_name: str) -> None: """ Instruction execution for "(indirect),Y LD[A, X, Y] (oper),Y". @@ -1107,14 +1112,14 @@ def execute_load_indirect_indexed(self: Self, instruction: instructions.Instruct # Just remember to pass register names and dereference in methods if necessary. address: Word = self.fetch_indirect_indexed_mode_address() - setattr(self, register_name, self.read_byte(address=address)) - self.set_load_status_flags(register_name=register_name) + setattr(self, register_name, self.read_byte(address)) + self.set_load_status_flags(register_name) register: Byte = getattr(self, register_name) - self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(value=register)}") + self.log.debug(f"{instructions.InstructionSet(int(instruction)).name}: {Byte(register)}") - def _adc_bcd(self: Self, a: int, value: int, carry_in: int) -> tuple[int, int, int, int]: + def _adc_bcd(self, a: int, value: int, carry_in: int) -> Tuple[int, int, int, int]: """ Perform BCD (Binary-Coded Decimal) addition. @@ -1161,7 +1166,7 @@ def _adc_bcd(self: Self, a: int, value: int, carry_in: int) -> tuple[int, int, i return result, carry_out, overflow, binary_result - def _sbc_bcd(self: Self, a: int, value: int, carry_in: int) -> tuple[int, int, int, int]: + def _sbc_bcd(self, a: int, value: int, carry_in: int) -> Tuple[int, int, int, int]: """ Perform BCD (Binary-Coded Decimal) subtraction. @@ -1208,7 +1213,7 @@ def _sbc_bcd(self: Self, a: int, value: int, carry_in: int) -> tuple[int, int, i return result, carry_out, overflow, binary_result - def execute(self: Self, cycles: int = None, max_instructions: int = None) -> int: # noqa: C901 + def execute(self, cycles: int = None, max_instructions: int = None) -> int: # noqa: C901 """ Fetch and execute CPU instructions. @@ -1277,9 +1282,9 @@ def execute(self: Self, cycles: int = None, max_instructions: int = None) -> int if use_instruction_limit: self.instructions_remaining = instructions_remaining raise errors.CPUHaltError( - opcode=0x00, # Unknown - set by JAM instruction - address=int(self.PC), - message="CPU is halted. Call reset() to recover." + 0x00, # Unknown opcode - set by JAM instruction + int(self.PC), + "CPU is halted. Call reset() to recover." ) # Check for cycle exhaustion BEFORE fetching the next instruction @@ -1369,10 +1374,10 @@ def execute(self: Self, cycles: int = None, max_instructions: int = None) -> int if len(machine_code) == 1: operand: memory.MemoryUnit = Byte( - value=machine_code[0], - endianness=self.endianness, + machine_code[0], + self.endianness, ) - assembly = instruction_map["assembler"].format(oper=f"0x{operand:02X}") + assembly = instruction_map["assembler"].replace("{oper}", f"0x{operand:02X}") if len(machine_code) == 2: low_byte: memory.MemoryUnit = machine_code[0] @@ -1380,7 +1385,7 @@ def execute(self: Self, cycles: int = None, max_instructions: int = None) -> int operand: memory.MemoryUnit = Word((high_byte << 8) + low_byte) - assembly: str = instruction_map["assembler"].format(oper=f"0x{operand:02X}") + assembly: str = instruction_map["assembler"].replace("{oper}", f"0x{operand:02X}") if operand is not None: self.log.info( @@ -1453,7 +1458,7 @@ def execute(self: Self, cycles: int = None, max_instructions: int = None) -> int if self.irq_pending and not self.I: handle_irq() - def _handle_irq(self: Self) -> None: + def _handle_irq(self) -> None: """Handle a pending hardware IRQ. This implements the 6502 IRQ sequence: @@ -1499,7 +1504,7 @@ def _handle_irq(self: Self) -> None: self.log.info(f"*** IRQ HANDLER CALLED: PC ${old_pc:04X} -> ${irq_vector:04X}, I flag now set ***") - def _handle_nmi(self: Self) -> None: + def _handle_nmi(self) -> None: """Handle a pending NMI (Non-Maskable Interrupt). This implements the 6502 NMI sequence: @@ -1548,10 +1553,10 @@ def _handle_nmi(self: Self) -> None: self.log.info(f"*** NMI HANDLER CALLED: PC ${old_pc:04X} -> ${nmi_vector:04X}, I flag now set ***") - def push_pc_to_stack(self: Self) -> None: + def push_pc_to_stack(self) -> None: """Push the PC to the stack.""" - def reset(self: Self) -> None: + def reset(self) -> None: """ Reset the CPU. @@ -1579,7 +1584,7 @@ def reset(self: Self) -> None: # VARIANT: 65C02 - Same behavior as 6502 # VARIANT: 65C816 - Same behavior as 6502 (8-bit mode) # The stack pointer is stored with 0x0100 offset for convenience - self.S: Word = Word(0x01FD, endianness=self.endianness) + self.S: Word = Word(0x01FD, self.endianness) # VARIANT: 6502 - Status register set to 0x34 on reset # VARIANT: 65C02 - Same as 6502 (I=1, bit5=1, D=0) @@ -1617,7 +1622,7 @@ def reset(self: Self) -> None: reset_vector = self.peek_word(reset_vector_addr) # Set PC to the reset vector value - self.PC: Word = Word(reset_vector, endianness=self.endianness) + self.PC: Word = Word(reset_vector, self.endianness) # VARIANT: 6502 - Reset sequence consumes 7 cycles total # VARIANT: 65C02 - Same as 6502 @@ -1629,7 +1634,7 @@ def reset(self: Self) -> None: self.log.info(f"Reset complete: PC=${self.PC:04X}, S=${self.S & 0xFF:02X}, P=0x34, 7 cycles consumed") @property - def flags(self: Self) -> Byte: + def flags(self) -> Byte: """ Return the CPU flags register. @@ -1640,7 +1645,7 @@ def flags(self: Self) -> Byte: return self._flags @flags.setter - def flags(self: Self, flags: int) -> None: + def flags(self, flags: int) -> None: """ Set the CPU flags register. @@ -1657,7 +1662,7 @@ def flags(self: Self, flags: int) -> None: setattr(self._flags, flags) @property - def PC(self: Self) -> int: # noqa: N802 + def PC(self) -> int: # noqa: N802 """ Return the CPU PC register. @@ -1670,7 +1675,7 @@ def PC(self: Self) -> int: # noqa: N802 return self._registers.PC @PC.setter - def PC(self: Self, PC: int) -> None: # noqa: N802 N803 + def PC(self, PC: int) -> None: # noqa: N802 N803 """ Set the CPU PC register. @@ -1691,7 +1696,7 @@ def PC(self: Self, PC: int) -> None: # noqa: N802 N803 self.pc_callback(self._registers.PC) @property - def S(self: Self) -> int: # noqa: N802 + def S(self) -> int: # noqa: N802 """ Return the CPU S register. @@ -1708,7 +1713,7 @@ def S(self: Self) -> int: # noqa: N802 return self._registers.S @S.setter - def S(self: Self, S: int) -> None: # noqa: N802 N803 + def S(self, S: int) -> None: # noqa: N802 N803 """ Set the CPU S register. @@ -1730,7 +1735,7 @@ def S(self: Self, S: int) -> None: # noqa: N802 N803 self.log.info(f"S -> 0x{self._registers.S & 0xFF:02X}") @property - def A(self: Self) -> int: # noqa: N802 + def A(self) -> int: # noqa: N802 """ Return the CPU A register. @@ -1743,7 +1748,7 @@ def A(self: Self) -> int: # noqa: N802 return self._registers.A @A.setter - def A(self: Self, A: int) -> None: # noqa: N802 N803 + def A(self, A: int) -> None: # noqa: N802 N803 """ Set the CPU A register. @@ -1760,7 +1765,7 @@ def A(self: Self, A: int) -> None: # noqa: N802 N803 self.log.info(f"A -> 0x{self._registers.A:02X}") @property - def X(self: Self) -> int: # noqa: N802 + def X(self) -> int: # noqa: N802 """ Return the CPU X register. @@ -1773,7 +1778,7 @@ def X(self: Self) -> int: # noqa: N802 return self._registers.X @X.setter - def X(self: Self, X: int) -> None: # noqa: N802 N803 + def X(self, X: int) -> None: # noqa: N802 N803 """ Set the CPU X register. @@ -1790,7 +1795,7 @@ def X(self: Self, X: int) -> None: # noqa: N802 N803 self.log.info(f"X -> 0x{self._registers.X:02X}") @property - def Y(self: Self) -> int: # noqa: N802 + def Y(self) -> int: # noqa: N802 """ Return the CPU Y register. @@ -1803,7 +1808,7 @@ def Y(self: Self) -> int: # noqa: N802 return self._registers.Y @Y.setter - def Y(self: Self, Y: int) -> None: # noqa: N802 N803 + def Y(self, Y: int) -> None: # noqa: N802 N803 """ Set the CPU Y register. @@ -1819,7 +1824,7 @@ def Y(self: Self, Y: int) -> None: # noqa: N802 N803 if self.verbose_cycles: self.log.info(f"Y -> 0x{self._registers.Y:02X}") - def __str__(self: Self) -> str: + def __str__(self) -> str: """Return the CPU status.""" description: str = f"{type(self).__name__}\n" description += f"\tPC: 0x{self.PC:04X}\n" @@ -1847,7 +1852,10 @@ def main() -> None: then loads 0x23 from 0x4243 using the LDA_IMMEDIATE instruction. """ log: logging.Logger = logging.getLogger("mos6502") - logging.basicConfig(format="%(message)s", level=logging.INFO) + try: + logging.basicConfig(format="%(message)s", level=logging.INFO) + except TypeError: + pass # MicroPython frozen module - kwargs not supported with MOS6502CPU() as cpu: cpu.reset() @@ -1878,7 +1886,7 @@ def main() -> None: log.info(cpu) try: - cpu.execute(cycles=20) + cpu.execute(20) except errors.CPUCycleExhaustionError: log.info(f"Used: {cpu.cycles_executed} cycles") diff --git a/mos6502/flags.py b/mos6502/flags.py index ba0107c..1a22739 100755 --- a/mos6502/flags.py +++ b/mos6502/flags.py @@ -5,8 +5,7 @@ Bit operations use simple masking instead of bitarray indexing. """ -import logging -from typing import Self +from mos6502.compat import logging # Dedicated logger for flag modifications flag_logger = logging.getLogger("mos6502.cpu.flags") @@ -78,7 +77,7 @@ class FlagsRegister: __slots__ = ('_value', '_last_logged_value') - def __init__(self: Self, value: int = 0, endianness: str = "little") -> None: + def __init__(self, value: int = 0, endianness: str = "little") -> None: """Initialize FlagsRegister. Args: @@ -95,20 +94,20 @@ def __init__(self: Self, value: int = 0, endianness: str = "little") -> None: self._last_logged_value: int = self._value @property - def value(self: Self) -> int: + def value(self) -> int: """Return the flags register value as an int (0x00-0xFF).""" return self._value @value.setter - def value(self: Self, new_value: int) -> None: + def value(self, new_value: int) -> None: """Set the flags register value.""" self._value = int(new_value) & 0xFF - def __getitem__(self: Self, bit_index: int) -> int: + def __getitem__(self, bit_index: int) -> int: """Get a flag bit by index. Returns 0 or 1.""" return (self._value >> bit_index) & 1 - def __setitem__(self: Self, bit_index: int, bit_value: int) -> None: + def __setitem__(self, bit_index: int, bit_value: int) -> None: """Set a flag bit by index. Args: @@ -134,19 +133,19 @@ def __setitem__(self: Self, bit_index: int, bit_value: int) -> None: flag_logger.info(f"⎿ {format_flags(self._value)} (0x{self._value:02X})") self._last_logged_value = self._value - def __int__(self: Self) -> int: + def __int__(self) -> int: """Return the flags value as an int.""" return self._value - def __and__(self: Self, other: int) -> int: + def __and__(self, other: int) -> int: """Bitwise AND with an int.""" return self._value & other - def __or__(self: Self, other: int) -> int: + def __or__(self, other: int) -> int: """Bitwise OR with an int.""" return self._value | other - def __eq__(self: Self, other: object) -> bool: + def __eq__(self, other: object) -> bool: """Compare equality with another FlagsRegister or int.""" if isinstance(other, FlagsRegister): return self._value == other._value @@ -154,7 +153,7 @@ def __eq__(self: Self, other: object) -> bool: return self._value == other return NotImplemented - def __repr__(self: Self) -> str: + def __repr__(self) -> str: """Return a string representation.""" return f"FlagsRegister(0x{self._value:02X})" @@ -191,71 +190,71 @@ class ProcessorStatusFlagsInterface: __slots__ = () # No instance attributes - just property accessors @property - def C(self: Self) -> bool: # noqa: N802 + def C(self) -> bool: # noqa: N802 """C is True if the Carry flag is set.""" return bool(self._flags[C]) @C.setter - def C(self: Self, flag: int) -> None: # noqa: N802 + def C(self, flag: int) -> None: # noqa: N802 """Set the Carry flag.""" self._flags[C] = flag @property - def Z(self: Self) -> bool: # noqa: N802 + def Z(self) -> bool: # noqa: N802 """Z is True if the Zero flag is set.""" return bool(self._flags[Z]) @Z.setter - def Z(self: Self, flag: int) -> None: # noqa: N802 + def Z(self, flag: int) -> None: # noqa: N802 """Set the Zero flag.""" self._flags[Z] = flag @property - def I(self: Self) -> bool: # noqa: E743 N802 + def I(self) -> bool: # noqa: E743 N802 """I is True if the IRQ Disable flag is set.""" return bool(self._flags[I]) @I.setter - def I(self: Self, flag: int) -> None: # noqa: E743 N802 + def I(self, flag: int) -> None: # noqa: E743 N802 """Set the IRQ Disable flag.""" self._flags[I] = flag @property - def D(self: Self) -> bool: # noqa: N802 + def D(self) -> bool: # noqa: N802 """D is True if the Decimal Mode flag is set.""" return bool(self._flags[D]) @D.setter - def D(self: Self, flag: int) -> None: # noqa: N802 + def D(self, flag: int) -> None: # noqa: N802 """Set the Decimal Mode flag.""" self._flags[D] = flag @property - def B(self: Self) -> bool: # noqa: N802 + def B(self) -> bool: # noqa: N802 """B is True if the Break flag is set.""" return bool(self._flags[B]) @B.setter - def B(self: Self, flag: int) -> None: # noqa: N802 + def B(self, flag: int) -> None: # noqa: N802 """Set the Break flag.""" self._flags[B] = flag @property - def V(self: Self) -> bool: # noqa: N802 + def V(self) -> bool: # noqa: N802 """V is True if the Overflow flag is set.""" return bool(self._flags[V]) @V.setter - def V(self: Self, flag: int) -> None: # noqa: N802 + def V(self, flag: int) -> None: # noqa: N802 """Set the Overflow flag.""" self._flags[V] = flag @property - def N(self: Self) -> bool: # noqa: N802 + def N(self) -> bool: # noqa: N802 """N is True if the Negative flag is set.""" return bool(self._flags[N]) @N.setter - def N(self: Self, flag: int) -> None: # noqa: N802 + def N(self, flag: int) -> None: # noqa: N802 """Set the Negative flag.""" self._flags[N] = flag diff --git a/mos6502/instructions/__init__.py b/mos6502/instructions/__init__.py index 09253a9..49b7c3f 100644 --- a/mos6502/instructions/__init__.py +++ b/mos6502/instructions/__init__.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Instruction set for the mos6502 CPU.""" -import enum -from dataclasses import dataclass -from typing import Literal, NoReturn, Self +from mos6502.compat import enum +from mos6502.compat import make_dataclass +from mos6502.compat import Literal, NoReturn, List, Dict from mos6502.errors import IllegalCPUInstructionError @@ -25,7 +25,8 @@ ] -@dataclass(frozen=True, slots=True) +# Use make_dataclass(frozen, slots) with positional args - kwargs don't work in MicroPython frozen modules +@make_dataclass(True, True) class CPUInstruction: """Metadata for a 6502 CPU instruction opcode. @@ -47,6 +48,12 @@ class CPUInstruction: """ + # Explicit field order for MicroPython compatibility (annotations may not work) + _field_order_ = ('opcode', 'mnemonic', 'addressing', 'assembler', 'bytes', + 'base_cycles', 'affected_flags', 'package', 'function', + 'page_boundary_penalty') + _field_defaults_ = {'page_boundary_penalty': False} + opcode: int mnemonic: str addressing: AddressingMode @@ -58,7 +65,7 @@ class CPUInstruction: function: str page_boundary_penalty: bool = False - def __post_init__(self: Self) -> None: + def __post_init__(self) -> None: """Validate instruction metadata after initialization.""" if not 0x00 <= self.opcode <= 0xFF: msg = f"Opcode must be 0x00-0xFF, got: 0x{self.opcode:02X}" @@ -79,7 +86,7 @@ def __post_init__(self: Self) -> None: raise ValueError(msg) @property - def name(self: Self) -> str: + def name(self) -> str: """Generate canonical name like LDA_IMMEDIATE_0xA9.""" addr_map = { "implied": "IMPLIED", @@ -100,43 +107,43 @@ def name(self: Self) -> str: return f"{self.mnemonic}_{addr_name}_0x{self.opcode:02X}" @property - def cycles_display(self: Self) -> str: + def cycles_display(self) -> str: """Return cycles as display string (e.g., '4' or '4*' for page penalty).""" if self.page_boundary_penalty: return f"{self.base_cycles}*" return str(self.base_cycles) @property - def affects_n(self: Self) -> bool: + def affects_n(self) -> bool: """True if instruction affects Negative flag.""" return "N" in self.affected_flags @property - def affects_v(self: Self) -> bool: + def affects_v(self) -> bool: """True if instruction affects Overflow flag.""" return "V" in self.affected_flags @property - def affects_z(self: Self) -> bool: + def affects_z(self) -> bool: """True if instruction affects Zero flag.""" return "Z" in self.affected_flags @property - def affects_c(self: Self) -> bool: + def affects_c(self) -> bool: """True if instruction affects Carry flag.""" return "C" in self.affected_flags @property - def affects_i(self: Self) -> bool: + def affects_i(self) -> bool: """True if instruction affects Interrupt Disable flag.""" return "I" in self.affected_flags @property - def affects_d(self: Self) -> bool: + def affects_d(self) -> bool: """True if instruction affects Decimal flag.""" return "D" in self.affected_flags - def to_legacy_dict(self: Self) -> dict: + def to_legacy_dict(self) -> dict: """Convert to legacy instruction_map dictionary format.""" from mos6502 import flags as flag_bits from mos6502.memory import Byte @@ -164,35 +171,63 @@ def to_legacy_dict(self: Self) -> dict: "flags": flags_byte, } - def __int__(self: Self) -> int: + def __int__(self) -> int: """Allow using CPUInstruction where an int opcode is expected.""" return self.opcode - def __hash__(self: Self) -> int: + def __hash__(self) -> int: """Hash by opcode for use in sets and as dict keys.""" return hash(self.opcode) -class PseudoEnumMember(int): - """Allows dynamic addition of members to IntEnum classes.""" +class PseudoEnumMember: + """Allows dynamic addition of members to IntEnum classes. + + Note: Does not inherit from int for MicroPython compatibility. + Implements __int__, __eq__, __hash__, and __index__ to behave like an int. + """ - def __new__(cls, value: int, name: str) -> "PseudoEnumMember": + __slots__ = ('_value_', '_name') + + def __init__(self, value: int, name: str) -> None: """Create a pseudo-enum member with the given value and name.""" - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + self._value_ = value + self._name = name @property - def name(self: Self) -> str: + def name(self) -> str: """Return the member name.""" return self._name @property - def value(self: Self) -> int: + def value(self) -> int: """Return the member value.""" return self._value_ + def __int__(self) -> int: + """Return the integer value.""" + return self._value_ + + def __eq__(self, other: object) -> bool: + """Compare equality with int or another PseudoEnumMember.""" + if isinstance(other, int): + return self._value_ == other + if isinstance(other, PseudoEnumMember): + return self._value_ == other._value_ + return NotImplemented + + def __hash__(self) -> int: + """Hash by value for use in sets and as dict keys.""" + return hash(self._value_) + + def __index__(self) -> int: + """Allow use in slices and hex().""" + return self._value_ + + def __repr__(self) -> str: + """Return string representation.""" + return f"<{self._name}: {self._value_}>" + def register_instruction( instruction: CPUInstruction, @@ -207,7 +242,7 @@ def register_instruction( def register_instructions( - instructions: list[CPUInstruction], + instructions: List[CPUInstruction], instruction_set_class: type, instruction_map: dict, ) -> None: @@ -216,11 +251,14 @@ def register_instructions( register_instruction(instruction, instruction_set_class, instruction_map) -class InstructionOpcode(int): +class InstructionOpcode: """Instruction opcode that carries its variant metadata. - This class extends int to carry package and function names for variant dispatch, - while remaining fully compatible with existing code that expects plain integers. + This class stores the opcode value and carries package and function names + for variant dispatch, while remaining compatible with code that expects integers. + + Note: Does not inherit from int for MicroPython compatibility. + Implements __int__, __eq__, __hash__, and __index__ to behave like an int. Example: ------- @@ -231,7 +269,9 @@ class InstructionOpcode(int): ) """ - def __new__(cls, value: int, package: str, function: str) -> "InstructionSet": + __slots__ = ('_value', 'package', 'function') + + def __init__(self, value: int, package: str, function: str) -> None: """Create instruction opcode with metadata. Arguments: @@ -240,189 +280,220 @@ def __new__(cls, value: int, package: str, function: str) -> "InstructionSet": package: Package name (e.g., "mos6502.instructions.nop") function: Function name (e.g., "nop_implied_0xea") """ - obj = int.__new__(cls, value) - obj.package = package # type: ignore - obj.function = function # type: ignore - return obj + self._value = value + self.package = package + self.function = function + + def __int__(self) -> int: + """Return the integer opcode value.""" + return self._value + + def __eq__(self, other: object) -> bool: + """Compare equality with int or another InstructionOpcode.""" + if isinstance(other, int): + return self._value == other + if isinstance(other, InstructionOpcode): + return self._value == other._value + return NotImplemented + + def __hash__(self) -> int: + """Hash by opcode value for use in sets and as dict keys.""" + return hash(self._value) + + def __index__(self) -> int: + """Allow use in slices and hex().""" + return self._value + + def __repr__(self) -> str: + """Return string representation.""" + return f"InstructionOpcode(0x{self._value:02X}, {self.package!r}, {self.function!r})" # Import from individual instruction modules from mos6502.instructions._bit import BIT_ZEROPAGE_0x24, BIT_ABSOLUTE_0x2C, register_bit_instructions from mos6502.instructions._brk import BRK_IMPLIED_0x00, register_brk_instructions -# Illegal instructions -from mos6502.instructions.illegal._lax import ( - LAX_ZEROPAGE_0xA7, - LAX_ZEROPAGE_Y_0xB7, - LAX_INDEXED_INDIRECT_X_0xA3, - LAX_INDIRECT_INDEXED_Y_0xB3, - LAX_ABSOLUTE_0xAF, - LAX_ABSOLUTE_Y_0xBF, - LAX_IMMEDIATE_0xAB, - register_lax_instructions, -) -from mos6502.instructions.illegal._sax import ( - SAX_ZEROPAGE_0x87, - SAX_ZEROPAGE_Y_0x97, - SAX_INDEXED_INDIRECT_X_0x83, - SAX_ABSOLUTE_0x8F, - register_sax_instructions, -) -from mos6502.instructions.illegal._dcp import ( - DCP_ZEROPAGE_0xC7, - DCP_ZEROPAGE_X_0xD7, - DCP_INDEXED_INDIRECT_X_0xC3, - DCP_INDIRECT_INDEXED_Y_0xD3, - DCP_ABSOLUTE_0xCF, - DCP_ABSOLUTE_X_0xDF, - DCP_ABSOLUTE_Y_0xDB, - register_dcp_instructions, -) -from mos6502.instructions.illegal._isc import ( - ISC_ZEROPAGE_0xE7, - ISC_ZEROPAGE_X_0xF7, - ISC_INDEXED_INDIRECT_X_0xE3, - ISC_INDIRECT_INDEXED_Y_0xF3, - ISC_ABSOLUTE_0xEF, - ISC_ABSOLUTE_X_0xFF, - ISC_ABSOLUTE_Y_0xFB, - register_isc_instructions, -) -from mos6502.instructions.illegal._slo import ( - SLO_ZEROPAGE_0x07, - SLO_ZEROPAGE_X_0x17, - SLO_INDEXED_INDIRECT_X_0x03, - SLO_INDIRECT_INDEXED_Y_0x13, - SLO_ABSOLUTE_0x0F, - SLO_ABSOLUTE_X_0x1F, - SLO_ABSOLUTE_Y_0x1B, - register_slo_instructions, -) -from mos6502.instructions.illegal._rla import ( - RLA_ZEROPAGE_0x27, - RLA_ZEROPAGE_X_0x37, - RLA_INDEXED_INDIRECT_X_0x23, - RLA_INDIRECT_INDEXED_Y_0x33, - RLA_ABSOLUTE_0x2F, - RLA_ABSOLUTE_X_0x3F, - RLA_ABSOLUTE_Y_0x3B, - register_rla_instructions, -) -from mos6502.instructions.illegal._sre import ( - SRE_ZEROPAGE_0x47, - SRE_ZEROPAGE_X_0x57, - SRE_INDEXED_INDIRECT_X_0x43, - SRE_INDIRECT_INDEXED_Y_0x53, - SRE_ABSOLUTE_0x4F, - SRE_ABSOLUTE_X_0x5F, - SRE_ABSOLUTE_Y_0x5B, - register_sre_instructions, -) -from mos6502.instructions.illegal._rra import ( - RRA_ZEROPAGE_0x67, - RRA_ZEROPAGE_X_0x77, - RRA_INDEXED_INDIRECT_X_0x63, - RRA_INDIRECT_INDEXED_Y_0x73, - RRA_ABSOLUTE_0x6F, - RRA_ABSOLUTE_X_0x7F, - RRA_ABSOLUTE_Y_0x7B, - register_rra_instructions, -) -from mos6502.instructions.illegal._anc import ( - ANC_IMMEDIATE_0x0B, - ANC_IMMEDIATE_0x2B, - register_anc_instructions, -) -from mos6502.instructions.illegal._alr import ( - ALR_IMMEDIATE_0x4B, - register_alr_instructions, -) -from mos6502.instructions.illegal._arr import ( - ARR_IMMEDIATE_0x6B, - register_arr_instructions, -) -from mos6502.instructions.illegal._sbx import ( - SBX_IMMEDIATE_0xCB, - register_sbx_instructions, -) -from mos6502.instructions.illegal._las import ( - LAS_ABSOLUTE_Y_0xBB, - register_las_instructions, -) -from mos6502.instructions.illegal._sbc_illegal import ( - SBC_IMMEDIATE_0xEB, - register_sbc_illegal_instructions, -) -from mos6502.instructions.illegal._ane import ( - ANE_IMMEDIATE_0x8B, - register_ane_instructions, -) -from mos6502.instructions.illegal._sha import ( - SHA_INDIRECT_INDEXED_Y_0x93, - SHA_ABSOLUTE_Y_0x9F, - register_sha_instructions, -) -from mos6502.instructions.illegal._shx import ( - SHX_ABSOLUTE_Y_0x9E, - register_shx_instructions, -) -from mos6502.instructions.illegal._shy import ( - SHY_ABSOLUTE_X_0x9C, - register_shy_instructions, -) -from mos6502.instructions.illegal._tas import ( - TAS_ABSOLUTE_Y_0x9B, - register_tas_instructions, -) -from mos6502.instructions.illegal._jam import ( - JAM_IMPLIED_0x02, - JAM_IMPLIED_0x12, - JAM_IMPLIED_0x22, - JAM_IMPLIED_0x32, - JAM_IMPLIED_0x42, - JAM_IMPLIED_0x52, - JAM_IMPLIED_0x62, - JAM_IMPLIED_0x72, - JAM_IMPLIED_0x92, - JAM_IMPLIED_0xB2, - JAM_IMPLIED_0xD2, - JAM_IMPLIED_0xF2, - register_jam_instructions, -) -from mos6502.instructions.illegal._nop_illegal import ( - # 1-byte implied - NOP_IMPLIED_0x1A, - NOP_IMPLIED_0x3A, - NOP_IMPLIED_0x5A, - NOP_IMPLIED_0x7A, - NOP_IMPLIED_0xDA, - NOP_IMPLIED_0xFA, - # 2-byte immediate - NOP_IMMEDIATE_0x80, - NOP_IMMEDIATE_0x82, - NOP_IMMEDIATE_0x89, - NOP_IMMEDIATE_0xC2, - NOP_IMMEDIATE_0xE2, - # 2-byte zero page - NOP_ZEROPAGE_0x04, - NOP_ZEROPAGE_0x44, - NOP_ZEROPAGE_0x64, - # 2-byte zero page,X - NOP_ZEROPAGE_X_0x14, - NOP_ZEROPAGE_X_0x34, - NOP_ZEROPAGE_X_0x54, - NOP_ZEROPAGE_X_0x74, - NOP_ZEROPAGE_X_0xD4, - NOP_ZEROPAGE_X_0xF4, - # 3-byte absolute - NOP_ABSOLUTE_0x0C, - # 3-byte absolute,X - NOP_ABSOLUTE_X_0x1C, - NOP_ABSOLUTE_X_0x3C, - NOP_ABSOLUTE_X_0x5C, - NOP_ABSOLUTE_X_0x7C, - NOP_ABSOLUTE_X_0xDC, - NOP_ABSOLUTE_X_0xFC, - register_nop_illegal_instructions, -) +# Illegal instructions - optional for MicroPython/Pico (memory constrained) +# These imports are wrapped in try/except so they gracefully fail on Pico +# where the illegal instruction modules are not deployed to save memory. +_ILLEGAL_INSTRUCTIONS_AVAILABLE = False +try: + from mos6502.instructions.illegal._lax import ( + LAX_ZEROPAGE_0xA7, + LAX_ZEROPAGE_Y_0xB7, + LAX_INDEXED_INDIRECT_X_0xA3, + LAX_INDIRECT_INDEXED_Y_0xB3, + LAX_ABSOLUTE_0xAF, + LAX_ABSOLUTE_Y_0xBF, + LAX_IMMEDIATE_0xAB, + register_lax_instructions, + ) + from mos6502.instructions.illegal._sax import ( + SAX_ZEROPAGE_0x87, + SAX_ZEROPAGE_Y_0x97, + SAX_INDEXED_INDIRECT_X_0x83, + SAX_ABSOLUTE_0x8F, + register_sax_instructions, + ) + from mos6502.instructions.illegal._dcp import ( + DCP_ZEROPAGE_0xC7, + DCP_ZEROPAGE_X_0xD7, + DCP_INDEXED_INDIRECT_X_0xC3, + DCP_INDIRECT_INDEXED_Y_0xD3, + DCP_ABSOLUTE_0xCF, + DCP_ABSOLUTE_X_0xDF, + DCP_ABSOLUTE_Y_0xDB, + register_dcp_instructions, + ) + from mos6502.instructions.illegal._isc import ( + ISC_ZEROPAGE_0xE7, + ISC_ZEROPAGE_X_0xF7, + ISC_INDEXED_INDIRECT_X_0xE3, + ISC_INDIRECT_INDEXED_Y_0xF3, + ISC_ABSOLUTE_0xEF, + ISC_ABSOLUTE_X_0xFF, + ISC_ABSOLUTE_Y_0xFB, + register_isc_instructions, + ) + from mos6502.instructions.illegal._slo import ( + SLO_ZEROPAGE_0x07, + SLO_ZEROPAGE_X_0x17, + SLO_INDEXED_INDIRECT_X_0x03, + SLO_INDIRECT_INDEXED_Y_0x13, + SLO_ABSOLUTE_0x0F, + SLO_ABSOLUTE_X_0x1F, + SLO_ABSOLUTE_Y_0x1B, + register_slo_instructions, + ) + from mos6502.instructions.illegal._rla import ( + RLA_ZEROPAGE_0x27, + RLA_ZEROPAGE_X_0x37, + RLA_INDEXED_INDIRECT_X_0x23, + RLA_INDIRECT_INDEXED_Y_0x33, + RLA_ABSOLUTE_0x2F, + RLA_ABSOLUTE_X_0x3F, + RLA_ABSOLUTE_Y_0x3B, + register_rla_instructions, + ) + from mos6502.instructions.illegal._sre import ( + SRE_ZEROPAGE_0x47, + SRE_ZEROPAGE_X_0x57, + SRE_INDEXED_INDIRECT_X_0x43, + SRE_INDIRECT_INDEXED_Y_0x53, + SRE_ABSOLUTE_0x4F, + SRE_ABSOLUTE_X_0x5F, + SRE_ABSOLUTE_Y_0x5B, + register_sre_instructions, + ) + from mos6502.instructions.illegal._rra import ( + RRA_ZEROPAGE_0x67, + RRA_ZEROPAGE_X_0x77, + RRA_INDEXED_INDIRECT_X_0x63, + RRA_INDIRECT_INDEXED_Y_0x73, + RRA_ABSOLUTE_0x6F, + RRA_ABSOLUTE_X_0x7F, + RRA_ABSOLUTE_Y_0x7B, + register_rra_instructions, + ) + from mos6502.instructions.illegal._anc import ( + ANC_IMMEDIATE_0x0B, + ANC_IMMEDIATE_0x2B, + register_anc_instructions, + ) + from mos6502.instructions.illegal._alr import ( + ALR_IMMEDIATE_0x4B, + register_alr_instructions, + ) + from mos6502.instructions.illegal._arr import ( + ARR_IMMEDIATE_0x6B, + register_arr_instructions, + ) + from mos6502.instructions.illegal._sbx import ( + SBX_IMMEDIATE_0xCB, + register_sbx_instructions, + ) + from mos6502.instructions.illegal._las import ( + LAS_ABSOLUTE_Y_0xBB, + register_las_instructions, + ) + from mos6502.instructions.illegal._sbc_illegal import ( + SBC_IMMEDIATE_0xEB, + register_sbc_illegal_instructions, + ) + from mos6502.instructions.illegal._ane import ( + ANE_IMMEDIATE_0x8B, + register_ane_instructions, + ) + from mos6502.instructions.illegal._sha import ( + SHA_INDIRECT_INDEXED_Y_0x93, + SHA_ABSOLUTE_Y_0x9F, + register_sha_instructions, + ) + from mos6502.instructions.illegal._shx import ( + SHX_ABSOLUTE_Y_0x9E, + register_shx_instructions, + ) + from mos6502.instructions.illegal._shy import ( + SHY_ABSOLUTE_X_0x9C, + register_shy_instructions, + ) + from mos6502.instructions.illegal._tas import ( + TAS_ABSOLUTE_Y_0x9B, + register_tas_instructions, + ) + from mos6502.instructions.illegal._jam import ( + JAM_IMPLIED_0x02, + JAM_IMPLIED_0x12, + JAM_IMPLIED_0x22, + JAM_IMPLIED_0x32, + JAM_IMPLIED_0x42, + JAM_IMPLIED_0x52, + JAM_IMPLIED_0x62, + JAM_IMPLIED_0x72, + JAM_IMPLIED_0x92, + JAM_IMPLIED_0xB2, + JAM_IMPLIED_0xD2, + JAM_IMPLIED_0xF2, + register_jam_instructions, + ) + from mos6502.instructions.illegal._nop_illegal import ( + # 1-byte implied + NOP_IMPLIED_0x1A, + NOP_IMPLIED_0x3A, + NOP_IMPLIED_0x5A, + NOP_IMPLIED_0x7A, + NOP_IMPLIED_0xDA, + NOP_IMPLIED_0xFA, + # 2-byte immediate + NOP_IMMEDIATE_0x80, + NOP_IMMEDIATE_0x82, + NOP_IMMEDIATE_0x89, + NOP_IMMEDIATE_0xC2, + NOP_IMMEDIATE_0xE2, + # 2-byte zero page + NOP_ZEROPAGE_0x04, + NOP_ZEROPAGE_0x44, + NOP_ZEROPAGE_0x64, + # 2-byte zero page,X + NOP_ZEROPAGE_X_0x14, + NOP_ZEROPAGE_X_0x34, + NOP_ZEROPAGE_X_0x54, + NOP_ZEROPAGE_X_0x74, + NOP_ZEROPAGE_X_0xD4, + NOP_ZEROPAGE_X_0xF4, + # 3-byte absolute + NOP_ABSOLUTE_0x0C, + # 3-byte absolute,X + NOP_ABSOLUTE_X_0x1C, + NOP_ABSOLUTE_X_0x3C, + NOP_ABSOLUTE_X_0x5C, + NOP_ABSOLUTE_X_0x7C, + NOP_ABSOLUTE_X_0xDC, + NOP_ABSOLUTE_X_0xFC, + register_nop_illegal_instructions, + ) + _ILLEGAL_INSTRUCTIONS_AVAILABLE = True +except ImportError: + # Illegal instructions not available (MicroPython/Pico deployment) + pass from mos6502.instructions.load._lda import ( LDA_IMMEDIATE_0xA9, LDA_ZEROPAGE_0xA5, @@ -993,8 +1064,9 @@ def _missing_(cls: type["InstructionSet"], value: int) -> NoReturn: raise IllegalCPUInstructionError(f"{value} ({value:02X}) is not a valid {cls}.") -# Initialize instruction map +# Initialize instruction map and _value2member_map_ (needed for MicroPython compatibility) InstructionSet.map = {} +InstructionSet._value2member_map_ = {} # Register instruction modules register_bit_instructions(InstructionSet, InstructionSet.map) @@ -1026,28 +1098,29 @@ def _missing_(cls: type["InstructionSet"], value: int) -> NoReturn: register_nop_instructions(InstructionSet, InstructionSet.map) register_rti_instructions(InstructionSet, InstructionSet.map) register_rts_instructions(InstructionSet, InstructionSet.map) -# Illegal instructions -register_lax_instructions(InstructionSet, InstructionSet.map) -register_sax_instructions(InstructionSet, InstructionSet.map) -register_dcp_instructions(InstructionSet, InstructionSet.map) -register_isc_instructions(InstructionSet, InstructionSet.map) -register_slo_instructions(InstructionSet, InstructionSet.map) -register_rla_instructions(InstructionSet, InstructionSet.map) -register_sre_instructions(InstructionSet, InstructionSet.map) -register_rra_instructions(InstructionSet, InstructionSet.map) -register_anc_instructions(InstructionSet, InstructionSet.map) -register_alr_instructions(InstructionSet, InstructionSet.map) -register_arr_instructions(InstructionSet, InstructionSet.map) -register_sbx_instructions(InstructionSet, InstructionSet.map) -register_las_instructions(InstructionSet, InstructionSet.map) -register_nop_illegal_instructions(InstructionSet, InstructionSet.map) -register_sbc_illegal_instructions(InstructionSet, InstructionSet.map) -register_ane_instructions(InstructionSet, InstructionSet.map) -register_sha_instructions(InstructionSet, InstructionSet.map) -register_shx_instructions(InstructionSet, InstructionSet.map) -register_shy_instructions(InstructionSet, InstructionSet.map) -register_tas_instructions(InstructionSet, InstructionSet.map) -register_jam_instructions(InstructionSet, InstructionSet.map) +# Illegal instructions - only register if available (not on MicroPython/Pico) +if _ILLEGAL_INSTRUCTIONS_AVAILABLE: + register_lax_instructions(InstructionSet, InstructionSet.map) + register_sax_instructions(InstructionSet, InstructionSet.map) + register_dcp_instructions(InstructionSet, InstructionSet.map) + register_isc_instructions(InstructionSet, InstructionSet.map) + register_slo_instructions(InstructionSet, InstructionSet.map) + register_rla_instructions(InstructionSet, InstructionSet.map) + register_sre_instructions(InstructionSet, InstructionSet.map) + register_rra_instructions(InstructionSet, InstructionSet.map) + register_anc_instructions(InstructionSet, InstructionSet.map) + register_alr_instructions(InstructionSet, InstructionSet.map) + register_arr_instructions(InstructionSet, InstructionSet.map) + register_sbx_instructions(InstructionSet, InstructionSet.map) + register_las_instructions(InstructionSet, InstructionSet.map) + register_nop_illegal_instructions(InstructionSet, InstructionSet.map) + register_sbc_illegal_instructions(InstructionSet, InstructionSet.map) + register_ane_instructions(InstructionSet, InstructionSet.map) + register_sha_instructions(InstructionSet, InstructionSet.map) + register_shx_instructions(InstructionSet, InstructionSet.map) + register_shy_instructions(InstructionSet, InstructionSet.map) + register_tas_instructions(InstructionSet, InstructionSet.map) + register_jam_instructions(InstructionSet, InstructionSet.map) # register_all_arithmetic_instructions(InstructionSet, InstructionSet.map) # MIGRATED to adc/sbc/inc/dec packages register_all_branch_instructions(InstructionSet, InstructionSet.map) # MIGRATED to individual branch packages # register_all_compare_instructions(InstructionSet, InstructionSet.map) # MIGRATED to cmp/cpx/cpy packages @@ -1061,7 +1134,7 @@ def _missing_(cls: type["InstructionSet"], value: int) -> NoReturn: # Build opcode lookup map for variant dispatch # This maps int opcode values to InstructionOpcode objects with metadata -def _build_opcode_lookup() -> dict[int, InstructionOpcode]: +def _build_opcode_lookup() -> Dict[int, InstructionOpcode]: """Build lookup map from opcode values to InstructionOpcode objects. This allows converting fetched instruction bytes to InstructionOpcode objects diff --git a/mos6502/instructions/_bit/__init__.py b/mos6502/instructions/_bit/__init__.py index 8529d06..2e6be56 100644 --- a/mos6502/instructions/_bit/__init__.py +++ b/mos6502/instructions/_bit/__init__.py @@ -32,21 +32,33 @@ def add_bit_to_instruction_set_enum(instruction_set_class) -> None: """Add BIT instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (BIT_ZEROPAGE_0x24, "BIT_ZEROPAGE_0x24"), (BIT_ABSOLUTE_0x2C, "BIT_ABSOLUTE_0x2C"), diff --git a/mos6502/instructions/_bit/_bit_6502.py b/mos6502/instructions/_bit/_bit_6502.py index 393f607..e1fa5c0 100644 --- a/mos6502/instructions/_bit/_bit_6502.py +++ b/mos6502/instructions/_bit/_bit_6502.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 """BIT instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU @@ -14,7 +13,7 @@ BYTE_BIT_6_MASK = 0b01000000 -def bit_zeropage_0x24(cpu: MOS6502CPU) -> None: +def bit_zeropage_0x24(cpu: "MOS6502CPU") -> None: """Execute BIT (Test Bits in Memory with Accumulator) - Zeropage addressing mode. Opcode: 0x24 @@ -33,8 +32,8 @@ def bit_zeropage_0x24(cpu: MOS6502CPU) -> None: from mos6502 import flags # Bit Test - AND A with memory, set flags but don't store result - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) # Z flag set based on A AND memory result: int = cpu.A & value @@ -49,7 +48,7 @@ def bit_zeropage_0x24(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def bit_absolute_0x2c(cpu: MOS6502CPU) -> None: +def bit_absolute_0x2c(cpu: "MOS6502CPU") -> None: """Execute BIT (Test Bits in Memory with Accumulator) - Absolute addressing mode. Opcode: 0x2C @@ -68,8 +67,8 @@ def bit_absolute_0x2c(cpu: MOS6502CPU) -> None: from mos6502 import flags # Bit Test - AND A with memory, set flags but don't store result - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) # Z flag set based on A AND memory result: int = cpu.A & value diff --git a/mos6502/instructions/_brk/__init__.py b/mos6502/instructions/_brk/__init__.py index 1472ba2..97e9c28 100644 --- a/mos6502/instructions/_brk/__init__.py +++ b/mos6502/instructions/_brk/__init__.py @@ -34,21 +34,33 @@ def add_brk_to_instruction_set_enum(instruction_set_class) -> None: """Add BRK instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + brk_member = PseudoEnumMember(BRK_IMPLIED_0x00, "BRK_IMPLIED_0x00") instruction_set_class._value2member_map_[BRK_IMPLIED_0x00] = brk_member setattr(instruction_set_class, "BRK_IMPLIED_0x00", BRK_IMPLIED_0x00) diff --git a/mos6502/instructions/_brk/_brk_6502.py b/mos6502/instructions/_brk/_brk_6502.py index 01b21aa..703a639 100644 --- a/mos6502/instructions/_brk/_brk_6502.py +++ b/mos6502/instructions/_brk/_brk_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BRK instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def brk_implied_0x00(cpu: MOS6502CPU) -> None: +def brk_implied_0x00(cpu: "MOS6502CPU") -> None: """Execute BRK (Force Break) - Implied addressing mode. Opcode: 0x00 @@ -43,17 +42,17 @@ def brk_implied_0x00(cpu: MOS6502CPU) -> None: pc_low = return_addr & 0xFF # Push PC high byte first (6502 pushes high byte before low byte) - cpu.write_byte(address=cpu.S, data=pc_high) + cpu.write_byte(cpu.S, pc_high) cpu.S -= 1 # Push PC low byte - cpu.write_byte(address=cpu.S, data=pc_low) + cpu.write_byte(cpu.S, pc_low) cpu.S -= 1 # Push status register with B flag set # Create a copy of flags with B flag set status_with_break: Byte = Byte(cpu._flags.value | (1 << flags.B)) - cpu.write_byte(address=cpu.S, data=status_with_break) + cpu.write_byte(cpu.S, status_with_break) cpu.S -= 1 # Set interrupt disable flag @@ -62,10 +61,10 @@ def brk_implied_0x00(cpu: MOS6502CPU) -> None: # Load PC from IRQ vector at 0xFFFE/0xFFFF # Read the IRQ vector (2 cycles) irq_vector = cpu.peek_word(0xFFFE) - cpu.spend_cpu_cycles(cost=2) + cpu.spend_cpu_cycles(2) # Jump to IRQ handler (1 cycle) cpu.PC = irq_vector - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) cpu.log.info("i") diff --git a/mos6502/instructions/_brk/_brk_65c02.py b/mos6502/instructions/_brk/_brk_65c02.py index c7eaf6c..23dc41f 100644 --- a/mos6502/instructions/_brk/_brk_65c02.py +++ b/mos6502/instructions/_brk/_brk_65c02.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BRK instruction implementation for 65C02 variant.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def brk_implied_0x00(cpu: MOS6502CPU) -> None: +def brk_implied_0x00(cpu: "MOS6502CPU") -> None: """Execute BRK (Force Break) - Implied addressing mode - 65C02 variant. Opcode: 0x00 @@ -41,17 +40,17 @@ def brk_implied_0x00(cpu: MOS6502CPU) -> None: pc_low = return_addr & 0xFF # Push PC high byte first (6502 pushes high byte before low byte) - cpu.write_byte(address=cpu.S, data=pc_high) + cpu.write_byte(cpu.S, pc_high) cpu.S -= 1 # Push PC low byte - cpu.write_byte(address=cpu.S, data=pc_low) + cpu.write_byte(cpu.S, pc_low) cpu.S -= 1 # Push status register with B flag set # Create a copy of flags with B flag set status_with_break: Byte = Byte(cpu._flags.value | (1 << flags.B)) - cpu.write_byte(address=cpu.S, data=status_with_break) + cpu.write_byte(cpu.S, status_with_break) cpu.S -= 1 # Set interrupt disable flag @@ -64,10 +63,10 @@ def brk_implied_0x00(cpu: MOS6502CPU) -> None: # Load PC from IRQ vector at 0xFFFE/0xFFFF # Read the IRQ vector (2 cycles) irq_vector = cpu.peek_word(0xFFFE) - cpu.spend_cpu_cycles(cost=2) + cpu.spend_cpu_cycles(2) # Jump to IRQ handler (1 cycle) cpu.PC = irq_vector - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) cpu.log.info("i") diff --git a/mos6502/instructions/_nop/__init__.py b/mos6502/instructions/_nop/__init__.py index 96ad3a6..dff7b0a 100644 --- a/mos6502/instructions/_nop/__init__.py +++ b/mos6502/instructions/_nop/__init__.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """NOP (No Operation) instruction.""" -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -25,21 +24,33 @@ def add_nop_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + nop_member = PseudoEnumMember(NOP_IMPLIED_0xEA, "NOP_IMPLIED_0xEA") instruction_set_class._value2member_map_[NOP_IMPLIED_0xEA] = nop_member setattr(instruction_set_class, "NOP_IMPLIED_0xEA", NOP_IMPLIED_0xEA) diff --git a/mos6502/instructions/_nop/_nop_6502.py b/mos6502/instructions/_nop/_nop_6502.py index b36cca2..5f41641 100644 --- a/mos6502/instructions/_nop/_nop_6502.py +++ b/mos6502/instructions/_nop/_nop_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """NOP instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def nop_implied_0xea(cpu: MOS6502CPU) -> None: +def nop_implied_0xea(cpu: "MOS6502CPU") -> None: """Execute NOP (No Operation) - Implied addressing mode. Opcode: 0xEA @@ -26,4 +25,4 @@ def nop_implied_0xea(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/arithmetic/_adc/__init__.py b/mos6502/instructions/arithmetic/_adc/__init__.py index 924a207..8c22ca6 100644 --- a/mos6502/instructions/arithmetic/_adc/__init__.py +++ b/mos6502/instructions/arithmetic/_adc/__init__.py @@ -69,21 +69,33 @@ def add_adc_to_instruction_set_enum(instruction_set_class) -> None: """Add ADC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (ADC_IMMEDIATE_0x69, "ADC_IMMEDIATE_0x69"), (ADC_ZEROPAGE_0x65, "ADC_ZEROPAGE_0x65"), diff --git a/mos6502/instructions/arithmetic/_adc/_adc_6502.py b/mos6502/instructions/arithmetic/_adc/_adc_6502.py index 488d936..70a0588 100644 --- a/mos6502/instructions/arithmetic/_adc/_adc_6502.py +++ b/mos6502/instructions/arithmetic/_adc/_adc_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """ADC instruction implementation for NMOS 6502 variants (6502, 6502A, 6502C).""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def adc_immediate_0x69(cpu: MOS6502CPU) -> None: +def adc_immediate_0x69(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Immediate addressing mode. Opcode: 0x69 @@ -54,7 +53,7 @@ def adc_immediate_0x69(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def adc_zeropage_0x65(cpu: MOS6502CPU) -> None: +def adc_zeropage_0x65(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Zero Page addressing mode. Opcode: 0x65 @@ -66,8 +65,8 @@ def adc_zeropage_0x65(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -86,7 +85,7 @@ def adc_zeropage_0x65(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def adc_zeropage_x_0x75(cpu: MOS6502CPU) -> None: +def adc_zeropage_x_0x75(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Zero Page,X addressing mode. Opcode: 0x75 @@ -98,8 +97,8 @@ def adc_zeropage_x_0x75(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -118,7 +117,7 @@ def adc_zeropage_x_0x75(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def adc_absolute_0x6d(cpu: MOS6502CPU) -> None: +def adc_absolute_0x6d(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Absolute addressing mode. Opcode: 0x6D @@ -130,8 +129,8 @@ def adc_absolute_0x6d(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -150,7 +149,7 @@ def adc_absolute_0x6d(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def adc_absolute_x_0x7d(cpu: MOS6502CPU) -> None: +def adc_absolute_x_0x7d(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Absolute,X addressing mode. Opcode: 0x7D @@ -162,8 +161,8 @@ def adc_absolute_x_0x7d(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -182,7 +181,7 @@ def adc_absolute_x_0x7d(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def adc_absolute_y_0x79(cpu: MOS6502CPU) -> None: +def adc_absolute_y_0x79(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Absolute,Y addressing mode. Opcode: 0x79 @@ -194,8 +193,8 @@ def adc_absolute_y_0x79(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -214,7 +213,7 @@ def adc_absolute_y_0x79(cpu: MOS6502CPU) -> None: cpu.log.info("ay") -def adc_indexed_indirect_x_0x61(cpu: MOS6502CPU) -> None: +def adc_indexed_indirect_x_0x61(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Indexed Indirect (X) addressing mode. Opcode: 0x61 @@ -227,7 +226,7 @@ def adc_indexed_indirect_x_0x61(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -246,7 +245,7 @@ def adc_indexed_indirect_x_0x61(cpu: MOS6502CPU) -> None: cpu.log.info("ix") -def adc_indirect_indexed_y_0x71(cpu: MOS6502CPU) -> None: +def adc_indirect_indexed_y_0x71(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Indirect Indexed (Y) addressing mode. Opcode: 0x71 @@ -259,7 +258,7 @@ def adc_indirect_indexed_y_0x71(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) diff --git a/mos6502/instructions/arithmetic/_adc/_adc_65c02.py b/mos6502/instructions/arithmetic/_adc/_adc_65c02.py index 93a29a4..fc0b91c 100644 --- a/mos6502/instructions/arithmetic/_adc/_adc_65c02.py +++ b/mos6502/instructions/arithmetic/_adc/_adc_65c02.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """ADC instruction implementation for CMOS 65C02 variant.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def adc_immediate_0x69(cpu: MOS6502CPU) -> None: +def adc_immediate_0x69(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Immediate addressing mode - 65C02 variant. Opcode: 0x69 @@ -58,7 +57,7 @@ def adc_immediate_0x69(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def adc_zeropage_0x65(cpu: MOS6502CPU) -> None: +def adc_zeropage_0x65(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Zero Page addressing mode - 65C02 variant. Opcode: 0x65 @@ -70,8 +69,8 @@ def adc_zeropage_0x65(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -91,7 +90,7 @@ def adc_zeropage_0x65(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def adc_zeropage_x_0x75(cpu: MOS6502CPU) -> None: +def adc_zeropage_x_0x75(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Zero Page,X addressing mode - 65C02 variant. Opcode: 0x75 @@ -103,8 +102,8 @@ def adc_zeropage_x_0x75(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -124,7 +123,7 @@ def adc_zeropage_x_0x75(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def adc_absolute_0x6d(cpu: MOS6502CPU) -> None: +def adc_absolute_0x6d(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Absolute addressing mode - 65C02 variant. Opcode: 0x6D @@ -136,8 +135,8 @@ def adc_absolute_0x6d(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -157,7 +156,7 @@ def adc_absolute_0x6d(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def adc_absolute_x_0x7d(cpu: MOS6502CPU) -> None: +def adc_absolute_x_0x7d(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Absolute,X addressing mode - 65C02 variant. Opcode: 0x7D @@ -169,8 +168,8 @@ def adc_absolute_x_0x7d(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -190,7 +189,7 @@ def adc_absolute_x_0x7d(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def adc_absolute_y_0x79(cpu: MOS6502CPU) -> None: +def adc_absolute_y_0x79(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Absolute,Y addressing mode - 65C02 variant. Opcode: 0x79 @@ -202,8 +201,8 @@ def adc_absolute_y_0x79(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -223,7 +222,7 @@ def adc_absolute_y_0x79(cpu: MOS6502CPU) -> None: cpu.log.info("ay") -def adc_indexed_indirect_x_0x61(cpu: MOS6502CPU) -> None: +def adc_indexed_indirect_x_0x61(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Indexed Indirect (X) addressing mode - 65C02 variant. Opcode: 0x61 @@ -236,7 +235,7 @@ def adc_indexed_indirect_x_0x61(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -256,7 +255,7 @@ def adc_indexed_indirect_x_0x61(cpu: MOS6502CPU) -> None: cpu.log.info("ix") -def adc_indirect_indexed_y_0x71(cpu: MOS6502CPU) -> None: +def adc_indirect_indexed_y_0x71(cpu: "MOS6502CPU") -> None: """Execute ADC (Add with Carry) - Indirect Indexed (Y) addressing mode - 65C02 variant. Opcode: 0x71 @@ -269,7 +268,7 @@ def adc_indirect_indexed_y_0x71(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._adc_bcd(cpu.A, value, cpu.flags[flags.C]) diff --git a/mos6502/instructions/arithmetic/_dec/__init__.py b/mos6502/instructions/arithmetic/_dec/__init__.py index 92c9d0f..dc1dca2 100644 --- a/mos6502/instructions/arithmetic/_dec/__init__.py +++ b/mos6502/instructions/arithmetic/_dec/__init__.py @@ -41,21 +41,33 @@ def add_dec_to_instruction_set_enum(instruction_set_class) -> None: """Add DEC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (DEC_ZEROPAGE_0xC6, "DEC_ZEROPAGE_0xC6"), (DEC_ZEROPAGE_X_0xD6, "DEC_ZEROPAGE_X_0xD6"), diff --git a/mos6502/instructions/arithmetic/_dec/_dec_6502.py b/mos6502/instructions/arithmetic/_dec/_dec_6502.py index e88c3d7..0a965d7 100644 --- a/mos6502/instructions/arithmetic/_dec/_dec_6502.py +++ b/mos6502/instructions/arithmetic/_dec/_dec_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """DEC instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def dec_zeropage_0xc6(cpu: MOS6502CPU) -> None: +def dec_zeropage_0xc6(cpu: "MOS6502CPU") -> None: """Execute DEC (Decrement Memory) - Zero Page addressing mode. Opcode: 0xC6 @@ -22,11 +21,11 @@ def dec_zeropage_0xc6(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) result: int = (value - 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -35,7 +34,7 @@ def dec_zeropage_0xc6(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def dec_zeropage_x_0xd6(cpu: MOS6502CPU) -> None: +def dec_zeropage_x_0xd6(cpu: "MOS6502CPU") -> None: """Execute DEC (Decrement Memory) - Zero Page,X addressing mode. Opcode: 0xD6 @@ -44,11 +43,11 @@ def dec_zeropage_x_0xd6(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) result: int = (value - 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -57,7 +56,7 @@ def dec_zeropage_x_0xd6(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def dec_absolute_0xce(cpu: MOS6502CPU) -> None: +def dec_absolute_0xce(cpu: "MOS6502CPU") -> None: """Execute DEC (Decrement Memory) - Absolute addressing mode. Opcode: 0xCE @@ -66,14 +65,14 @@ def dec_absolute_0xce(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) # Read-Modify-Write operations have an internal processing cycle cpu.spend_cpu_cycles(1) result: int = (value - 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -82,7 +81,7 @@ def dec_absolute_0xce(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def dec_absolute_x_0xde(cpu: MOS6502CPU) -> None: +def dec_absolute_x_0xde(cpu: "MOS6502CPU") -> None: """Execute DEC (Decrement Memory) - Absolute,X addressing mode. Opcode: 0xDE @@ -91,18 +90,18 @@ def dec_absolute_x_0xde(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) result: int = (value - 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 diff --git a/mos6502/instructions/arithmetic/_dex/__init__.py b/mos6502/instructions/arithmetic/_dex/__init__.py index bf8432d..a52eda2 100644 --- a/mos6502/instructions/arithmetic/_dex/__init__.py +++ b/mos6502/instructions/arithmetic/_dex/__init__.py @@ -21,21 +21,33 @@ def add_dex_to_instruction_set_enum(instruction_set_class) -> None: """Add DEX instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + dex_member = PseudoEnumMember(DEX_IMPLIED_0xCA, "DEX_IMPLIED_0xCA") instruction_set_class._value2member_map_[DEX_IMPLIED_0xCA] = dex_member setattr(instruction_set_class, "DEX_IMPLIED_0xCA", DEX_IMPLIED_0xCA) diff --git a/mos6502/instructions/arithmetic/_dex/_dex_6502.py b/mos6502/instructions/arithmetic/_dex/_dex_6502.py index 9e1b19d..2dcf1de 100644 --- a/mos6502/instructions/arithmetic/_dex/_dex_6502.py +++ b/mos6502/instructions/arithmetic/_dex/_dex_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """DEX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def dex_implied_0xca(cpu: MOS6502CPU) -> None: +def dex_implied_0xca(cpu: "MOS6502CPU") -> None: """Execute DEX (Decrement Index X by One) - Implied addressing mode. Opcode: 0xCA @@ -26,6 +25,6 @@ def dex_implied_0xca(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.X = (cpu.X - 1) & 0xFF - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/arithmetic/_dey/__init__.py b/mos6502/instructions/arithmetic/_dey/__init__.py index 77c61ef..ed2fc16 100644 --- a/mos6502/instructions/arithmetic/_dey/__init__.py +++ b/mos6502/instructions/arithmetic/_dey/__init__.py @@ -21,21 +21,33 @@ def add_dey_to_instruction_set_enum(instruction_set_class) -> None: """Add DEY instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + dey_member = PseudoEnumMember(DEY_IMPLIED_0x88, "DEY_IMPLIED_0x88") instruction_set_class._value2member_map_[DEY_IMPLIED_0x88] = dey_member setattr(instruction_set_class, "DEY_IMPLIED_0x88", DEY_IMPLIED_0x88) diff --git a/mos6502/instructions/arithmetic/_dey/_dey_6502.py b/mos6502/instructions/arithmetic/_dey/_dey_6502.py index c4fcd3f..132dccc 100644 --- a/mos6502/instructions/arithmetic/_dey/_dey_6502.py +++ b/mos6502/instructions/arithmetic/_dey/_dey_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """DEY instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def dey_implied_0x88(cpu: MOS6502CPU) -> None: +def dey_implied_0x88(cpu: "MOS6502CPU") -> None: """Execute DEY (Decrement Index Y by One) - Implied addressing mode. Opcode: 0x88 @@ -26,6 +25,6 @@ def dey_implied_0x88(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.Y = (cpu.Y - 1) & 0xFF - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/arithmetic/_inc/__init__.py b/mos6502/instructions/arithmetic/_inc/__init__.py index 91953fd..f76063a 100644 --- a/mos6502/instructions/arithmetic/_inc/__init__.py +++ b/mos6502/instructions/arithmetic/_inc/__init__.py @@ -41,21 +41,33 @@ def add_inc_to_instruction_set_enum(instruction_set_class) -> None: """Add INC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (INC_ZEROPAGE_0xE6, "INC_ZEROPAGE_0xE6"), (INC_ZEROPAGE_X_0xF6, "INC_ZEROPAGE_X_0xF6"), diff --git a/mos6502/instructions/arithmetic/_inc/_inc_6502.py b/mos6502/instructions/arithmetic/_inc/_inc_6502.py index e4a9fce..abc443e 100644 --- a/mos6502/instructions/arithmetic/_inc/_inc_6502.py +++ b/mos6502/instructions/arithmetic/_inc/_inc_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """INC instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def inc_zeropage_0xe6(cpu: MOS6502CPU) -> None: +def inc_zeropage_0xe6(cpu: "MOS6502CPU") -> None: """Execute INC (Increment Memory) - Zero Page addressing mode. Opcode: 0xE6 @@ -22,11 +21,11 @@ def inc_zeropage_0xe6(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) result: int = (value + 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -35,7 +34,7 @@ def inc_zeropage_0xe6(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def inc_zeropage_x_0xf6(cpu: MOS6502CPU) -> None: +def inc_zeropage_x_0xf6(cpu: "MOS6502CPU") -> None: """Execute INC (Increment Memory) - Zero Page,X addressing mode. Opcode: 0xF6 @@ -44,11 +43,11 @@ def inc_zeropage_x_0xf6(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) result: int = (value + 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -57,7 +56,7 @@ def inc_zeropage_x_0xf6(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def inc_absolute_0xee(cpu: MOS6502CPU) -> None: +def inc_absolute_0xee(cpu: "MOS6502CPU") -> None: """Execute INC (Increment Memory) - Absolute addressing mode. Opcode: 0xEE @@ -66,14 +65,14 @@ def inc_absolute_0xee(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) # Read-Modify-Write operations have an internal processing cycle cpu.spend_cpu_cycles(1) result: int = (value + 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -82,7 +81,7 @@ def inc_absolute_0xee(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def inc_absolute_x_0xfe(cpu: MOS6502CPU) -> None: +def inc_absolute_x_0xfe(cpu: "MOS6502CPU") -> None: """Execute INC (Increment Memory) - Absolute,X addressing mode. Opcode: 0xFE @@ -91,18 +90,18 @@ def inc_absolute_x_0xfe(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) result: int = (value + 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 diff --git a/mos6502/instructions/arithmetic/_inx/__init__.py b/mos6502/instructions/arithmetic/_inx/__init__.py index d48d482..b10175f 100644 --- a/mos6502/instructions/arithmetic/_inx/__init__.py +++ b/mos6502/instructions/arithmetic/_inx/__init__.py @@ -21,21 +21,33 @@ def add_inx_to_instruction_set_enum(instruction_set_class) -> None: """Add INX instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + inx_member = PseudoEnumMember(INX_IMPLIED_0xE8, "INX_IMPLIED_0xE8") instruction_set_class._value2member_map_[INX_IMPLIED_0xE8] = inx_member setattr(instruction_set_class, "INX_IMPLIED_0xE8", INX_IMPLIED_0xE8) diff --git a/mos6502/instructions/arithmetic/_inx/_inx_6502.py b/mos6502/instructions/arithmetic/_inx/_inx_6502.py index 7be8689..0078199 100644 --- a/mos6502/instructions/arithmetic/_inx/_inx_6502.py +++ b/mos6502/instructions/arithmetic/_inx/_inx_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """INX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def inx_implied_0xe8(cpu: MOS6502CPU) -> None: +def inx_implied_0xe8(cpu: "MOS6502CPU") -> None: """Execute INX (Increment Index X by One) - Implied addressing mode. Opcode: 0xE8 @@ -26,6 +25,6 @@ def inx_implied_0xe8(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.X = (cpu.X + 1) & 0xFF - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/arithmetic/_iny/__init__.py b/mos6502/instructions/arithmetic/_iny/__init__.py index 6d73eb0..2fc8b6f 100644 --- a/mos6502/instructions/arithmetic/_iny/__init__.py +++ b/mos6502/instructions/arithmetic/_iny/__init__.py @@ -21,21 +21,33 @@ def add_iny_to_instruction_set_enum(instruction_set_class) -> None: """Add INY instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + iny_member = PseudoEnumMember(INY_IMPLIED_0xC8, "INY_IMPLIED_0xC8") instruction_set_class._value2member_map_[INY_IMPLIED_0xC8] = iny_member setattr(instruction_set_class, "INY_IMPLIED_0xC8", INY_IMPLIED_0xC8) diff --git a/mos6502/instructions/arithmetic/_iny/_iny_6502.py b/mos6502/instructions/arithmetic/_iny/_iny_6502.py index 2a7b4d9..08ab47c 100644 --- a/mos6502/instructions/arithmetic/_iny/_iny_6502.py +++ b/mos6502/instructions/arithmetic/_iny/_iny_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """INY instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def iny_implied_0xc8(cpu: MOS6502CPU) -> None: +def iny_implied_0xc8(cpu: "MOS6502CPU") -> None: """Execute INY (Increment Index Y by One) - Implied addressing mode. Opcode: 0xC8 @@ -26,6 +25,6 @@ def iny_implied_0xc8(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.Y = (cpu.Y + 1) & 0xFF - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/arithmetic/_sbc/__init__.py b/mos6502/instructions/arithmetic/_sbc/__init__.py index 19ab65b..01d9559 100644 --- a/mos6502/instructions/arithmetic/_sbc/__init__.py +++ b/mos6502/instructions/arithmetic/_sbc/__init__.py @@ -69,21 +69,33 @@ def add_sbc_to_instruction_set_enum(instruction_set_class) -> None: """Add SBC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (SBC_IMMEDIATE_0xE9, "SBC_IMMEDIATE_0xE9"), (SBC_ZEROPAGE_0xE5, "SBC_ZEROPAGE_0xE5"), diff --git a/mos6502/instructions/arithmetic/_sbc/_sbc_6502.py b/mos6502/instructions/arithmetic/_sbc/_sbc_6502.py index df432d6..4750f98 100644 --- a/mos6502/instructions/arithmetic/_sbc/_sbc_6502.py +++ b/mos6502/instructions/arithmetic/_sbc/_sbc_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """SBC instruction implementation for NMOS 6502 variants (6502, 6502A, 6502C).""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sbc_immediate_0xe9(cpu: MOS6502CPU) -> None: +def sbc_immediate_0xe9(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Immediate addressing mode. Opcode: 0xE9 @@ -54,7 +53,7 @@ def sbc_immediate_0xe9(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sbc_zeropage_0xe5(cpu: MOS6502CPU) -> None: +def sbc_zeropage_0xe5(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Zero Page addressing mode. Opcode: 0xE5 @@ -66,8 +65,8 @@ def sbc_zeropage_0xe5(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -86,7 +85,7 @@ def sbc_zeropage_0xe5(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def sbc_zeropage_x_0xf5(cpu: MOS6502CPU) -> None: +def sbc_zeropage_x_0xf5(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Zero Page,X addressing mode. Opcode: 0xF5 @@ -98,8 +97,8 @@ def sbc_zeropage_x_0xf5(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -118,7 +117,7 @@ def sbc_zeropage_x_0xf5(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def sbc_absolute_0xed(cpu: MOS6502CPU) -> None: +def sbc_absolute_0xed(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Absolute addressing mode. Opcode: 0xED @@ -130,8 +129,8 @@ def sbc_absolute_0xed(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -150,7 +149,7 @@ def sbc_absolute_0xed(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def sbc_absolute_x_0xfd(cpu: MOS6502CPU) -> None: +def sbc_absolute_x_0xfd(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Absolute,X addressing mode. Opcode: 0xFD @@ -162,8 +161,8 @@ def sbc_absolute_x_0xfd(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -182,7 +181,7 @@ def sbc_absolute_x_0xfd(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def sbc_absolute_y_0xf9(cpu: MOS6502CPU) -> None: +def sbc_absolute_y_0xf9(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Absolute,Y addressing mode. Opcode: 0xF9 @@ -194,8 +193,8 @@ def sbc_absolute_y_0xf9(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -214,7 +213,7 @@ def sbc_absolute_y_0xf9(cpu: MOS6502CPU) -> None: cpu.log.info("ay") -def sbc_indexed_indirect_x_0xe1(cpu: MOS6502CPU) -> None: +def sbc_indexed_indirect_x_0xe1(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Indexed Indirect (X) addressing mode. Opcode: 0xE1 @@ -227,7 +226,7 @@ def sbc_indexed_indirect_x_0xe1(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -246,7 +245,7 @@ def sbc_indexed_indirect_x_0xe1(cpu: MOS6502CPU) -> None: cpu.log.info("ix") -def sbc_indirect_indexed_y_0xf1(cpu: MOS6502CPU) -> None: +def sbc_indirect_indexed_y_0xf1(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Indirect Indexed (Y) addressing mode. Opcode: 0xF1 @@ -259,7 +258,7 @@ def sbc_indirect_indexed_y_0xf1(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, _ = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) diff --git a/mos6502/instructions/arithmetic/_sbc/_sbc_65c02.py b/mos6502/instructions/arithmetic/_sbc/_sbc_65c02.py index 334dae4..d59cc68 100644 --- a/mos6502/instructions/arithmetic/_sbc/_sbc_65c02.py +++ b/mos6502/instructions/arithmetic/_sbc/_sbc_65c02.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """SBC instruction implementation for CMOS 65C02 variant.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sbc_immediate_0xe9(cpu: MOS6502CPU) -> None: +def sbc_immediate_0xe9(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Immediate addressing mode - 65C02 variant. Opcode: 0xE9 @@ -58,7 +57,7 @@ def sbc_immediate_0xe9(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sbc_zeropage_0xe5(cpu: MOS6502CPU) -> None: +def sbc_zeropage_0xe5(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Zero Page addressing mode - 65C02 variant. Opcode: 0xE5 @@ -70,8 +69,8 @@ def sbc_zeropage_0xe5(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -91,7 +90,7 @@ def sbc_zeropage_0xe5(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def sbc_zeropage_x_0xf5(cpu: MOS6502CPU) -> None: +def sbc_zeropage_x_0xf5(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Zero Page,X addressing mode - 65C02 variant. Opcode: 0xF5 @@ -103,8 +102,8 @@ def sbc_zeropage_x_0xf5(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -124,7 +123,7 @@ def sbc_zeropage_x_0xf5(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def sbc_absolute_0xed(cpu: MOS6502CPU) -> None: +def sbc_absolute_0xed(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Absolute addressing mode - 65C02 variant. Opcode: 0xED @@ -136,8 +135,8 @@ def sbc_absolute_0xed(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -157,7 +156,7 @@ def sbc_absolute_0xed(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def sbc_absolute_x_0xfd(cpu: MOS6502CPU) -> None: +def sbc_absolute_x_0xfd(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Absolute,X addressing mode - 65C02 variant. Opcode: 0xFD @@ -169,8 +168,8 @@ def sbc_absolute_x_0xfd(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -190,7 +189,7 @@ def sbc_absolute_x_0xfd(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def sbc_absolute_y_0xf9(cpu: MOS6502CPU) -> None: +def sbc_absolute_y_0xf9(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Absolute,Y addressing mode - 65C02 variant. Opcode: 0xF9 @@ -202,8 +201,8 @@ def sbc_absolute_y_0xf9(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -223,7 +222,7 @@ def sbc_absolute_y_0xf9(cpu: MOS6502CPU) -> None: cpu.log.info("ay") -def sbc_indexed_indirect_x_0xe1(cpu: MOS6502CPU) -> None: +def sbc_indexed_indirect_x_0xe1(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Indexed Indirect (X) addressing mode - 65C02 variant. Opcode: 0xE1 @@ -236,7 +235,7 @@ def sbc_indexed_indirect_x_0xe1(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) @@ -256,7 +255,7 @@ def sbc_indexed_indirect_x_0xe1(cpu: MOS6502CPU) -> None: cpu.log.info("ix") -def sbc_indirect_indexed_y_0xf1(cpu: MOS6502CPU) -> None: +def sbc_indirect_indexed_y_0xf1(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Indirect Indexed (Y) addressing mode - 65C02 variant. Opcode: 0xF1 @@ -269,7 +268,7 @@ def sbc_indirect_indexed_y_0xf1(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) if cpu.flags[flags.D]: result, carry_out, overflow, binary_result = cpu._sbc_bcd(cpu.A, value, cpu.flags[flags.C]) diff --git a/mos6502/instructions/branch/_bcc/__init__.py b/mos6502/instructions/branch/_bcc/__init__.py index fb1a8be..8a16b5f 100644 --- a/mos6502/instructions/branch/_bcc/__init__.py +++ b/mos6502/instructions/branch/_bcc/__init__.py @@ -20,21 +20,33 @@ def add_bcc_to_instruction_set_enum(instruction_set_class) -> None: """Add BCC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BCC_RELATIVE_0x90, "BCC_RELATIVE_0x90") instruction_set_class._value2member_map_[BCC_RELATIVE_0x90] = member setattr(instruction_set_class, "BCC_RELATIVE_0x90", BCC_RELATIVE_0x90) diff --git a/mos6502/instructions/branch/_bcc/_bcc_6502.py b/mos6502/instructions/branch/_bcc/_bcc_6502.py index 79e0b14..63903a3 100644 --- a/mos6502/instructions/branch/_bcc/_bcc_6502.py +++ b/mos6502/instructions/branch/_bcc/_bcc_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BCC instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bcc_relative_0x90(cpu: MOS6502CPU) -> None: +def bcc_relative_0x90(cpu: "MOS6502CPU") -> None: """Execute BCC (Branch on Carry Clear) - Relative addressing mode. Opcode: 0x90 diff --git a/mos6502/instructions/branch/_bcs/__init__.py b/mos6502/instructions/branch/_bcs/__init__.py index c7ece33..ad0f2bf 100644 --- a/mos6502/instructions/branch/_bcs/__init__.py +++ b/mos6502/instructions/branch/_bcs/__init__.py @@ -19,21 +19,33 @@ def add_bcs_to_instruction_set_enum(instruction_set_class) -> None: """Add BCS instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BCS_RELATIVE_0xB0, "BCS_RELATIVE_0xB0") instruction_set_class._value2member_map_[BCS_RELATIVE_0xB0] = member setattr(instruction_set_class, "BCS_RELATIVE_0xB0", BCS_RELATIVE_0xB0) diff --git a/mos6502/instructions/branch/_bcs/_bcs_6502.py b/mos6502/instructions/branch/_bcs/_bcs_6502.py index d23d6b9..de32c2d 100644 --- a/mos6502/instructions/branch/_bcs/_bcs_6502.py +++ b/mos6502/instructions/branch/_bcs/_bcs_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BCS instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bcs_relative_0xb0(cpu: MOS6502CPU) -> None: +def bcs_relative_0xb0(cpu: "MOS6502CPU") -> None: """Execute BCS (Branch on Carry Set) - Relative addressing mode. Opcode: 0xB0 diff --git a/mos6502/instructions/branch/_beq/__init__.py b/mos6502/instructions/branch/_beq/__init__.py index 2ccf6cb..606f8a7 100644 --- a/mos6502/instructions/branch/_beq/__init__.py +++ b/mos6502/instructions/branch/_beq/__init__.py @@ -19,21 +19,33 @@ def add_beq_to_instruction_set_enum(instruction_set_class) -> None: """Add BEQ instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BEQ_RELATIVE_0xF0, "BEQ_RELATIVE_0xF0") instruction_set_class._value2member_map_[BEQ_RELATIVE_0xF0] = member setattr(instruction_set_class, "BEQ_RELATIVE_0xF0", BEQ_RELATIVE_0xF0) diff --git a/mos6502/instructions/branch/_beq/_beq_6502.py b/mos6502/instructions/branch/_beq/_beq_6502.py index 9eb2a9a..56ef59f 100644 --- a/mos6502/instructions/branch/_beq/_beq_6502.py +++ b/mos6502/instructions/branch/_beq/_beq_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BEQ instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def beq_relative_0xf0(cpu: MOS6502CPU) -> None: +def beq_relative_0xf0(cpu: "MOS6502CPU") -> None: """Execute BEQ (Branch on Equal/Zero) - Relative addressing mode. Opcode: 0xF0 diff --git a/mos6502/instructions/branch/_bmi/__init__.py b/mos6502/instructions/branch/_bmi/__init__.py index 46200b1..844741a 100644 --- a/mos6502/instructions/branch/_bmi/__init__.py +++ b/mos6502/instructions/branch/_bmi/__init__.py @@ -19,21 +19,33 @@ def add_bmi_to_instruction_set_enum(instruction_set_class) -> None: """Add BMI instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BMI_RELATIVE_0x30, "BMI_RELATIVE_0x30") instruction_set_class._value2member_map_[BMI_RELATIVE_0x30] = member setattr(instruction_set_class, "BMI_RELATIVE_0x30", BMI_RELATIVE_0x30) diff --git a/mos6502/instructions/branch/_bmi/_bmi_6502.py b/mos6502/instructions/branch/_bmi/_bmi_6502.py index 836eec6..a4d2d7e 100644 --- a/mos6502/instructions/branch/_bmi/_bmi_6502.py +++ b/mos6502/instructions/branch/_bmi/_bmi_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BMI instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bmi_relative_0x30(cpu: MOS6502CPU) -> None: +def bmi_relative_0x30(cpu: "MOS6502CPU") -> None: """Execute BMI (Branch on Minus/Negative) - Relative addressing mode. Opcode: 0x30 diff --git a/mos6502/instructions/branch/_bne/__init__.py b/mos6502/instructions/branch/_bne/__init__.py index 22fd1a8..78f1b20 100644 --- a/mos6502/instructions/branch/_bne/__init__.py +++ b/mos6502/instructions/branch/_bne/__init__.py @@ -19,21 +19,33 @@ def add_bne_to_instruction_set_enum(instruction_set_class) -> None: """Add BNE instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BNE_RELATIVE_0xD0, "BNE_RELATIVE_0xD0") instruction_set_class._value2member_map_[BNE_RELATIVE_0xD0] = member setattr(instruction_set_class, "BNE_RELATIVE_0xD0", BNE_RELATIVE_0xD0) diff --git a/mos6502/instructions/branch/_bne/_bne_6502.py b/mos6502/instructions/branch/_bne/_bne_6502.py index de1625f..ef5e0bd 100644 --- a/mos6502/instructions/branch/_bne/_bne_6502.py +++ b/mos6502/instructions/branch/_bne/_bne_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BNE instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bne_relative_0xd0(cpu: MOS6502CPU) -> None: +def bne_relative_0xd0(cpu: "MOS6502CPU") -> None: """Execute BNE (Branch on Not Equal) - Relative addressing mode. Opcode: 0xD0 diff --git a/mos6502/instructions/branch/_bpl/__init__.py b/mos6502/instructions/branch/_bpl/__init__.py index ab633c1..ebcb15b 100644 --- a/mos6502/instructions/branch/_bpl/__init__.py +++ b/mos6502/instructions/branch/_bpl/__init__.py @@ -19,21 +19,33 @@ def add_bpl_to_instruction_set_enum(instruction_set_class) -> None: """Add BPL instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BPL_RELATIVE_0x10, "BPL_RELATIVE_0x10") instruction_set_class._value2member_map_[BPL_RELATIVE_0x10] = member setattr(instruction_set_class, "BPL_RELATIVE_0x10", BPL_RELATIVE_0x10) diff --git a/mos6502/instructions/branch/_bpl/_bpl_6502.py b/mos6502/instructions/branch/_bpl/_bpl_6502.py index 58e085b..34df0b3 100644 --- a/mos6502/instructions/branch/_bpl/_bpl_6502.py +++ b/mos6502/instructions/branch/_bpl/_bpl_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BPL instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bpl_relative_0x10(cpu: MOS6502CPU) -> None: +def bpl_relative_0x10(cpu: "MOS6502CPU") -> None: """Execute BPL (Branch on Plus/Positive) - Relative addressing mode. Opcode: 0x10 diff --git a/mos6502/instructions/branch/_bvc/__init__.py b/mos6502/instructions/branch/_bvc/__init__.py index ce327c0..8267540 100644 --- a/mos6502/instructions/branch/_bvc/__init__.py +++ b/mos6502/instructions/branch/_bvc/__init__.py @@ -19,21 +19,33 @@ def add_bvc_to_instruction_set_enum(instruction_set_class) -> None: """Add BVC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BVC_RELATIVE_0x50, "BVC_RELATIVE_0x50") instruction_set_class._value2member_map_[BVC_RELATIVE_0x50] = member setattr(instruction_set_class, "BVC_RELATIVE_0x50", BVC_RELATIVE_0x50) diff --git a/mos6502/instructions/branch/_bvc/_bvc_6502.py b/mos6502/instructions/branch/_bvc/_bvc_6502.py index 2e0c1e7..dd73453 100644 --- a/mos6502/instructions/branch/_bvc/_bvc_6502.py +++ b/mos6502/instructions/branch/_bvc/_bvc_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BVC instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bvc_relative_0x50(cpu: MOS6502CPU) -> None: +def bvc_relative_0x50(cpu: "MOS6502CPU") -> None: """Execute BVC (Branch on Overflow Clear) - Relative addressing mode. Opcode: 0x50 diff --git a/mos6502/instructions/branch/_bvs/__init__.py b/mos6502/instructions/branch/_bvs/__init__.py index 0a9b2bc..bcab4d0 100644 --- a/mos6502/instructions/branch/_bvs/__init__.py +++ b/mos6502/instructions/branch/_bvs/__init__.py @@ -19,21 +19,33 @@ def add_bvs_to_instruction_set_enum(instruction_set_class) -> None: """Add BVS instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(BVS_RELATIVE_0x70, "BVS_RELATIVE_0x70") instruction_set_class._value2member_map_[BVS_RELATIVE_0x70] = member setattr(instruction_set_class, "BVS_RELATIVE_0x70", BVS_RELATIVE_0x70) diff --git a/mos6502/instructions/branch/_bvs/_bvs_6502.py b/mos6502/instructions/branch/_bvs/_bvs_6502.py index 732e624..91382ae 100644 --- a/mos6502/instructions/branch/_bvs/_bvs_6502.py +++ b/mos6502/instructions/branch/_bvs/_bvs_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """BVS instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def bvs_relative_0x70(cpu: MOS6502CPU) -> None: +def bvs_relative_0x70(cpu: "MOS6502CPU") -> None: """Execute BVS (Branch on Overflow Set) - Relative addressing mode. Opcode: 0x70 diff --git a/mos6502/instructions/compare/_cmp/__init__.py b/mos6502/instructions/compare/_cmp/__init__.py index 3aa1a90..c84c23e 100644 --- a/mos6502/instructions/compare/_cmp/__init__.py +++ b/mos6502/instructions/compare/_cmp/__init__.py @@ -69,21 +69,33 @@ def add_cmp_to_instruction_set_enum(instruction_set_class) -> None: """Add CMP instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (CMP_IMMEDIATE_0xC9, "CMP_IMMEDIATE_0xC9"), (CMP_ZEROPAGE_0xC5, "CMP_ZEROPAGE_0xC5"), diff --git a/mos6502/instructions/compare/_cmp/_cmp_6502.py b/mos6502/instructions/compare/_cmp/_cmp_6502.py index 9458ff5..33d66c2 100644 --- a/mos6502/instructions/compare/_cmp/_cmp_6502.py +++ b/mos6502/instructions/compare/_cmp/_cmp_6502.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 """CMP instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU from mos6502 import flags -def cmp_immediate_0xc9(cpu: MOS6502CPU) -> None: +def cmp_immediate_0xc9(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - Immediate addressing mode. Opcode: 0xC9 @@ -38,7 +37,7 @@ def cmp_immediate_0xc9(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_zeropage_0xc5(cpu: MOS6502CPU) -> None: +def cmp_zeropage_0xc5(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - Zeropage addressing mode. Opcode: 0xC5 @@ -54,8 +53,8 @@ def cmp_zeropage_0xc5(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags @@ -66,7 +65,7 @@ def cmp_zeropage_0xc5(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_zeropage_x_0xd5(cpu: MOS6502CPU) -> None: +def cmp_zeropage_x_0xd5(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - Zeropage,X addressing mode. Opcode: 0xD5 @@ -82,8 +81,8 @@ def cmp_zeropage_x_0xd5(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags @@ -94,7 +93,7 @@ def cmp_zeropage_x_0xd5(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_absolute_0xcd(cpu: MOS6502CPU) -> None: +def cmp_absolute_0xcd(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - Absolute addressing mode. Opcode: 0xCD @@ -110,8 +109,8 @@ def cmp_absolute_0xcd(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags @@ -122,7 +121,7 @@ def cmp_absolute_0xcd(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_absolute_x_0xdd(cpu: MOS6502CPU) -> None: +def cmp_absolute_x_0xdd(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - Absolute,X addressing mode. Opcode: 0xDD @@ -138,8 +137,8 @@ def cmp_absolute_x_0xdd(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags @@ -150,7 +149,7 @@ def cmp_absolute_x_0xdd(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_absolute_y_0xd9(cpu: MOS6502CPU) -> None: +def cmp_absolute_y_0xd9(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - Absolute,Y addressing mode. Opcode: 0xD9 @@ -166,8 +165,8 @@ def cmp_absolute_y_0xd9(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags @@ -178,7 +177,7 @@ def cmp_absolute_y_0xd9(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_indexed_indirect_x_0xc1(cpu: MOS6502CPU) -> None: +def cmp_indexed_indirect_x_0xc1(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - (Indirect,X) addressing mode. Opcode: 0xC1 @@ -195,7 +194,7 @@ def cmp_indexed_indirect_x_0xc1(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags @@ -206,7 +205,7 @@ def cmp_indexed_indirect_x_0xc1(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cmp_indirect_indexed_y_0xd1(cpu: MOS6502CPU) -> None: +def cmp_indirect_indexed_y_0xd1(cpu: "MOS6502CPU") -> None: """Execute CMP (Compare Accumulator with Memory) - (Indirect),Y addressing mode. Opcode: 0xD1 @@ -223,7 +222,7 @@ def cmp_indirect_indexed_y_0xd1(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) result: int = (cpu.A - value) & 0xFF from mos6502 import flags diff --git a/mos6502/instructions/compare/_cpx/__init__.py b/mos6502/instructions/compare/_cpx/__init__.py index b78ad00..85055c4 100644 --- a/mos6502/instructions/compare/_cpx/__init__.py +++ b/mos6502/instructions/compare/_cpx/__init__.py @@ -34,21 +34,33 @@ def add_cpx_to_instruction_set_enum(instruction_set_class) -> None: """Add CPX instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (CPX_IMMEDIATE_0xE0, "CPX_IMMEDIATE_0xE0"), (CPX_ZEROPAGE_0xE4, "CPX_ZEROPAGE_0xE4"), diff --git a/mos6502/instructions/compare/_cpx/_cpx_6502.py b/mos6502/instructions/compare/_cpx/_cpx_6502.py index 163dca1..f862ca3 100644 --- a/mos6502/instructions/compare/_cpx/_cpx_6502.py +++ b/mos6502/instructions/compare/_cpx/_cpx_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """CPX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def cpx_immediate_0xe0(cpu: MOS6502CPU) -> None: +def cpx_immediate_0xe0(cpu: "MOS6502CPU") -> None: """Execute CPX (Compare X Register with Memory) - Immediate addressing mode. Opcode: 0xE0 @@ -37,7 +36,7 @@ def cpx_immediate_0xe0(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cpx_zeropage_0xe4(cpu: MOS6502CPU) -> None: +def cpx_zeropage_0xe4(cpu: "MOS6502CPU") -> None: """Execute CPX (Compare X Register with Memory) - Zeropage addressing mode. Opcode: 0xE4 @@ -53,8 +52,8 @@ def cpx_zeropage_0xe4(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) result: int = (cpu.X - value) & 0xFF from mos6502 import flags @@ -65,7 +64,7 @@ def cpx_zeropage_0xe4(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cpx_absolute_0xec(cpu: MOS6502CPU) -> None: +def cpx_absolute_0xec(cpu: "MOS6502CPU") -> None: """Execute CPX (Compare X Register with Memory) - Absolute addressing mode. Opcode: 0xEC @@ -81,8 +80,8 @@ def cpx_absolute_0xec(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) result: int = (cpu.X - value) & 0xFF from mos6502 import flags diff --git a/mos6502/instructions/compare/_cpy/__init__.py b/mos6502/instructions/compare/_cpy/__init__.py index de4cfb9..31371fb 100644 --- a/mos6502/instructions/compare/_cpy/__init__.py +++ b/mos6502/instructions/compare/_cpy/__init__.py @@ -34,21 +34,33 @@ def add_cpy_to_instruction_set_enum(instruction_set_class) -> None: """Add CPY instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (CPY_IMMEDIATE_0xC0, "CPY_IMMEDIATE_0xC0"), (CPY_ZEROPAGE_0xC4, "CPY_ZEROPAGE_0xC4"), diff --git a/mos6502/instructions/compare/_cpy/_cpy_6502.py b/mos6502/instructions/compare/_cpy/_cpy_6502.py index 9125e41..32a1148 100644 --- a/mos6502/instructions/compare/_cpy/_cpy_6502.py +++ b/mos6502/instructions/compare/_cpy/_cpy_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """CPY instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def cpy_immediate_0xc0(cpu: MOS6502CPU) -> None: +def cpy_immediate_0xc0(cpu: "MOS6502CPU") -> None: """Execute CPY (Compare Y Register with Memory) - Immediate addressing mode. Opcode: 0xC0 @@ -37,7 +36,7 @@ def cpy_immediate_0xc0(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cpy_zeropage_0xc4(cpu: MOS6502CPU) -> None: +def cpy_zeropage_0xc4(cpu: "MOS6502CPU") -> None: """Execute CPY (Compare Y Register with Memory) - Zeropage addressing mode. Opcode: 0xC4 @@ -53,8 +52,8 @@ def cpy_zeropage_0xc4(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) result: int = (cpu.Y - value) & 0xFF from mos6502 import flags @@ -65,7 +64,7 @@ def cpy_zeropage_0xc4(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def cpy_absolute_0xcc(cpu: MOS6502CPU) -> None: +def cpy_absolute_0xcc(cpu: "MOS6502CPU") -> None: """Execute CPY (Compare Y Register with Memory) - Absolute addressing mode. Opcode: 0xCC @@ -81,8 +80,8 @@ def cpy_absolute_0xcc(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) result: int = (cpu.Y - value) & 0xFF from mos6502 import flags diff --git a/mos6502/instructions/flags/_clc/__init__.py b/mos6502/instructions/flags/_clc/__init__.py index bb5d3ce..5041dba 100644 --- a/mos6502/instructions/flags/_clc/__init__.py +++ b/mos6502/instructions/flags/_clc/__init__.py @@ -24,21 +24,33 @@ def add_clc_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + clc_member = PseudoEnumMember(CLC_IMPLIED_0x18, "CLC_IMPLIED_0x18") instruction_set_class._value2member_map_[CLC_IMPLIED_0x18] = clc_member setattr(instruction_set_class, "CLC_IMPLIED_0x18", CLC_IMPLIED_0x18) diff --git a/mos6502/instructions/flags/_clc/_clc_6502.py b/mos6502/instructions/flags/_clc/_clc_6502.py index 006d6b7..e724cf4 100644 --- a/mos6502/instructions/flags/_clc/_clc_6502.py +++ b/mos6502/instructions/flags/_clc/_clc_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """CLC instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def clc_implied_0x18(cpu: MOS6502CPU) -> None: +def clc_implied_0x18(cpu: "MOS6502CPU") -> None: """Execute CLC (Clear Carry Flag) - Implied addressing mode. Opcode: 0x18 diff --git a/mos6502/instructions/flags/_cld/__init__.py b/mos6502/instructions/flags/_cld/__init__.py index e90097a..3f8dac8 100644 --- a/mos6502/instructions/flags/_cld/__init__.py +++ b/mos6502/instructions/flags/_cld/__init__.py @@ -24,21 +24,33 @@ def add_cld_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + cld_member = PseudoEnumMember(CLD_IMPLIED_0xD8, "CLD_IMPLIED_0xD8") instruction_set_class._value2member_map_[CLD_IMPLIED_0xD8] = cld_member setattr(instruction_set_class, "CLD_IMPLIED_0xD8", CLD_IMPLIED_0xD8) diff --git a/mos6502/instructions/flags/_cld/_cld_6502.py b/mos6502/instructions/flags/_cld/_cld_6502.py index 3eb439c..61c42c2 100644 --- a/mos6502/instructions/flags/_cld/_cld_6502.py +++ b/mos6502/instructions/flags/_cld/_cld_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """CLD instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def cld_implied_0xd8(cpu: MOS6502CPU) -> None: +def cld_implied_0xd8(cpu: "MOS6502CPU") -> None: """Execute CLD (Clear Decimal Mode) - Implied addressing mode. Opcode: 0xD8 diff --git a/mos6502/instructions/flags/_cli/__init__.py b/mos6502/instructions/flags/_cli/__init__.py index 8f7e57c..2e6332e 100644 --- a/mos6502/instructions/flags/_cli/__init__.py +++ b/mos6502/instructions/flags/_cli/__init__.py @@ -24,21 +24,33 @@ def add_cli_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + cli_member = PseudoEnumMember(CLI_IMPLIED_0x58, "CLI_IMPLIED_0x58") instruction_set_class._value2member_map_[CLI_IMPLIED_0x58] = cli_member setattr(instruction_set_class, "CLI_IMPLIED_0x58", CLI_IMPLIED_0x58) diff --git a/mos6502/instructions/flags/_cli/_cli_6502.py b/mos6502/instructions/flags/_cli/_cli_6502.py index 8acdb21..68ba451 100644 --- a/mos6502/instructions/flags/_cli/_cli_6502.py +++ b/mos6502/instructions/flags/_cli/_cli_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """CLI instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def cli_implied_0x58(cpu: MOS6502CPU) -> None: +def cli_implied_0x58(cpu: "MOS6502CPU") -> None: """Execute CLI (Clear Interrupt Disable Bit) - Implied addressing mode. Opcode: 0x58 diff --git a/mos6502/instructions/flags/_clv/__init__.py b/mos6502/instructions/flags/_clv/__init__.py index 1b63bdc..088203b 100644 --- a/mos6502/instructions/flags/_clv/__init__.py +++ b/mos6502/instructions/flags/_clv/__init__.py @@ -24,21 +24,33 @@ def add_clv_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + clv_member = PseudoEnumMember(CLV_IMPLIED_0xB8, "CLV_IMPLIED_0xB8") instruction_set_class._value2member_map_[CLV_IMPLIED_0xB8] = clv_member setattr(instruction_set_class, "CLV_IMPLIED_0xB8", CLV_IMPLIED_0xB8) diff --git a/mos6502/instructions/flags/_clv/_clv_6502.py b/mos6502/instructions/flags/_clv/_clv_6502.py index 71ba546..0d26606 100644 --- a/mos6502/instructions/flags/_clv/_clv_6502.py +++ b/mos6502/instructions/flags/_clv/_clv_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """CLV instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def clv_implied_0xb8(cpu: MOS6502CPU) -> None: +def clv_implied_0xb8(cpu: "MOS6502CPU") -> None: """Execute CLV (Clear Overflow Flag) - Implied addressing mode. Opcode: 0xB8 diff --git a/mos6502/instructions/flags/_sec/__init__.py b/mos6502/instructions/flags/_sec/__init__.py index 892ee66..6d1031c 100644 --- a/mos6502/instructions/flags/_sec/__init__.py +++ b/mos6502/instructions/flags/_sec/__init__.py @@ -24,21 +24,33 @@ def add_sec_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + sec_member = PseudoEnumMember(SEC_IMPLIED_0x38, "SEC_IMPLIED_0x38") instruction_set_class._value2member_map_[SEC_IMPLIED_0x38] = sec_member setattr(instruction_set_class, "SEC_IMPLIED_0x38", SEC_IMPLIED_0x38) diff --git a/mos6502/instructions/flags/_sec/_sec_6502.py b/mos6502/instructions/flags/_sec/_sec_6502.py index aacb65f..683c139 100644 --- a/mos6502/instructions/flags/_sec/_sec_6502.py +++ b/mos6502/instructions/flags/_sec/_sec_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """SEC instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sec_implied_0x38(cpu: MOS6502CPU) -> None: +def sec_implied_0x38(cpu: "MOS6502CPU") -> None: """Execute SEC (Set Carry Flag) - Implied addressing mode. Opcode: 0x38 diff --git a/mos6502/instructions/flags/_sed/__init__.py b/mos6502/instructions/flags/_sed/__init__.py index 0b3f035..4df0f12 100644 --- a/mos6502/instructions/flags/_sed/__init__.py +++ b/mos6502/instructions/flags/_sed/__init__.py @@ -24,21 +24,33 @@ def add_sed_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + sed_member = PseudoEnumMember(SED_IMPLIED_0xF8, "SED_IMPLIED_0xF8") instruction_set_class._value2member_map_[SED_IMPLIED_0xF8] = sed_member setattr(instruction_set_class, "SED_IMPLIED_0xF8", SED_IMPLIED_0xF8) diff --git a/mos6502/instructions/flags/_sed/_sed_6502.py b/mos6502/instructions/flags/_sed/_sed_6502.py index 9eaa308..2fd9bcf 100644 --- a/mos6502/instructions/flags/_sed/_sed_6502.py +++ b/mos6502/instructions/flags/_sed/_sed_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """SED instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sed_implied_0xf8(cpu: MOS6502CPU) -> None: +def sed_implied_0xf8(cpu: "MOS6502CPU") -> None: """Execute SED (Set Decimal Flag) - Implied addressing mode. Opcode: 0xF8 diff --git a/mos6502/instructions/flags/_sei/__init__.py b/mos6502/instructions/flags/_sei/__init__.py index 449ec1d..7368396 100644 --- a/mos6502/instructions/flags/_sei/__init__.py +++ b/mos6502/instructions/flags/_sei/__init__.py @@ -24,21 +24,33 @@ def add_sei_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + sei_member = PseudoEnumMember(SEI_IMPLIED_0x78, "SEI_IMPLIED_0x78") instruction_set_class._value2member_map_[SEI_IMPLIED_0x78] = sei_member setattr(instruction_set_class, "SEI_IMPLIED_0x78", SEI_IMPLIED_0x78) diff --git a/mos6502/instructions/flags/_sei/_sei_6502.py b/mos6502/instructions/flags/_sei/_sei_6502.py index fec947c..b26a8b4 100644 --- a/mos6502/instructions/flags/_sei/_sei_6502.py +++ b/mos6502/instructions/flags/_sei/_sei_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """SEI instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sei_implied_0x78(cpu: MOS6502CPU) -> None: +def sei_implied_0x78(cpu: "MOS6502CPU") -> None: """Execute SEI (Set Interrupt Disable Status) - Implied addressing mode. Opcode: 0x78 diff --git a/mos6502/instructions/illegal/_alr/__init__.py b/mos6502/instructions/illegal/_alr/__init__.py index 37edf0d..d059f91 100644 --- a/mos6502/instructions/illegal/_alr/__init__.py +++ b/mos6502/instructions/illegal/_alr/__init__.py @@ -11,7 +11,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#ALR - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -26,21 +25,33 @@ def add_alr_to_instruction_set_enum(instruction_set_class) -> None: """Add ALR instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(ALR_IMMEDIATE_0x4B, "ALR_IMMEDIATE_0x4B") instruction_set_class._value2member_map_[ALR_IMMEDIATE_0x4B] = member setattr(instruction_set_class, "ALR_IMMEDIATE_0x4B", ALR_IMMEDIATE_0x4B) diff --git a/mos6502/instructions/illegal/_alr/_alr_6502.py b/mos6502/instructions/illegal/_alr/_alr_6502.py index 1e75a8d..c8ec486 100644 --- a/mos6502/instructions/illegal/_alr/_alr_6502.py +++ b/mos6502/instructions/illegal/_alr/_alr_6502.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def alr_immediate_0x4b(cpu: MOS6502CPU) -> None: +def alr_immediate_0x4b(cpu: "MOS6502CPU") -> None: """Execute ALR (AND then Logical Shift Right) - Immediate addressing mode. Opcode: 0x4B diff --git a/mos6502/instructions/illegal/_alr/_alr_65c02.py b/mos6502/instructions/illegal/_alr/_alr_65c02.py index d98ca30..683511a 100644 --- a/mos6502/instructions/illegal/_alr/_alr_65c02.py +++ b/mos6502/instructions/illegal/_alr/_alr_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def alr_immediate_0x4b(cpu: MOS6502CPU) -> None: +def alr_immediate_0x4b(cpu: "MOS6502CPU") -> None: """Execute ALR (AND then Logical Shift Right) - Immediate addressing mode. Opcode: 0x4B diff --git a/mos6502/instructions/illegal/_anc/__init__.py b/mos6502/instructions/illegal/_anc/__init__.py index 500d43f..57c8da0 100644 --- a/mos6502/instructions/illegal/_anc/__init__.py +++ b/mos6502/instructions/illegal/_anc/__init__.py @@ -11,7 +11,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#ANC - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -28,21 +27,33 @@ def add_anc_to_instruction_set_enum(instruction_set_class) -> None: """Add ANC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for opcode_name, opcode_value in [ ("ANC_IMMEDIATE_0x0B", ANC_IMMEDIATE_0x0B), ("ANC_IMMEDIATE_0x2B", ANC_IMMEDIATE_0x2B), diff --git a/mos6502/instructions/illegal/_anc/_anc_6502.py b/mos6502/instructions/illegal/_anc/_anc_6502.py index 8e3b711..f76f19b 100644 --- a/mos6502/instructions/illegal/_anc/_anc_6502.py +++ b/mos6502/instructions/illegal/_anc/_anc_6502.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def anc_immediate_0x0b(cpu: MOS6502CPU) -> None: +def anc_immediate_0x0b(cpu: "MOS6502CPU") -> None: """Execute ANC (AND with Carry) - Immediate addressing mode. Opcode: 0x0B @@ -55,7 +54,7 @@ def anc_immediate_0x0b(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def anc_immediate_0x2b(cpu: MOS6502CPU) -> None: +def anc_immediate_0x2b(cpu: "MOS6502CPU") -> None: """Execute ANC (AND with Carry) - Immediate addressing mode (duplicate). Opcode: 0x2B diff --git a/mos6502/instructions/illegal/_anc/_anc_65c02.py b/mos6502/instructions/illegal/_anc/_anc_65c02.py index 4156f19..3bb9b71 100644 --- a/mos6502/instructions/illegal/_anc/_anc_65c02.py +++ b/mos6502/instructions/illegal/_anc/_anc_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def anc_immediate_0x0b(cpu: MOS6502CPU) -> None: +def anc_immediate_0x0b(cpu: "MOS6502CPU") -> None: """Execute ANC (AND with Carry) - Immediate addressing mode. Opcode: 0x0B @@ -39,7 +38,7 @@ def anc_immediate_0x0b(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def anc_immediate_0x2b(cpu: MOS6502CPU) -> None: +def anc_immediate_0x2b(cpu: "MOS6502CPU") -> None: """Execute ANC (AND with Carry) - Immediate addressing mode (duplicate). Opcode: 0x2B diff --git a/mos6502/instructions/illegal/_ane/__init__.py b/mos6502/instructions/illegal/_ane/__init__.py index 60e4275..ba7aff3 100644 --- a/mos6502/instructions/illegal/_ane/__init__.py +++ b/mos6502/instructions/illegal/_ane/__init__.py @@ -24,7 +24,6 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes#Highly_unstable_opcodes - https://csdb.dk/release/?id=198357 (Visual 6502 analysis) """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -42,21 +41,33 @@ def add_ane_to_instruction_set_enum(instruction_set_class) -> None: """Add ANE instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(ANE_IMMEDIATE_0x8B, "ANE_IMMEDIATE_0x8B") instruction_set_class._value2member_map_[ANE_IMMEDIATE_0x8B] = member setattr(instruction_set_class, "ANE_IMMEDIATE_0x8B", ANE_IMMEDIATE_0x8B) diff --git a/mos6502/instructions/illegal/_ane/_ane_6502.py b/mos6502/instructions/illegal/_ane/_ane_6502.py index 1f410c3..6b3da05 100644 --- a/mos6502/instructions/illegal/_ane/_ane_6502.py +++ b/mos6502/instructions/illegal/_ane/_ane_6502.py @@ -18,15 +18,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes#Highly_unstable_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def ane_immediate_0x8b(cpu: MOS6502CPU) -> None: +def ane_immediate_0x8b(cpu: "MOS6502CPU") -> None: """Execute ANE (XAA) - Immediate addressing mode. Opcode: 0x8B diff --git a/mos6502/instructions/illegal/_ane/_ane_65c02.py b/mos6502/instructions/illegal/_ane/_ane_65c02.py index c3ce4a4..9adbac6 100644 --- a/mos6502/instructions/illegal/_ane/_ane_65c02.py +++ b/mos6502/instructions/illegal/_ane/_ane_65c02.py @@ -12,15 +12,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def ane_immediate_0x8b(cpu: MOS6502CPU) -> None: +def ane_immediate_0x8b(cpu: "MOS6502CPU") -> None: """Execute ANE (XAA) - Immediate addressing mode - 65C02 variant. Opcode: 0x8B diff --git a/mos6502/instructions/illegal/_arr/__init__.py b/mos6502/instructions/illegal/_arr/__init__.py index 78c0e97..2762a0c 100644 --- a/mos6502/instructions/illegal/_arr/__init__.py +++ b/mos6502/instructions/illegal/_arr/__init__.py @@ -12,7 +12,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#ARR - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -27,21 +26,33 @@ def add_arr_to_instruction_set_enum(instruction_set_class) -> None: """Add ARR instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(ARR_IMMEDIATE_0x6B, "ARR_IMMEDIATE_0x6B") instruction_set_class._value2member_map_[ARR_IMMEDIATE_0x6B] = member setattr(instruction_set_class, "ARR_IMMEDIATE_0x6B", ARR_IMMEDIATE_0x6B) diff --git a/mos6502/instructions/illegal/_arr/_arr_6502.py b/mos6502/instructions/illegal/_arr/_arr_6502.py index 1fcdae2..c4159f8 100644 --- a/mos6502/instructions/illegal/_arr/_arr_6502.py +++ b/mos6502/instructions/illegal/_arr/_arr_6502.py @@ -14,15 +14,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def arr_immediate_0x6b(cpu: MOS6502CPU) -> None: +def arr_immediate_0x6b(cpu: "MOS6502CPU") -> None: """Execute ARR (AND then Rotate Right) - Immediate addressing mode. Opcode: 0x6B diff --git a/mos6502/instructions/illegal/_arr/_arr_65c02.py b/mos6502/instructions/illegal/_arr/_arr_65c02.py index 0a6e4b3..dd14fb4 100644 --- a/mos6502/instructions/illegal/_arr/_arr_65c02.py +++ b/mos6502/instructions/illegal/_arr/_arr_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def arr_immediate_0x6b(cpu: MOS6502CPU) -> None: +def arr_immediate_0x6b(cpu: "MOS6502CPU") -> None: """Execute ARR (AND then Rotate Right) - Immediate addressing mode. Opcode: 0x6B diff --git a/mos6502/instructions/illegal/_dcp/__init__.py b/mos6502/instructions/illegal/_dcp/__init__.py index 9cdefbf..23c369e 100644 --- a/mos6502/instructions/illegal/_dcp/__init__.py +++ b/mos6502/instructions/illegal/_dcp/__init__.py @@ -9,7 +9,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/Programming_with_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -100,21 +99,33 @@ def add_dcp_to_instruction_set_enum(instruction_set_class) -> None: """Add DCP instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + # Add each DCP variant to the enum for opcode_name, opcode_value in [ ("DCP_ZEROPAGE_0xC7", DCP_ZEROPAGE_0xC7), diff --git a/mos6502/instructions/illegal/_dcp/_dcp_6502.py b/mos6502/instructions/illegal/_dcp/_dcp_6502.py index fa41789..3fd7ea8 100644 --- a/mos6502/instructions/illegal/_dcp/_dcp_6502.py +++ b/mos6502/instructions/illegal/_dcp/_dcp_6502.py @@ -12,15 +12,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def dcp_zeropage_0xc7(cpu: MOS6502CPU) -> None: +def dcp_zeropage_0xc7(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Zero Page addressing mode. Opcode: 0xC7 @@ -42,16 +41,16 @@ def dcp_zeropage_0xc7(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch zero page address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Decrement value decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address, data=decremented) + cpu.write_byte(address, decremented) # Compare A with decremented value (like CMP) result: int = (int(cpu.A) - decremented) & 0xFF @@ -63,7 +62,7 @@ def dcp_zeropage_0xc7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_zeropage_x_0xd7(cpu: MOS6502CPU) -> None: +def dcp_zeropage_x_0xd7(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Zero Page,X addressing mode. Opcode: 0xD7 @@ -85,16 +84,16 @@ def dcp_zeropage_x_0xd7(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch zero page,X address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Decrement value decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address, data=decremented) + cpu.write_byte(address, decremented) # Compare A with decremented value result: int = (int(cpu.A) - decremented) & 0xFF @@ -106,7 +105,7 @@ def dcp_zeropage_x_0xd7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_indexed_indirect_x_0xc3(cpu: MOS6502CPU) -> None: +def dcp_indexed_indirect_x_0xc3(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - (Indirect,X) addressing mode. Opcode: 0xC3 @@ -131,13 +130,13 @@ def dcp_indexed_indirect_x_0xc3(cpu: MOS6502CPU) -> None: address: int = cpu.fetch_indexed_indirect_mode_address() # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Decrement value decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=decremented) + cpu.write_byte(address & 0xFFFF, decremented) # Compare A with decremented value result: int = (int(cpu.A) - decremented) & 0xFF @@ -149,7 +148,7 @@ def dcp_indexed_indirect_x_0xc3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_indirect_indexed_y_0xd3(cpu: MOS6502CPU) -> None: +def dcp_indirect_indexed_y_0xd3(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - (Indirect),Y addressing mode. Opcode: 0xD3 @@ -174,13 +173,13 @@ def dcp_indirect_indexed_y_0xd3(cpu: MOS6502CPU) -> None: address: int = cpu.fetch_indirect_indexed_mode_address() # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Decrement value decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=decremented) + cpu.write_byte(address & 0xFFFF, decremented) # Compare A with decremented value result: int = (int(cpu.A) - decremented) & 0xFF @@ -192,7 +191,7 @@ def dcp_indirect_indexed_y_0xd3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_absolute_0xcf(cpu: MOS6502CPU) -> None: +def dcp_absolute_0xcf(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Absolute addressing mode. Opcode: 0xCF @@ -214,16 +213,16 @@ def dcp_absolute_0xcf(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch absolute address - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) + address: int = cpu.fetch_absolute_mode_address(None) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Decrement value decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=decremented) + cpu.write_byte(address & 0xFFFF, decremented) # Compare A with decremented value result: int = (int(cpu.A) - decremented) & 0xFF @@ -235,7 +234,7 @@ def dcp_absolute_0xcf(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_absolute_x_0xdf(cpu: MOS6502CPU) -> None: +def dcp_absolute_x_0xdf(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Absolute,X addressing mode. Opcode: 0xDF @@ -257,13 +256,13 @@ def dcp_absolute_x_0xdf(cpu: MOS6502CPU) -> None: from mos6502 import flags # Use existing helper for absolute X addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -272,7 +271,7 @@ def dcp_absolute_x_0xdf(cpu: MOS6502CPU) -> None: decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=decremented) + cpu.write_byte(address & 0xFFFF, decremented) # Compare A with decremented value result: int = (int(cpu.A) - decremented) & 0xFF @@ -283,7 +282,7 @@ def dcp_absolute_x_0xdf(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def dcp_absolute_y_0xdb(cpu: MOS6502CPU) -> None: +def dcp_absolute_y_0xdb(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Absolute,Y addressing mode. Opcode: 0xDB @@ -305,13 +304,13 @@ def dcp_absolute_y_0xdb(cpu: MOS6502CPU) -> None: from mos6502 import flags # Use existing helper for absolute Y addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Read-Modify-Write with Absolute,Y always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) # Read value from memory - value: int = cpu.read_byte(address=address & 0xFFFF) + value: int = cpu.read_byte(address & 0xFFFF) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -320,7 +319,7 @@ def dcp_absolute_y_0xdb(cpu: MOS6502CPU) -> None: decremented: int = (value - 1) & 0xFF # Write decremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=decremented) + cpu.write_byte(address & 0xFFFF, decremented) # Compare A with decremented value result: int = (int(cpu.A) - decremented) & 0xFF diff --git a/mos6502/instructions/illegal/_dcp/_dcp_65c02.py b/mos6502/instructions/illegal/_dcp/_dcp_65c02.py index 159d869..63208fa 100644 --- a/mos6502/instructions/illegal/_dcp/_dcp_65c02.py +++ b/mos6502/instructions/illegal/_dcp/_dcp_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def dcp_zeropage_0xc7(cpu: MOS6502CPU) -> None: +def dcp_zeropage_0xc7(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Zero Page addressing mode. Opcode: 0xC7 @@ -44,7 +43,7 @@ def dcp_zeropage_0xc7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_zeropage_x_0xd7(cpu: MOS6502CPU) -> None: +def dcp_zeropage_x_0xd7(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Zero Page,X addressing mode. Opcode: 0xD7 @@ -70,7 +69,7 @@ def dcp_zeropage_x_0xd7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_indexed_indirect_x_0xc3(cpu: MOS6502CPU) -> None: +def dcp_indexed_indirect_x_0xc3(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - (Indirect,X) addressing mode. Opcode: 0xC3 @@ -98,7 +97,7 @@ def dcp_indexed_indirect_x_0xc3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_indirect_indexed_y_0xd3(cpu: MOS6502CPU) -> None: +def dcp_indirect_indexed_y_0xd3(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - (Indirect),Y addressing mode. Opcode: 0xD3 @@ -126,7 +125,7 @@ def dcp_indirect_indexed_y_0xd3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_absolute_0xcf(cpu: MOS6502CPU) -> None: +def dcp_absolute_0xcf(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Absolute addressing mode. Opcode: 0xCF @@ -152,7 +151,7 @@ def dcp_absolute_0xcf(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_absolute_x_0xdf(cpu: MOS6502CPU) -> None: +def dcp_absolute_x_0xdf(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Absolute,X addressing mode. Opcode: 0xDF @@ -179,7 +178,7 @@ def dcp_absolute_x_0xdf(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def dcp_absolute_y_0xdb(cpu: MOS6502CPU) -> None: +def dcp_absolute_y_0xdb(cpu: "MOS6502CPU") -> None: """Execute DCP (Decrement and Compare) - Absolute,Y addressing mode. Opcode: 0xDB diff --git a/mos6502/instructions/illegal/_isc/__init__.py b/mos6502/instructions/illegal/_isc/__init__.py index 57da2cf..2dafdc9 100644 --- a/mos6502/instructions/illegal/_isc/__init__.py +++ b/mos6502/instructions/illegal/_isc/__init__.py @@ -11,7 +11,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/Programming_with_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -103,21 +102,33 @@ def add_isc_to_instruction_set_enum(instruction_set_class) -> None: """Add ISC instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + # Add each ISC variant to the enum for opcode_name, opcode_value in [ ("ISC_ZEROPAGE_0xE7", ISC_ZEROPAGE_0xE7), diff --git a/mos6502/instructions/illegal/_isc/_isc_6502.py b/mos6502/instructions/illegal/_isc/_isc_6502.py index a5bb35e..a6c37f8 100644 --- a/mos6502/instructions/illegal/_isc/_isc_6502.py +++ b/mos6502/instructions/illegal/_isc/_isc_6502.py @@ -12,15 +12,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def isc_zeropage_0xe7(cpu: MOS6502CPU) -> None: +def isc_zeropage_0xe7(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Zero Page addressing mode. Opcode: 0xE7 @@ -42,16 +41,16 @@ def isc_zeropage_0xe7(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch zero page address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Increment value incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address, data=incremented) + cpu.write_byte(address, incremented) # Subtract incremented value from A with carry (like SBC) if cpu.flags[flags.D]: @@ -72,7 +71,7 @@ def isc_zeropage_0xe7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_zeropage_x_0xf7(cpu: MOS6502CPU) -> None: +def isc_zeropage_x_0xf7(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Zero Page,X addressing mode. Opcode: 0xF7 @@ -94,16 +93,16 @@ def isc_zeropage_x_0xf7(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch zero page,X address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Increment value incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address, data=incremented) + cpu.write_byte(address, incremented) # Subtract incremented value from A with carry if cpu.flags[flags.D]: @@ -124,7 +123,7 @@ def isc_zeropage_x_0xf7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_indexed_indirect_x_0xe3(cpu: MOS6502CPU) -> None: +def isc_indexed_indirect_x_0xe3(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - (Indirect,X) addressing mode. Opcode: 0xE3 @@ -149,13 +148,13 @@ def isc_indexed_indirect_x_0xe3(cpu: MOS6502CPU) -> None: address: int = cpu.fetch_indexed_indirect_mode_address() # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Increment value incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=incremented) + cpu.write_byte(address & 0xFFFF, incremented) # Subtract incremented value from A with carry if cpu.flags[flags.D]: @@ -176,7 +175,7 @@ def isc_indexed_indirect_x_0xe3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_indirect_indexed_y_0xf3(cpu: MOS6502CPU) -> None: +def isc_indirect_indexed_y_0xf3(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - (Indirect),Y addressing mode. Opcode: 0xF3 @@ -201,13 +200,13 @@ def isc_indirect_indexed_y_0xf3(cpu: MOS6502CPU) -> None: address: int = cpu.fetch_indirect_indexed_mode_address() # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Increment value incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=incremented) + cpu.write_byte(address & 0xFFFF, incremented) # Subtract incremented value from A with carry if cpu.flags[flags.D]: @@ -228,7 +227,7 @@ def isc_indirect_indexed_y_0xf3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_absolute_0xef(cpu: MOS6502CPU) -> None: +def isc_absolute_0xef(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Absolute addressing mode. Opcode: 0xEF @@ -250,16 +249,16 @@ def isc_absolute_0xef(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch absolute address - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) + address: int = cpu.fetch_absolute_mode_address(None) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Increment value incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=incremented) + cpu.write_byte(address & 0xFFFF, incremented) # Subtract incremented value from A with carry if cpu.flags[flags.D]: @@ -280,7 +279,7 @@ def isc_absolute_0xef(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_absolute_x_0xff(cpu: MOS6502CPU) -> None: +def isc_absolute_x_0xff(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Absolute,X addressing mode. Opcode: 0xFF @@ -302,13 +301,13 @@ def isc_absolute_x_0xff(cpu: MOS6502CPU) -> None: from mos6502 import flags # Use existing helper for absolute X addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -317,7 +316,7 @@ def isc_absolute_x_0xff(cpu: MOS6502CPU) -> None: incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=incremented) + cpu.write_byte(address & 0xFFFF, incremented) # Subtract incremented value from A with carry if cpu.flags[flags.D]: @@ -337,7 +336,7 @@ def isc_absolute_x_0xff(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def isc_absolute_y_0xfb(cpu: MOS6502CPU) -> None: +def isc_absolute_y_0xfb(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Absolute,Y addressing mode. Opcode: 0xFB @@ -359,13 +358,13 @@ def isc_absolute_y_0xfb(cpu: MOS6502CPU) -> None: from mos6502 import flags # Use existing helper for absolute Y addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Read-Modify-Write with Absolute,Y always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) # Read value from memory - value: int = cpu.read_byte(address=address & 0xFFFF) + value: int = cpu.read_byte(address & 0xFFFF) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -374,7 +373,7 @@ def isc_absolute_y_0xfb(cpu: MOS6502CPU) -> None: incremented: int = (value + 1) & 0xFF # Write incremented value back to memory - cpu.write_byte(address=address & 0xFFFF, data=incremented) + cpu.write_byte(address & 0xFFFF, incremented) # Subtract incremented value from A with carry if cpu.flags[flags.D]: diff --git a/mos6502/instructions/illegal/_isc/_isc_65c02.py b/mos6502/instructions/illegal/_isc/_isc_65c02.py index 2edf7e9..3547edf 100644 --- a/mos6502/instructions/illegal/_isc/_isc_65c02.py +++ b/mos6502/instructions/illegal/_isc/_isc_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def isc_zeropage_0xe7(cpu: MOS6502CPU) -> None: +def isc_zeropage_0xe7(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Zero Page addressing mode. Opcode: 0xE7 @@ -44,7 +43,7 @@ def isc_zeropage_0xe7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_zeropage_x_0xf7(cpu: MOS6502CPU) -> None: +def isc_zeropage_x_0xf7(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Zero Page,X addressing mode. Opcode: 0xF7 @@ -70,7 +69,7 @@ def isc_zeropage_x_0xf7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_indexed_indirect_x_0xe3(cpu: MOS6502CPU) -> None: +def isc_indexed_indirect_x_0xe3(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - (Indirect,X) addressing mode. Opcode: 0xE3 @@ -98,7 +97,7 @@ def isc_indexed_indirect_x_0xe3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_indirect_indexed_y_0xf3(cpu: MOS6502CPU) -> None: +def isc_indirect_indexed_y_0xf3(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - (Indirect),Y addressing mode. Opcode: 0xF3 @@ -126,7 +125,7 @@ def isc_indirect_indexed_y_0xf3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_absolute_0xef(cpu: MOS6502CPU) -> None: +def isc_absolute_0xef(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Absolute addressing mode. Opcode: 0xEF @@ -152,7 +151,7 @@ def isc_absolute_0xef(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_absolute_x_0xff(cpu: MOS6502CPU) -> None: +def isc_absolute_x_0xff(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Absolute,X addressing mode. Opcode: 0xFF @@ -179,7 +178,7 @@ def isc_absolute_x_0xff(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def isc_absolute_y_0xfb(cpu: MOS6502CPU) -> None: +def isc_absolute_y_0xfb(cpu: "MOS6502CPU") -> None: """Execute ISC (Increment and Subtract with Carry) - Absolute,Y addressing mode. Opcode: 0xFB diff --git a/mos6502/instructions/illegal/_jam/__init__.py b/mos6502/instructions/illegal/_jam/__init__.py index 934e05d..8730951 100644 --- a/mos6502/instructions/illegal/_jam/__init__.py +++ b/mos6502/instructions/illegal/_jam/__init__.py @@ -16,7 +16,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -45,21 +44,33 @@ def add_jam_to_instruction_set_enum(instruction_set_class) -> None: """Add JAM instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + jam_opcodes = [ (JAM_IMPLIED_0x02, "JAM_IMPLIED_0x02"), (JAM_IMPLIED_0x12, "JAM_IMPLIED_0x12"), diff --git a/mos6502/instructions/illegal/_jam/_jam_6502.py b/mos6502/instructions/illegal/_jam/_jam_6502.py index f9ea2f9..0e906b8 100644 --- a/mos6502/instructions/illegal/_jam/_jam_6502.py +++ b/mos6502/instructions/illegal/_jam/_jam_6502.py @@ -16,9 +16,8 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING from mos6502 import errors @@ -26,7 +25,7 @@ from mos6502.core import MOS6502CPU -def _jam_common(cpu: MOS6502CPU, opcode: int) -> None: +def _jam_common(cpu: "MOS6502CPU", opcode: int) -> None: """Common JAM implementation for all NMOS variants. Sets the halted flag and raises CPUHaltError. @@ -47,64 +46,64 @@ def _jam_common(cpu: MOS6502CPU, opcode: int) -> None: cpu.log.warning(f"JAM instruction ${opcode:02X} executed at ${jam_address:04X} - CPU halted") # Raise the halt exception - raise errors.CPUHaltError(opcode=opcode, address=jam_address) + raise errors.CPUHaltError(opcode, jam_address) -def jam_implied_0x02(cpu: MOS6502CPU) -> None: +def jam_implied_0x02(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x02""" _jam_common(cpu, 0x02) -def jam_implied_0x12(cpu: MOS6502CPU) -> None: +def jam_implied_0x12(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x12""" _jam_common(cpu, 0x12) -def jam_implied_0x22(cpu: MOS6502CPU) -> None: +def jam_implied_0x22(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x22""" _jam_common(cpu, 0x22) -def jam_implied_0x32(cpu: MOS6502CPU) -> None: +def jam_implied_0x32(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x32""" _jam_common(cpu, 0x32) -def jam_implied_0x42(cpu: MOS6502CPU) -> None: +def jam_implied_0x42(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x42""" _jam_common(cpu, 0x42) -def jam_implied_0x52(cpu: MOS6502CPU) -> None: +def jam_implied_0x52(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x52""" _jam_common(cpu, 0x52) -def jam_implied_0x62(cpu: MOS6502CPU) -> None: +def jam_implied_0x62(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x62""" _jam_common(cpu, 0x62) -def jam_implied_0x72(cpu: MOS6502CPU) -> None: +def jam_implied_0x72(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x72""" _jam_common(cpu, 0x72) -def jam_implied_0x92(cpu: MOS6502CPU) -> None: +def jam_implied_0x92(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0x92""" _jam_common(cpu, 0x92) -def jam_implied_0xb2(cpu: MOS6502CPU) -> None: +def jam_implied_0xb2(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0xB2""" _jam_common(cpu, 0xB2) -def jam_implied_0xd2(cpu: MOS6502CPU) -> None: +def jam_implied_0xd2(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0xD2""" _jam_common(cpu, 0xD2) -def jam_implied_0xf2(cpu: MOS6502CPU) -> None: +def jam_implied_0xf2(cpu: "MOS6502CPU") -> None: """Execute JAM - Halt CPU. Opcode: 0xF2""" _jam_common(cpu, 0xF2) diff --git a/mos6502/instructions/illegal/_jam/_jam_65c02.py b/mos6502/instructions/illegal/_jam/_jam_65c02.py index 0e5fa3b..6070f6f 100644 --- a/mos6502/instructions/illegal/_jam/_jam_65c02.py +++ b/mos6502/instructions/illegal/_jam/_jam_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def _jam_nop(cpu: MOS6502CPU) -> None: +def _jam_nop(cpu: "MOS6502CPU") -> None: """Common JAM NOP implementation for 65C02. Does nothing - opcode was already fetched, PC already advanced. @@ -27,61 +26,61 @@ def _jam_nop(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def jam_implied_0x02(cpu: MOS6502CPU) -> None: +def jam_implied_0x02(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x02""" _jam_nop(cpu) -def jam_implied_0x12(cpu: MOS6502CPU) -> None: +def jam_implied_0x12(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x12""" _jam_nop(cpu) -def jam_implied_0x22(cpu: MOS6502CPU) -> None: +def jam_implied_0x22(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x22""" _jam_nop(cpu) -def jam_implied_0x32(cpu: MOS6502CPU) -> None: +def jam_implied_0x32(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x32""" _jam_nop(cpu) -def jam_implied_0x42(cpu: MOS6502CPU) -> None: +def jam_implied_0x42(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x42""" _jam_nop(cpu) -def jam_implied_0x52(cpu: MOS6502CPU) -> None: +def jam_implied_0x52(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x52""" _jam_nop(cpu) -def jam_implied_0x62(cpu: MOS6502CPU) -> None: +def jam_implied_0x62(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x62""" _jam_nop(cpu) -def jam_implied_0x72(cpu: MOS6502CPU) -> None: +def jam_implied_0x72(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x72""" _jam_nop(cpu) -def jam_implied_0x92(cpu: MOS6502CPU) -> None: +def jam_implied_0x92(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0x92""" _jam_nop(cpu) -def jam_implied_0xb2(cpu: MOS6502CPU) -> None: +def jam_implied_0xb2(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0xB2""" _jam_nop(cpu) -def jam_implied_0xd2(cpu: MOS6502CPU) -> None: +def jam_implied_0xd2(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0xD2""" _jam_nop(cpu) -def jam_implied_0xf2(cpu: MOS6502CPU) -> None: +def jam_implied_0xf2(cpu: "MOS6502CPU") -> None: """Execute JAM as NOP - 65C02 variant. Opcode: 0xF2""" _jam_nop(cpu) diff --git a/mos6502/instructions/illegal/_las/__init__.py b/mos6502/instructions/illegal/_las/__init__.py index 470e988..2324fbd 100644 --- a/mos6502/instructions/illegal/_las/__init__.py +++ b/mos6502/instructions/illegal/_las/__init__.py @@ -11,7 +11,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#LAS - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -26,21 +25,33 @@ def add_las_to_instruction_set_enum(instruction_set_class) -> None: """Add LAS instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(LAS_ABSOLUTE_Y_0xBB, "LAS_ABSOLUTE_Y_0xBB") instruction_set_class._value2member_map_[LAS_ABSOLUTE_Y_0xBB] = member setattr(instruction_set_class, "LAS_ABSOLUTE_Y_0xBB", LAS_ABSOLUTE_Y_0xBB) diff --git a/mos6502/instructions/illegal/_las/_las_6502.py b/mos6502/instructions/illegal/_las/_las_6502.py index 73411cf..37ee73f 100644 --- a/mos6502/instructions/illegal/_las/_las_6502.py +++ b/mos6502/instructions/illegal/_las/_las_6502.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def las_absolute_y_0xbb(cpu: MOS6502CPU) -> None: +def las_absolute_y_0xbb(cpu: "MOS6502CPU") -> None: """Execute LAS (Load A, X, and S) - Absolute,Y addressing mode. Opcode: 0xBB @@ -42,8 +41,8 @@ def las_absolute_y_0xbb(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) # Perform M & S result: int = value & int(cpu.S) diff --git a/mos6502/instructions/illegal/_las/_las_65c02.py b/mos6502/instructions/illegal/_las/_las_65c02.py index e1d5dbc..b1c3b91 100644 --- a/mos6502/instructions/illegal/_las/_las_65c02.py +++ b/mos6502/instructions/illegal/_las/_las_65c02.py @@ -10,15 +10,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def las_absolute_y_0xbb(cpu: MOS6502CPU) -> None: +def las_absolute_y_0xbb(cpu: "MOS6502CPU") -> None: """Execute LAS (Load A, X, and S) - Absolute,Y addressing mode. Opcode: 0xBB diff --git a/mos6502/instructions/illegal/_lax/__init__.py b/mos6502/instructions/illegal/_lax/__init__.py index 26411c2..6f24646 100644 --- a/mos6502/instructions/illegal/_lax/__init__.py +++ b/mos6502/instructions/illegal/_lax/__init__.py @@ -9,7 +9,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/Programming_with_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -109,21 +108,33 @@ def add_lax_to_instruction_set_enum(instruction_set_class) -> None: """Add LAX instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + # Add each LAX variant to the enum for opcode_name, opcode_value in [ ("LAX_ZEROPAGE_0xA7", LAX_ZEROPAGE_0xA7), diff --git a/mos6502/instructions/illegal/_lax/_lax_6502.py b/mos6502/instructions/illegal/_lax/_lax_6502.py index f9eec08..fe4fd66 100644 --- a/mos6502/instructions/illegal/_lax/_lax_6502.py +++ b/mos6502/instructions/illegal/_lax/_lax_6502.py @@ -12,15 +12,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING, Union if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def lax_zeropage_0xa7(cpu: MOS6502CPU) -> None: +def lax_zeropage_0xa7(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - Zero Page addressing mode. Opcode: 0xA7 @@ -40,8 +39,8 @@ def lax_zeropage_0xa7(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Fetch zero page address and read value - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) # Load into both A and X cpu.A = value @@ -55,7 +54,7 @@ def lax_zeropage_0xa7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lax_zeropage_y_0xb7(cpu: MOS6502CPU) -> None: +def lax_zeropage_y_0xb7(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - Zero Page,Y addressing mode. Opcode: 0xB7 @@ -75,8 +74,8 @@ def lax_zeropage_y_0xb7(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Fetch zero page,Y address and read value - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("Y") + value: int = cpu.read_byte(address) # Load into both A and X cpu.A = value @@ -90,7 +89,7 @@ def lax_zeropage_y_0xb7(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lax_indexed_indirect_x_0xa3(cpu: MOS6502CPU) -> None: +def lax_indexed_indirect_x_0xa3(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - (Indirect,X) addressing mode. Opcode: 0xA3 @@ -111,7 +110,7 @@ def lax_indexed_indirect_x_0xa3(cpu: MOS6502CPU) -> None: """ # Use existing helper for indexed indirect addressing address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Load into both A and X cpu.A = value @@ -125,7 +124,7 @@ def lax_indexed_indirect_x_0xa3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lax_indirect_indexed_y_0xb3(cpu: MOS6502CPU) -> None: +def lax_indirect_indexed_y_0xb3(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - (Indirect),Y addressing mode. Opcode: 0xB3 @@ -146,7 +145,7 @@ def lax_indirect_indexed_y_0xb3(cpu: MOS6502CPU) -> None: """ # Use existing helper for indirect indexed addressing address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Load into both A and X cpu.A = value @@ -160,7 +159,7 @@ def lax_indirect_indexed_y_0xb3(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lax_absolute_0xaf(cpu: MOS6502CPU) -> None: +def lax_absolute_0xaf(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - Absolute addressing mode. Opcode: 0xAF @@ -180,8 +179,8 @@ def lax_absolute_0xaf(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Fetch absolute address and read value - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) # Load into both A and X cpu.A = value @@ -195,7 +194,7 @@ def lax_absolute_0xaf(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lax_absolute_y_0xbf(cpu: MOS6502CPU) -> None: +def lax_absolute_y_0xbf(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - Absolute,Y addressing mode. Opcode: 0xBF @@ -215,8 +214,8 @@ def lax_absolute_y_0xbf(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Use existing helper for absolute Y addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) # Load into both A and X cpu.A = value @@ -230,7 +229,7 @@ def lax_absolute_y_0xbf(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lax_immediate_0xab(cpu: MOS6502CPU) -> None: +def lax_immediate_0xab(cpu: "MOS6502CPU") -> None: """Execute LAX (Load A and X) - Immediate addressing mode. Opcode: 0xAB @@ -242,7 +241,7 @@ def lax_immediate_0xab(cpu: MOS6502CPU) -> None: This opcode is extremely unstable and may produce different results on different chip revisions, temperatures, and manufacturing runs. - The actual operation is: (A | CONST) & immediate → A → X + The actual operation is: Union[(A, CONST]) & immediate → A → X where CONST is an undefined magic constant that varies by chip. VARIANT: 6502 - UNSTABLE - behavior varies diff --git a/mos6502/instructions/illegal/_lax/_lax_65c02.py b/mos6502/instructions/illegal/_lax/_lax_65c02.py index 164941d..1bec1e1 100644 --- a/mos6502/instructions/illegal/_lax/_lax_65c02.py +++ b/mos6502/instructions/illegal/_lax/_lax_65c02.py @@ -12,15 +12,14 @@ - https://wilsonminesco.com/NMOS-CMOSdif/ """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def lax_zeropage_0xa7(cpu: MOS6502CPU) -> None: +def lax_zeropage_0xa7(cpu: "MOS6502CPU") -> None: """Execute LAX - Zero Page (acts as NOP on 65C02). Opcode: 0xA7 @@ -47,7 +46,7 @@ def lax_zeropage_0xa7(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def lax_zeropage_y_0xb7(cpu: MOS6502CPU) -> None: +def lax_zeropage_y_0xb7(cpu: "MOS6502CPU") -> None: """Execute LAX - Zero Page,Y (acts as NOP on 65C02). Opcode: 0xB7 @@ -75,7 +74,7 @@ def lax_zeropage_y_0xb7(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def lax_indexed_indirect_x_0xa3(cpu: MOS6502CPU) -> None: +def lax_indexed_indirect_x_0xa3(cpu: "MOS6502CPU") -> None: """Execute LAX - (Indirect,X) (acts as NOP on 65C02). Opcode: 0xA3 @@ -102,7 +101,7 @@ def lax_indexed_indirect_x_0xa3(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def lax_indirect_indexed_y_0xb3(cpu: MOS6502CPU) -> None: +def lax_indirect_indexed_y_0xb3(cpu: "MOS6502CPU") -> None: """Execute LAX - (Indirect),Y (acts as NOP on 65C02). Opcode: 0xB3 @@ -129,7 +128,7 @@ def lax_indirect_indexed_y_0xb3(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def lax_absolute_0xaf(cpu: MOS6502CPU) -> None: +def lax_absolute_0xaf(cpu: "MOS6502CPU") -> None: """Execute LAX - Absolute (acts as NOP on 65C02). Opcode: 0xAF @@ -156,7 +155,7 @@ def lax_absolute_0xaf(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def lax_absolute_y_0xbf(cpu: MOS6502CPU) -> None: +def lax_absolute_y_0xbf(cpu: "MOS6502CPU") -> None: """Execute LAX - Absolute,Y (acts as NOP on 65C02). Opcode: 0xBF @@ -183,7 +182,7 @@ def lax_absolute_y_0xbf(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def lax_immediate_0xab(cpu: MOS6502CPU) -> None: +def lax_immediate_0xab(cpu: "MOS6502CPU") -> None: """Execute LAX - Immediate (acts as NOP on 65C02). Opcode: 0xAB diff --git a/mos6502/instructions/illegal/_nop_illegal/__init__.py b/mos6502/instructions/illegal/_nop_illegal/__init__.py index 5c42add..363abcf 100644 --- a/mos6502/instructions/illegal/_nop_illegal/__init__.py +++ b/mos6502/instructions/illegal/_nop_illegal/__init__.py @@ -11,7 +11,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -70,21 +69,33 @@ def add_nop_illegal_to_instruction_set_enum(instruction_set_class) -> None: """Add illegal NOP instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + # All illegal NOP opcodes all_nops = [ # 1-byte implied diff --git a/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_6502.py b/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_6502.py index cea9706..ee3e5b0 100644 --- a/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_6502.py +++ b/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_6502.py @@ -14,9 +14,8 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU @@ -26,7 +25,7 @@ # 1-byte NOPs (implied mode, 2 cycles) # ============================================================================= -def nop_implied_0x1a(cpu: MOS6502CPU) -> None: +def nop_implied_0x1a(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Implied addressing mode. Opcode: 0x1A @@ -35,10 +34,10 @@ def nop_implied_0x1a(cpu: MOS6502CPU) -> None: Flags: None affected """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) -def nop_implied_0x3a(cpu: MOS6502CPU) -> None: +def nop_implied_0x3a(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Implied addressing mode. Opcode: 0x3A @@ -47,10 +46,10 @@ def nop_implied_0x3a(cpu: MOS6502CPU) -> None: Flags: None affected """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) -def nop_implied_0x5a(cpu: MOS6502CPU) -> None: +def nop_implied_0x5a(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Implied addressing mode. Opcode: 0x5A @@ -59,10 +58,10 @@ def nop_implied_0x5a(cpu: MOS6502CPU) -> None: Flags: None affected """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) -def nop_implied_0x7a(cpu: MOS6502CPU) -> None: +def nop_implied_0x7a(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Implied addressing mode. Opcode: 0x7A @@ -71,10 +70,10 @@ def nop_implied_0x7a(cpu: MOS6502CPU) -> None: Flags: None affected """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) -def nop_implied_0xda(cpu: MOS6502CPU) -> None: +def nop_implied_0xda(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Implied addressing mode. Opcode: 0xDA @@ -83,10 +82,10 @@ def nop_implied_0xda(cpu: MOS6502CPU) -> None: Flags: None affected """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) -def nop_implied_0xfa(cpu: MOS6502CPU) -> None: +def nop_implied_0xfa(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Implied addressing mode. Opcode: 0xFA @@ -95,14 +94,14 @@ def nop_implied_0xfa(cpu: MOS6502CPU) -> None: Flags: None affected """ cpu.log.info("i") - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) # ============================================================================= # 2-byte NOPs (immediate mode, 2 cycles) # ============================================================================= -def nop_immediate_0x80(cpu: MOS6502CPU) -> None: +def nop_immediate_0x80(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Immediate addressing mode. Opcode: 0x80 @@ -114,7 +113,7 @@ def nop_immediate_0x80(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def nop_immediate_0x82(cpu: MOS6502CPU) -> None: +def nop_immediate_0x82(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Immediate addressing mode. Opcode: 0x82 @@ -126,7 +125,7 @@ def nop_immediate_0x82(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def nop_immediate_0x89(cpu: MOS6502CPU) -> None: +def nop_immediate_0x89(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Immediate addressing mode. Opcode: 0x89 @@ -138,7 +137,7 @@ def nop_immediate_0x89(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def nop_immediate_0xc2(cpu: MOS6502CPU) -> None: +def nop_immediate_0xc2(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Immediate addressing mode. Opcode: 0xC2 @@ -150,7 +149,7 @@ def nop_immediate_0xc2(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def nop_immediate_0xe2(cpu: MOS6502CPU) -> None: +def nop_immediate_0xe2(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Immediate addressing mode. Opcode: 0xE2 @@ -166,7 +165,7 @@ def nop_immediate_0xe2(cpu: MOS6502CPU) -> None: # 2-byte NOPs (zero page mode, 3 cycles) # ============================================================================= -def nop_zeropage_0x04(cpu: MOS6502CPU) -> None: +def nop_zeropage_0x04(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page addressing mode. Opcode: 0x04 @@ -174,12 +173,12 @@ def nop_zeropage_0x04(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_0x44(cpu: MOS6502CPU) -> None: +def nop_zeropage_0x44(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page addressing mode. Opcode: 0x44 @@ -187,12 +186,12 @@ def nop_zeropage_0x44(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_0x64(cpu: MOS6502CPU) -> None: +def nop_zeropage_0x64(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page addressing mode. Opcode: 0x64 @@ -200,7 +199,7 @@ def nop_zeropage_0x64(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) cpu.read_byte(address) # Dummy read cpu.log.info("i") @@ -209,7 +208,7 @@ def nop_zeropage_0x64(cpu: MOS6502CPU) -> None: # 2-byte NOPs (zero page,X mode, 4 cycles) # ============================================================================= -def nop_zeropage_x_0x14(cpu: MOS6502CPU) -> None: +def nop_zeropage_x_0x14(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page,X addressing mode. Opcode: 0x14 @@ -217,12 +216,12 @@ def nop_zeropage_x_0x14(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_x_0x34(cpu: MOS6502CPU) -> None: +def nop_zeropage_x_0x34(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page,X addressing mode. Opcode: 0x34 @@ -230,12 +229,12 @@ def nop_zeropage_x_0x34(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_x_0x54(cpu: MOS6502CPU) -> None: +def nop_zeropage_x_0x54(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page,X addressing mode. Opcode: 0x54 @@ -243,12 +242,12 @@ def nop_zeropage_x_0x54(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_x_0x74(cpu: MOS6502CPU) -> None: +def nop_zeropage_x_0x74(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page,X addressing mode. Opcode: 0x74 @@ -256,12 +255,12 @@ def nop_zeropage_x_0x74(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_x_0xd4(cpu: MOS6502CPU) -> None: +def nop_zeropage_x_0xd4(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page,X addressing mode. Opcode: 0xD4 @@ -269,12 +268,12 @@ def nop_zeropage_x_0xd4(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_zeropage_x_0xf4(cpu: MOS6502CPU) -> None: +def nop_zeropage_x_0xf4(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Zero Page,X addressing mode. Opcode: 0xF4 @@ -282,7 +281,7 @@ def nop_zeropage_x_0xf4(cpu: MOS6502CPU) -> None: Bytes: 2 Flags: None affected """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") @@ -291,7 +290,7 @@ def nop_zeropage_x_0xf4(cpu: MOS6502CPU) -> None: # 3-byte NOPs (absolute mode, 4 cycles) # ============================================================================= -def nop_absolute_0x0c(cpu: MOS6502CPU) -> None: +def nop_absolute_0x0c(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute addressing mode. Opcode: 0x0C @@ -299,7 +298,7 @@ def nop_absolute_0x0c(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) + address: int = cpu.fetch_absolute_mode_address(None) cpu.read_byte(address) # Dummy read cpu.log.info("i") @@ -309,7 +308,7 @@ def nop_absolute_0x0c(cpu: MOS6502CPU) -> None: # Extra cycle on page boundary crossing # ============================================================================= -def nop_absolute_x_0x1c(cpu: MOS6502CPU) -> None: +def nop_absolute_x_0x1c(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute,X addressing mode. Opcode: 0x1C @@ -317,12 +316,12 @@ def nop_absolute_x_0x1c(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_absolute_x_0x3c(cpu: MOS6502CPU) -> None: +def nop_absolute_x_0x3c(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute,X addressing mode. Opcode: 0x3C @@ -330,12 +329,12 @@ def nop_absolute_x_0x3c(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_absolute_x_0x5c(cpu: MOS6502CPU) -> None: +def nop_absolute_x_0x5c(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute,X addressing mode. Opcode: 0x5C @@ -343,12 +342,12 @@ def nop_absolute_x_0x5c(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_absolute_x_0x7c(cpu: MOS6502CPU) -> None: +def nop_absolute_x_0x7c(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute,X addressing mode. Opcode: 0x7C @@ -356,12 +355,12 @@ def nop_absolute_x_0x7c(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_absolute_x_0xdc(cpu: MOS6502CPU) -> None: +def nop_absolute_x_0xdc(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute,X addressing mode. Opcode: 0xDC @@ -369,12 +368,12 @@ def nop_absolute_x_0xdc(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") -def nop_absolute_x_0xfc(cpu: MOS6502CPU) -> None: +def nop_absolute_x_0xfc(cpu: "MOS6502CPU") -> None: """Execute illegal NOP - Absolute,X addressing mode. Opcode: 0xFC @@ -382,6 +381,6 @@ def nop_absolute_x_0xfc(cpu: MOS6502CPU) -> None: Bytes: 3 Flags: None affected """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") cpu.read_byte(address) # Dummy read cpu.log.info("i") diff --git a/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_65c02.py b/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_65c02.py index bd41830..50eb58b 100644 --- a/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_65c02.py +++ b/mos6502/instructions/illegal/_nop_illegal/_nop_illegal_65c02.py @@ -12,7 +12,6 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations # 65C02 illegal NOP behavior is identical to NMOS - just import all handlers from mos6502.instructions.illegal._nop_illegal._nop_illegal_6502 import ( diff --git a/mos6502/instructions/illegal/_rla/__init__.py b/mos6502/instructions/illegal/_rla/__init__.py index aa0fc87..69757ae 100644 --- a/mos6502/instructions/illegal/_rla/__init__.py +++ b/mos6502/instructions/illegal/_rla/__init__.py @@ -8,7 +8,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#RLA - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -29,21 +28,33 @@ def add_rla_to_instruction_set_enum(instruction_set_class) -> None: """Add RLA instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for opcode_name, opcode_value in [ ("RLA_ZEROPAGE_0x27", RLA_ZEROPAGE_0x27), ("RLA_ZEROPAGE_X_0x37", RLA_ZEROPAGE_X_0x37), diff --git a/mos6502/instructions/illegal/_rla/_rla_6502.py b/mos6502/instructions/illegal/_rla/_rla_6502.py index 9c38770..7bb91df 100644 --- a/mos6502/instructions/illegal/_rla/_rla_6502.py +++ b/mos6502/instructions/illegal/_rla/_rla_6502.py @@ -12,15 +12,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rla_zeropage_0x27(cpu: MOS6502CPU) -> None: +def rla_zeropage_0x27(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Zero Page addressing mode. Opcode: 0x27 @@ -41,8 +40,8 @@ def rla_zeropage_0x27(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Rotate left (bit 7 goes to carry, carry goes to bit 0) @@ -50,7 +49,7 @@ def rla_zeropage_0x27(cpu: MOS6502CPU) -> None: rotated: int = ((value << 1) | carry_in) & 0xFF # Write rotated value back to memory - cpu.write_byte(address=address, data=rotated) + cpu.write_byte(address, rotated) # AND with accumulator cpu.A = int(cpu.A) & rotated @@ -62,7 +61,7 @@ def rla_zeropage_0x27(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_zeropage_x_0x37(cpu: MOS6502CPU) -> None: +def rla_zeropage_x_0x37(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Zero Page,X addressing mode. Opcode: 0x37 @@ -83,14 +82,14 @@ def rla_zeropage_x_0x37(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x80) else 0 rotated: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address, data=rotated) + cpu.write_byte(address, rotated) cpu.A = int(cpu.A) & rotated @@ -100,7 +99,7 @@ def rla_zeropage_x_0x37(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_indexed_indirect_x_0x23(cpu: MOS6502CPU) -> None: +def rla_indexed_indirect_x_0x23(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - (Indirect,X) addressing mode. Opcode: 0x23 @@ -122,13 +121,13 @@ def rla_indexed_indirect_x_0x23(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x80) else 0 rotated: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) cpu.A = int(cpu.A) & rotated @@ -138,7 +137,7 @@ def rla_indexed_indirect_x_0x23(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_indirect_indexed_y_0x33(cpu: MOS6502CPU) -> None: +def rla_indirect_indexed_y_0x33(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - (Indirect),Y addressing mode. Opcode: 0x33 @@ -160,13 +159,13 @@ def rla_indirect_indexed_y_0x33(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x80) else 0 rotated: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) cpu.A = int(cpu.A) & rotated @@ -176,7 +175,7 @@ def rla_indirect_indexed_y_0x33(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_absolute_0x2f(cpu: MOS6502CPU) -> None: +def rla_absolute_0x2f(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Absolute addressing mode. Opcode: 0x2F @@ -197,14 +196,14 @@ def rla_absolute_0x2f(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x80) else 0 rotated: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) cpu.A = int(cpu.A) & rotated @@ -214,7 +213,7 @@ def rla_absolute_0x2f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_absolute_x_0x3f(cpu: MOS6502CPU) -> None: +def rla_absolute_x_0x3f(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Absolute,X addressing mode. Opcode: 0x3F @@ -235,12 +234,12 @@ def rla_absolute_x_0x3f(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -250,7 +249,7 @@ def rla_absolute_x_0x3f(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x80) else 0 rotated: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) cpu.A = int(cpu.A) & rotated @@ -260,7 +259,7 @@ def rla_absolute_x_0x3f(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def rla_absolute_y_0x3b(cpu: MOS6502CPU) -> None: +def rla_absolute_y_0x3b(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Absolute,Y addressing mode. Opcode: 0x3B @@ -281,12 +280,12 @@ def rla_absolute_y_0x3b(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Read-Modify-Write with Absolute,Y always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address & 0xFFFF) + value: int = cpu.read_byte(address & 0xFFFF) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -296,7 +295,7 @@ def rla_absolute_y_0x3b(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x80) else 0 rotated: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) cpu.A = int(cpu.A) & rotated diff --git a/mos6502/instructions/illegal/_rla/_rla_65c02.py b/mos6502/instructions/illegal/_rla/_rla_65c02.py index 536ef06..9cb1e8e 100644 --- a/mos6502/instructions/illegal/_rla/_rla_65c02.py +++ b/mos6502/instructions/illegal/_rla/_rla_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rla_zeropage_0x27(cpu: MOS6502CPU) -> None: +def rla_zeropage_0x27(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Zero Page addressing mode. Opcode: 0x27 @@ -41,7 +40,7 @@ def rla_zeropage_0x27(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_zeropage_x_0x37(cpu: MOS6502CPU) -> None: +def rla_zeropage_x_0x37(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Zero Page,X addressing mode. Opcode: 0x37 @@ -64,7 +63,7 @@ def rla_zeropage_x_0x37(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_indexed_indirect_x_0x23(cpu: MOS6502CPU) -> None: +def rla_indexed_indirect_x_0x23(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - (Indirect,X) addressing mode. Opcode: 0x23 @@ -89,7 +88,7 @@ def rla_indexed_indirect_x_0x23(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_indirect_indexed_y_0x33(cpu: MOS6502CPU) -> None: +def rla_indirect_indexed_y_0x33(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - (Indirect),Y addressing mode. Opcode: 0x33 @@ -114,7 +113,7 @@ def rla_indirect_indexed_y_0x33(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_absolute_0x2f(cpu: MOS6502CPU) -> None: +def rla_absolute_0x2f(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Absolute addressing mode. Opcode: 0x2F @@ -137,7 +136,7 @@ def rla_absolute_0x2f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_absolute_x_0x3f(cpu: MOS6502CPU) -> None: +def rla_absolute_x_0x3f(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Absolute,X addressing mode. Opcode: 0x3F @@ -161,7 +160,7 @@ def rla_absolute_x_0x3f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rla_absolute_y_0x3b(cpu: MOS6502CPU) -> None: +def rla_absolute_y_0x3b(cpu: "MOS6502CPU") -> None: """Execute RLA (Rotate Left and AND) - Absolute,Y addressing mode. Opcode: 0x3B diff --git a/mos6502/instructions/illegal/_rra/__init__.py b/mos6502/instructions/illegal/_rra/__init__.py index 5883d2c..8efe5f9 100644 --- a/mos6502/instructions/illegal/_rra/__init__.py +++ b/mos6502/instructions/illegal/_rra/__init__.py @@ -8,7 +8,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#RRA - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -29,21 +28,33 @@ def add_rra_to_instruction_set_enum(instruction_set_class) -> None: """Add RRA instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for opcode_name, opcode_value in [ ("RRA_ZEROPAGE_0x67", RRA_ZEROPAGE_0x67), ("RRA_ZEROPAGE_X_0x77", RRA_ZEROPAGE_X_0x77), diff --git a/mos6502/instructions/illegal/_rra/_rra_6502.py b/mos6502/instructions/illegal/_rra/_rra_6502.py index 7d56683..155924d 100644 --- a/mos6502/instructions/illegal/_rra/_rra_6502.py +++ b/mos6502/instructions/illegal/_rra/_rra_6502.py @@ -13,15 +13,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rra_zeropage_0x67(cpu: MOS6502CPU) -> None: +def rra_zeropage_0x67(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Zero Page addressing mode. Opcode: 0x67 @@ -42,8 +41,8 @@ def rra_zeropage_0x67(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Rotate right (bit 0 goes to carry, carry goes to bit 7) @@ -51,7 +50,7 @@ def rra_zeropage_0x67(cpu: MOS6502CPU) -> None: rotated: int = (value >> 1) | (carry_in << 7) # Write rotated value back to memory - cpu.write_byte(address=address, data=rotated) + cpu.write_byte(address, rotated) # Add with carry (using the NEW carry from rotation) result: int = cpu.A + rotated + cpu.flags[flags.C] @@ -72,7 +71,7 @@ def rra_zeropage_0x67(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_zeropage_x_0x77(cpu: MOS6502CPU) -> None: +def rra_zeropage_x_0x77(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Zero Page,X addressing mode. Opcode: 0x77 @@ -93,14 +92,14 @@ def rra_zeropage_x_0x77(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x01) else 0 rotated: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address, data=rotated) + cpu.write_byte(address, rotated) result: int = cpu.A + rotated + cpu.flags[flags.C] cpu.flags[flags.C] = 1 if result > 0xFF else 0 @@ -113,7 +112,7 @@ def rra_zeropage_x_0x77(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_indexed_indirect_x_0x63(cpu: MOS6502CPU) -> None: +def rra_indexed_indirect_x_0x63(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - (Indirect,X) addressing mode. Opcode: 0x63 @@ -135,13 +134,13 @@ def rra_indexed_indirect_x_0x63(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x01) else 0 rotated: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) result: int = cpu.A + rotated + cpu.flags[flags.C] cpu.flags[flags.C] = 1 if result > 0xFF else 0 @@ -154,7 +153,7 @@ def rra_indexed_indirect_x_0x63(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_indirect_indexed_y_0x73(cpu: MOS6502CPU) -> None: +def rra_indirect_indexed_y_0x73(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - (Indirect),Y addressing mode. Opcode: 0x73 @@ -176,13 +175,13 @@ def rra_indirect_indexed_y_0x73(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x01) else 0 rotated: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) result: int = cpu.A + rotated + cpu.flags[flags.C] cpu.flags[flags.C] = 1 if result > 0xFF else 0 @@ -195,7 +194,7 @@ def rra_indirect_indexed_y_0x73(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_absolute_0x6f(cpu: MOS6502CPU) -> None: +def rra_absolute_0x6f(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Absolute addressing mode. Opcode: 0x6F @@ -216,14 +215,14 @@ def rra_absolute_0x6f(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] cpu.flags[flags.C] = 1 if (value & 0x01) else 0 rotated: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) result: int = cpu.A + rotated + cpu.flags[flags.C] cpu.flags[flags.C] = 1 if result > 0xFF else 0 @@ -236,7 +235,7 @@ def rra_absolute_0x6f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_absolute_x_0x7f(cpu: MOS6502CPU) -> None: +def rra_absolute_x_0x7f(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Absolute,X addressing mode. Opcode: 0x7F @@ -257,12 +256,12 @@ def rra_absolute_x_0x7f(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -272,7 +271,7 @@ def rra_absolute_x_0x7f(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x01) else 0 rotated: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) result: int = cpu.A + rotated + cpu.flags[flags.C] cpu.flags[flags.C] = 1 if result > 0xFF else 0 @@ -285,7 +284,7 @@ def rra_absolute_x_0x7f(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def rra_absolute_y_0x7b(cpu: MOS6502CPU) -> None: +def rra_absolute_y_0x7b(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Absolute,Y addressing mode. Opcode: 0x7B @@ -306,12 +305,12 @@ def rra_absolute_y_0x7b(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Read-Modify-Write with Absolute,Y always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address & 0xFFFF) + value: int = cpu.read_byte(address & 0xFFFF) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -321,7 +320,7 @@ def rra_absolute_y_0x7b(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x01) else 0 rotated: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=rotated) + cpu.write_byte(address & 0xFFFF, rotated) result: int = cpu.A + rotated + cpu.flags[flags.C] cpu.flags[flags.C] = 1 if result > 0xFF else 0 diff --git a/mos6502/instructions/illegal/_rra/_rra_65c02.py b/mos6502/instructions/illegal/_rra/_rra_65c02.py index a267a69..5f06dfc 100644 --- a/mos6502/instructions/illegal/_rra/_rra_65c02.py +++ b/mos6502/instructions/illegal/_rra/_rra_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rra_zeropage_0x67(cpu: MOS6502CPU) -> None: +def rra_zeropage_0x67(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Zero Page addressing mode. Opcode: 0x67 @@ -41,7 +40,7 @@ def rra_zeropage_0x67(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_zeropage_x_0x77(cpu: MOS6502CPU) -> None: +def rra_zeropage_x_0x77(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Zero Page,X addressing mode. Opcode: 0x77 @@ -64,7 +63,7 @@ def rra_zeropage_x_0x77(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_indexed_indirect_x_0x63(cpu: MOS6502CPU) -> None: +def rra_indexed_indirect_x_0x63(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - (Indirect,X) addressing mode. Opcode: 0x63 @@ -89,7 +88,7 @@ def rra_indexed_indirect_x_0x63(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_indirect_indexed_y_0x73(cpu: MOS6502CPU) -> None: +def rra_indirect_indexed_y_0x73(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - (Indirect),Y addressing mode. Opcode: 0x73 @@ -114,7 +113,7 @@ def rra_indirect_indexed_y_0x73(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_absolute_0x6f(cpu: MOS6502CPU) -> None: +def rra_absolute_0x6f(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Absolute addressing mode. Opcode: 0x6F @@ -137,7 +136,7 @@ def rra_absolute_0x6f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_absolute_x_0x7f(cpu: MOS6502CPU) -> None: +def rra_absolute_x_0x7f(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Absolute,X addressing mode. Opcode: 0x7F @@ -161,7 +160,7 @@ def rra_absolute_x_0x7f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rra_absolute_y_0x7b(cpu: MOS6502CPU) -> None: +def rra_absolute_y_0x7b(cpu: "MOS6502CPU") -> None: """Execute RRA (Rotate Right and Add with Carry) - Absolute,Y addressing mode. Opcode: 0x7B diff --git a/mos6502/instructions/illegal/_sax/__init__.py b/mos6502/instructions/illegal/_sax/__init__.py index 9471514..d1ad095 100644 --- a/mos6502/instructions/illegal/_sax/__init__.py +++ b/mos6502/instructions/illegal/_sax/__init__.py @@ -9,7 +9,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/Programming_with_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -75,21 +74,33 @@ def add_sax_to_instruction_set_enum(instruction_set_class) -> None: """Add SAX instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + # Add each SAX variant to the enum for opcode_name, opcode_value in [ ("SAX_ZEROPAGE_0x87", SAX_ZEROPAGE_0x87), diff --git a/mos6502/instructions/illegal/_sax/_sax_6502.py b/mos6502/instructions/illegal/_sax/_sax_6502.py index 0c666a5..330b1b7 100644 --- a/mos6502/instructions/illegal/_sax/_sax_6502.py +++ b/mos6502/instructions/illegal/_sax/_sax_6502.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sax_zeropage_0x87(cpu: MOS6502CPU) -> None: +def sax_zeropage_0x87(cpu: "MOS6502CPU") -> None: """Execute SAX (Store A AND X) - Zero Page addressing mode. Opcode: 0x87 @@ -39,19 +38,19 @@ def sax_zeropage_0x87(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Fetch zero page address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) # Calculate A & X result: int = int(cpu.A) & int(cpu.X) # Store to memory - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Internal cycle cpu.log.info("i") -def sax_zeropage_y_0x97(cpu: MOS6502CPU) -> None: +def sax_zeropage_y_0x97(cpu: "MOS6502CPU") -> None: """Execute SAX (Store A AND X) - Zero Page,Y addressing mode. Opcode: 0x97 @@ -71,19 +70,19 @@ def sax_zeropage_y_0x97(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Fetch zero page,Y address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="Y") + address: int = cpu.fetch_zeropage_mode_address("Y") # Calculate A & X result: int = int(cpu.A) & int(cpu.X) # Store to memory - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Internal cycle cpu.log.info("i") -def sax_indexed_indirect_x_0x83(cpu: MOS6502CPU) -> None: +def sax_indexed_indirect_x_0x83(cpu: "MOS6502CPU") -> None: """Execute SAX (Store A AND X) - (Indirect,X) addressing mode. Opcode: 0x83 @@ -109,13 +108,13 @@ def sax_indexed_indirect_x_0x83(cpu: MOS6502CPU) -> None: result: int = int(cpu.A) & int(cpu.X) # Store to memory - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Internal cycle cpu.log.info("i") -def sax_absolute_0x8f(cpu: MOS6502CPU) -> None: +def sax_absolute_0x8f(cpu: "MOS6502CPU") -> None: """Execute SAX (Store A AND X) - Absolute addressing mode. Opcode: 0x8F @@ -135,13 +134,13 @@ def sax_absolute_0x8f(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ # Fetch absolute address - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) + address: int = cpu.fetch_absolute_mode_address(None) # Calculate A & X result: int = int(cpu.A) & int(cpu.X) # Store to memory - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Internal cycle cpu.log.info("i") diff --git a/mos6502/instructions/illegal/_sax/_sax_65c02.py b/mos6502/instructions/illegal/_sax/_sax_65c02.py index 322a192..e8b389c 100644 --- a/mos6502/instructions/illegal/_sax/_sax_65c02.py +++ b/mos6502/instructions/illegal/_sax/_sax_65c02.py @@ -12,15 +12,14 @@ - https://wilsonminesco.com/NMOS-CMOSdif/ """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sax_zeropage_0x87(cpu: MOS6502CPU) -> None: +def sax_zeropage_0x87(cpu: "MOS6502CPU") -> None: """Execute SAX - Zero Page (acts as NOP on 65C02). Opcode: 0x87 @@ -47,7 +46,7 @@ def sax_zeropage_0x87(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def sax_zeropage_y_0x97(cpu: MOS6502CPU) -> None: +def sax_zeropage_y_0x97(cpu: "MOS6502CPU") -> None: """Execute SAX - Zero Page,Y (acts as NOP on 65C02). Opcode: 0x97 @@ -75,7 +74,7 @@ def sax_zeropage_y_0x97(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def sax_indexed_indirect_x_0x83(cpu: MOS6502CPU) -> None: +def sax_indexed_indirect_x_0x83(cpu: "MOS6502CPU") -> None: """Execute SAX - (Indirect,X) (acts as NOP on 65C02). Opcode: 0x83 @@ -102,7 +101,7 @@ def sax_indexed_indirect_x_0x83(cpu: MOS6502CPU) -> None: # No registers, flags, or memory modified -def sax_absolute_0x8f(cpu: MOS6502CPU) -> None: +def sax_absolute_0x8f(cpu: "MOS6502CPU") -> None: """Execute SAX - Absolute (acts as NOP on 65C02). Opcode: 0x8F diff --git a/mos6502/instructions/illegal/_sbc_illegal/__init__.py b/mos6502/instructions/illegal/_sbc_illegal/__init__.py index a63964b..bd30ed8 100644 --- a/mos6502/instructions/illegal/_sbc_illegal/__init__.py +++ b/mos6502/instructions/illegal/_sbc_illegal/__init__.py @@ -12,7 +12,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -28,21 +27,33 @@ def add_sbc_illegal_to_instruction_set_enum(instruction_set_class) -> None: """Add illegal SBC instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(SBC_IMMEDIATE_0xEB, "SBC_IMMEDIATE_0xEB") instruction_set_class._value2member_map_[SBC_IMMEDIATE_0xEB] = member setattr(instruction_set_class, "SBC_IMMEDIATE_0xEB", SBC_IMMEDIATE_0xEB) diff --git a/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_6502.py b/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_6502.py index 92b7873..e57f243 100644 --- a/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_6502.py +++ b/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_6502.py @@ -13,15 +13,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sbc_immediate_0xeb(cpu: MOS6502CPU) -> None: +def sbc_immediate_0xeb(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Immediate addressing mode. Opcode: 0xEB (illegal duplicate of 0xE9) diff --git a/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_65c02.py b/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_65c02.py index aade222..8d0b8ab 100644 --- a/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_65c02.py +++ b/mos6502/instructions/illegal/_sbc_illegal/_sbc_illegal_65c02.py @@ -11,15 +11,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sbc_immediate_0xeb(cpu: MOS6502CPU) -> None: +def sbc_immediate_0xeb(cpu: "MOS6502CPU") -> None: """Execute SBC (Subtract with Carry) - Immediate addressing mode - 65C02 variant. Opcode: 0xEB (illegal duplicate of 0xE9) diff --git a/mos6502/instructions/illegal/_sbx/__init__.py b/mos6502/instructions/illegal/_sbx/__init__.py index 6bc4f33..cd16b6b 100644 --- a/mos6502/instructions/illegal/_sbx/__init__.py +++ b/mos6502/instructions/illegal/_sbx/__init__.py @@ -12,7 +12,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#SBX - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -27,21 +26,33 @@ def add_sbx_to_instruction_set_enum(instruction_set_class) -> None: """Add SBX instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(SBX_IMMEDIATE_0xCB, "SBX_IMMEDIATE_0xCB") instruction_set_class._value2member_map_[SBX_IMMEDIATE_0xCB] = member setattr(instruction_set_class, "SBX_IMMEDIATE_0xCB", SBX_IMMEDIATE_0xCB) diff --git a/mos6502/instructions/illegal/_sbx/_sbx_6502.py b/mos6502/instructions/illegal/_sbx/_sbx_6502.py index 2094064..62f4fb4 100644 --- a/mos6502/instructions/illegal/_sbx/_sbx_6502.py +++ b/mos6502/instructions/illegal/_sbx/_sbx_6502.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sbx_immediate_0xcb(cpu: MOS6502CPU) -> None: +def sbx_immediate_0xcb(cpu: "MOS6502CPU") -> None: """Execute SBX (Subtract from X) - Immediate addressing mode. Opcode: 0xCB diff --git a/mos6502/instructions/illegal/_sbx/_sbx_65c02.py b/mos6502/instructions/illegal/_sbx/_sbx_65c02.py index 341be59..0a74277 100644 --- a/mos6502/instructions/illegal/_sbx/_sbx_65c02.py +++ b/mos6502/instructions/illegal/_sbx/_sbx_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sbx_immediate_0xcb(cpu: MOS6502CPU) -> None: +def sbx_immediate_0xcb(cpu: "MOS6502CPU") -> None: """Execute SBX (Subtract from X) - Immediate addressing mode. Opcode: 0xCB diff --git a/mos6502/instructions/illegal/_sha/__init__.py b/mos6502/instructions/illegal/_sha/__init__.py index 0cccf1b..66c7a10 100644 --- a/mos6502/instructions/illegal/_sha/__init__.py +++ b/mos6502/instructions/illegal/_sha/__init__.py @@ -13,7 +13,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -35,21 +34,33 @@ def add_sha_to_instruction_set_enum(instruction_set_class) -> None: """Add SHA instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for opcode, name in [ (SHA_INDIRECT_INDEXED_Y_0x93, "SHA_INDIRECT_INDEXED_Y_0x93"), (SHA_ABSOLUTE_Y_0x9F, "SHA_ABSOLUTE_Y_0x9F"), diff --git a/mos6502/instructions/illegal/_sha/_sha_6502.py b/mos6502/instructions/illegal/_sha/_sha_6502.py index 1a85e3c..a67c472 100644 --- a/mos6502/instructions/illegal/_sha/_sha_6502.py +++ b/mos6502/instructions/illegal/_sha/_sha_6502.py @@ -18,15 +18,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sha_indirect_indexed_y_0x93(cpu: MOS6502CPU) -> None: +def sha_indirect_indexed_y_0x93(cpu: "MOS6502CPU") -> None: """Execute SHA (AHX) - Indirect Indexed Y addressing mode. Opcode: 0x93 @@ -65,12 +64,12 @@ def sha_indirect_indexed_y_0x93(cpu: MOS6502CPU) -> None: value: int = int(cpu.A) & int(cpu.X) & ((high_byte + 1) & 0xFF) # Store the value - cpu.write_byte(address=effective_address, data=value) + cpu.write_byte(effective_address, value) cpu.log.info("i") -def sha_absolute_y_0x9f(cpu: MOS6502CPU) -> None: +def sha_absolute_y_0x9f(cpu: "MOS6502CPU") -> None: """Execute SHA (AHX) - Absolute Y addressing mode. Opcode: 0x9F @@ -106,6 +105,6 @@ def sha_absolute_y_0x9f(cpu: MOS6502CPU) -> None: value: int = int(cpu.A) & int(cpu.X) & ((high_byte + 1) & 0xFF) # Store the value - cpu.write_byte(address=effective_address, data=value) + cpu.write_byte(effective_address, value) cpu.log.info("i") diff --git a/mos6502/instructions/illegal/_sha/_sha_65c02.py b/mos6502/instructions/illegal/_sha/_sha_65c02.py index 0ae0ca5..a1f016a 100644 --- a/mos6502/instructions/illegal/_sha/_sha_65c02.py +++ b/mos6502/instructions/illegal/_sha/_sha_65c02.py @@ -12,15 +12,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sha_indirect_indexed_y_0x93(cpu: MOS6502CPU) -> None: +def sha_indirect_indexed_y_0x93(cpu: "MOS6502CPU") -> None: """Execute SHA (AHX) - Indirect Indexed Y addressing mode - 65C02 variant. Opcode: 0x93 @@ -39,7 +38,7 @@ def sha_indirect_indexed_y_0x93(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sha_absolute_y_0x9f(cpu: MOS6502CPU) -> None: +def sha_absolute_y_0x9f(cpu: "MOS6502CPU") -> None: """Execute SHA (AHX) - Absolute Y addressing mode - 65C02 variant. Opcode: 0x9F diff --git a/mos6502/instructions/illegal/_shx/__init__.py b/mos6502/instructions/illegal/_shx/__init__.py index f0d1a6a..d9e5a68 100644 --- a/mos6502/instructions/illegal/_shx/__init__.py +++ b/mos6502/instructions/illegal/_shx/__init__.py @@ -10,7 +10,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -26,21 +25,33 @@ def add_shx_to_instruction_set_enum(instruction_set_class) -> None: """Add SHX instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(SHX_ABSOLUTE_Y_0x9E, "SHX_ABSOLUTE_Y_0x9E") instruction_set_class._value2member_map_[SHX_ABSOLUTE_Y_0x9E] = member setattr(instruction_set_class, "SHX_ABSOLUTE_Y_0x9E", SHX_ABSOLUTE_Y_0x9E) diff --git a/mos6502/instructions/illegal/_shx/_shx_6502.py b/mos6502/instructions/illegal/_shx/_shx_6502.py index 14da026..4514684 100644 --- a/mos6502/instructions/illegal/_shx/_shx_6502.py +++ b/mos6502/instructions/illegal/_shx/_shx_6502.py @@ -15,15 +15,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def shx_absolute_y_0x9e(cpu: MOS6502CPU) -> None: +def shx_absolute_y_0x9e(cpu: "MOS6502CPU") -> None: """Execute SHX (SXA) - Absolute Y addressing mode. Opcode: 0x9E @@ -59,6 +58,6 @@ def shx_absolute_y_0x9e(cpu: MOS6502CPU) -> None: value: int = int(cpu.X) & ((high_byte + 1) & 0xFF) # Store the value - cpu.write_byte(address=effective_address, data=value) + cpu.write_byte(effective_address, value) cpu.log.info("i") diff --git a/mos6502/instructions/illegal/_shx/_shx_65c02.py b/mos6502/instructions/illegal/_shx/_shx_65c02.py index 871783f..e91f921 100644 --- a/mos6502/instructions/illegal/_shx/_shx_65c02.py +++ b/mos6502/instructions/illegal/_shx/_shx_65c02.py @@ -9,15 +9,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def shx_absolute_y_0x9e(cpu: MOS6502CPU) -> None: +def shx_absolute_y_0x9e(cpu: "MOS6502CPU") -> None: """Execute SHX (SXA) - Absolute Y addressing mode - 65C02 variant. Opcode: 0x9E diff --git a/mos6502/instructions/illegal/_shy/__init__.py b/mos6502/instructions/illegal/_shy/__init__.py index 51c9035..cfb827b 100644 --- a/mos6502/instructions/illegal/_shy/__init__.py +++ b/mos6502/instructions/illegal/_shy/__init__.py @@ -10,7 +10,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -26,21 +25,33 @@ def add_shy_to_instruction_set_enum(instruction_set_class) -> None: """Add SHY instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(SHY_ABSOLUTE_X_0x9C, "SHY_ABSOLUTE_X_0x9C") instruction_set_class._value2member_map_[SHY_ABSOLUTE_X_0x9C] = member setattr(instruction_set_class, "SHY_ABSOLUTE_X_0x9C", SHY_ABSOLUTE_X_0x9C) diff --git a/mos6502/instructions/illegal/_shy/_shy_6502.py b/mos6502/instructions/illegal/_shy/_shy_6502.py index 5b4fa04..0c61a9d 100644 --- a/mos6502/instructions/illegal/_shy/_shy_6502.py +++ b/mos6502/instructions/illegal/_shy/_shy_6502.py @@ -15,15 +15,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def shy_absolute_x_0x9c(cpu: MOS6502CPU) -> None: +def shy_absolute_x_0x9c(cpu: "MOS6502CPU") -> None: """Execute SHY (SYA) - Absolute X addressing mode. Opcode: 0x9C @@ -59,6 +58,6 @@ def shy_absolute_x_0x9c(cpu: MOS6502CPU) -> None: value: int = int(cpu.Y) & ((high_byte + 1) & 0xFF) # Store the value - cpu.write_byte(address=effective_address, data=value) + cpu.write_byte(effective_address, value) cpu.log.info("i") diff --git a/mos6502/instructions/illegal/_shy/_shy_65c02.py b/mos6502/instructions/illegal/_shy/_shy_65c02.py index bfbddd0..d09abcf 100644 --- a/mos6502/instructions/illegal/_shy/_shy_65c02.py +++ b/mos6502/instructions/illegal/_shy/_shy_65c02.py @@ -9,15 +9,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def shy_absolute_x_0x9c(cpu: MOS6502CPU) -> None: +def shy_absolute_x_0x9c(cpu: "MOS6502CPU") -> None: """Execute SHY (SYA) - Absolute X addressing mode - 65C02 variant. Opcode: 0x9C diff --git a/mos6502/instructions/illegal/_slo/__init__.py b/mos6502/instructions/illegal/_slo/__init__.py index f7c99a2..2088fbf 100644 --- a/mos6502/instructions/illegal/_slo/__init__.py +++ b/mos6502/instructions/illegal/_slo/__init__.py @@ -11,7 +11,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/Programming_with_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -101,21 +100,33 @@ def add_slo_to_instruction_set_enum(instruction_set_class) -> None: """Add SLO instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + # Add each SLO variant to the enum for opcode_name, opcode_value in [ ("SLO_ZEROPAGE_0x07", SLO_ZEROPAGE_0x07), diff --git a/mos6502/instructions/illegal/_slo/_slo_6502.py b/mos6502/instructions/illegal/_slo/_slo_6502.py index 5047f82..901f7da 100644 --- a/mos6502/instructions/illegal/_slo/_slo_6502.py +++ b/mos6502/instructions/illegal/_slo/_slo_6502.py @@ -12,15 +12,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def slo_zeropage_0x07(cpu: MOS6502CPU) -> None: +def slo_zeropage_0x07(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Zero Page addressing mode. Opcode: 0x07 @@ -42,17 +41,17 @@ def slo_zeropage_0x07(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch zero page address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) + address: int = cpu.fetch_zeropage_mode_address(None) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Shift left (bit 7 goes to carry) cpu.flags[flags.C] = 1 if (value & 0x80) else 0 shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address, data=shifted) + cpu.write_byte(address, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted @@ -65,7 +64,7 @@ def slo_zeropage_0x07(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_zeropage_x_0x17(cpu: MOS6502CPU) -> None: +def slo_zeropage_x_0x17(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Zero Page,X addressing mode. Opcode: 0x17 @@ -87,17 +86,17 @@ def slo_zeropage_x_0x17(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch zero page,X address - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") + address: int = cpu.fetch_zeropage_mode_address("X") # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Shift left (bit 7 goes to carry) cpu.flags[flags.C] = 1 if (value & 0x80) else 0 shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address, data=shifted) + cpu.write_byte(address, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted @@ -110,7 +109,7 @@ def slo_zeropage_x_0x17(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_indexed_indirect_x_0x03(cpu: MOS6502CPU) -> None: +def slo_indexed_indirect_x_0x03(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - (Indirect,X) addressing mode. Opcode: 0x03 @@ -135,14 +134,14 @@ def slo_indexed_indirect_x_0x03(cpu: MOS6502CPU) -> None: address: int = cpu.fetch_indexed_indirect_mode_address() # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Shift left (bit 7 goes to carry) cpu.flags[flags.C] = 1 if (value & 0x80) else 0 shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted @@ -155,7 +154,7 @@ def slo_indexed_indirect_x_0x03(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_indirect_indexed_y_0x13(cpu: MOS6502CPU) -> None: +def slo_indirect_indexed_y_0x13(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - (Indirect),Y addressing mode. Opcode: 0x13 @@ -180,14 +179,14 @@ def slo_indirect_indexed_y_0x13(cpu: MOS6502CPU) -> None: address: int = cpu.fetch_indirect_indexed_mode_address() # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Shift left (bit 7 goes to carry) cpu.flags[flags.C] = 1 if (value & 0x80) else 0 shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted @@ -200,7 +199,7 @@ def slo_indirect_indexed_y_0x13(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_absolute_0x0f(cpu: MOS6502CPU) -> None: +def slo_absolute_0x0f(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Absolute addressing mode. Opcode: 0x0F @@ -222,17 +221,17 @@ def slo_absolute_0x0f(cpu: MOS6502CPU) -> None: from mos6502 import flags # Fetch absolute address - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) + address: int = cpu.fetch_absolute_mode_address(None) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Shift left (bit 7 goes to carry) cpu.flags[flags.C] = 1 if (value & 0x80) else 0 shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted @@ -245,7 +244,7 @@ def slo_absolute_0x0f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_absolute_x_0x1f(cpu: MOS6502CPU) -> None: +def slo_absolute_x_0x1f(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Absolute,X addressing mode. Opcode: 0x1F @@ -267,13 +266,13 @@ def slo_absolute_x_0x1f(cpu: MOS6502CPU) -> None: from mos6502 import flags # Use existing helper for absolute X addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) # Read value from memory - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -283,7 +282,7 @@ def slo_absolute_x_0x1f(cpu: MOS6502CPU) -> None: shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted @@ -295,7 +294,7 @@ def slo_absolute_x_0x1f(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def slo_absolute_y_0x1b(cpu: MOS6502CPU) -> None: +def slo_absolute_y_0x1b(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Absolute,Y addressing mode. Opcode: 0x1B @@ -317,13 +316,13 @@ def slo_absolute_y_0x1b(cpu: MOS6502CPU) -> None: from mos6502 import flags # Use existing helper for absolute Y addressing - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Read-Modify-Write with Absolute,Y always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) # Read value from memory - value: int = cpu.read_byte(address=address & 0xFFFF) + value: int = cpu.read_byte(address & 0xFFFF) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -333,7 +332,7 @@ def slo_absolute_y_0x1b(cpu: MOS6502CPU) -> None: shifted: int = (value << 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) # OR with accumulator cpu.A = int(cpu.A) | shifted diff --git a/mos6502/instructions/illegal/_slo/_slo_65c02.py b/mos6502/instructions/illegal/_slo/_slo_65c02.py index 79e8c3e..d525bf6 100644 --- a/mos6502/instructions/illegal/_slo/_slo_65c02.py +++ b/mos6502/instructions/illegal/_slo/_slo_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def slo_zeropage_0x07(cpu: MOS6502CPU) -> None: +def slo_zeropage_0x07(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Zero Page addressing mode. Opcode: 0x07 @@ -44,7 +43,7 @@ def slo_zeropage_0x07(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_zeropage_x_0x17(cpu: MOS6502CPU) -> None: +def slo_zeropage_x_0x17(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Zero Page,X addressing mode. Opcode: 0x17 @@ -70,7 +69,7 @@ def slo_zeropage_x_0x17(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_indexed_indirect_x_0x03(cpu: MOS6502CPU) -> None: +def slo_indexed_indirect_x_0x03(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - (Indirect,X) addressing mode. Opcode: 0x03 @@ -98,7 +97,7 @@ def slo_indexed_indirect_x_0x03(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_indirect_indexed_y_0x13(cpu: MOS6502CPU) -> None: +def slo_indirect_indexed_y_0x13(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - (Indirect),Y addressing mode. Opcode: 0x13 @@ -126,7 +125,7 @@ def slo_indirect_indexed_y_0x13(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_absolute_0x0f(cpu: MOS6502CPU) -> None: +def slo_absolute_0x0f(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Absolute addressing mode. Opcode: 0x0F @@ -152,7 +151,7 @@ def slo_absolute_0x0f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_absolute_x_0x1f(cpu: MOS6502CPU) -> None: +def slo_absolute_x_0x1f(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Absolute,X addressing mode. Opcode: 0x1F @@ -179,7 +178,7 @@ def slo_absolute_x_0x1f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def slo_absolute_y_0x1b(cpu: MOS6502CPU) -> None: +def slo_absolute_y_0x1b(cpu: "MOS6502CPU") -> None: """Execute SLO (Shift Left and OR) - Absolute,Y addressing mode. Opcode: 0x1B diff --git a/mos6502/instructions/illegal/_sre/__init__.py b/mos6502/instructions/illegal/_sre/__init__.py index 11b3b7c..236ce58 100644 --- a/mos6502/instructions/illegal/_sre/__init__.py +++ b/mos6502/instructions/illegal/_sre/__init__.py @@ -8,7 +8,6 @@ - https://masswerk.at/6502/6502_instruction_set.html#SRE - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -29,21 +28,33 @@ def add_sre_to_instruction_set_enum(instruction_set_class) -> None: """Add SRE instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for opcode_name, opcode_value in [ ("SRE_ZEROPAGE_0x47", SRE_ZEROPAGE_0x47), ("SRE_ZEROPAGE_X_0x57", SRE_ZEROPAGE_X_0x57), diff --git a/mos6502/instructions/illegal/_sre/_sre_6502.py b/mos6502/instructions/illegal/_sre/_sre_6502.py index c739b23..7b808cc 100644 --- a/mos6502/instructions/illegal/_sre/_sre_6502.py +++ b/mos6502/instructions/illegal/_sre/_sre_6502.py @@ -12,15 +12,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sre_zeropage_0x47(cpu: MOS6502CPU) -> None: +def sre_zeropage_0x47(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Zero Page addressing mode. Opcode: 0x47 @@ -41,15 +40,15 @@ def sre_zeropage_0x47(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) # Shift right (bit 0 goes to carry) cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF # Write shifted value back to memory - cpu.write_byte(address=address, data=shifted) + cpu.write_byte(address, shifted) # EOR with accumulator cpu.A = int(cpu.A) ^ shifted @@ -61,7 +60,7 @@ def sre_zeropage_0x47(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_zeropage_x_0x57(cpu: MOS6502CPU) -> None: +def sre_zeropage_x_0x57(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Zero Page,X addressing mode. Opcode: 0x57 @@ -82,13 +81,13 @@ def sre_zeropage_x_0x57(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF - cpu.write_byte(address=address, data=shifted) + cpu.write_byte(address, shifted) cpu.A = int(cpu.A) ^ shifted @@ -98,7 +97,7 @@ def sre_zeropage_x_0x57(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_indexed_indirect_x_0x43(cpu: MOS6502CPU) -> None: +def sre_indexed_indirect_x_0x43(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - (Indirect,X) addressing mode. Opcode: 0x43 @@ -120,12 +119,12 @@ def sre_indexed_indirect_x_0x43(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) cpu.A = int(cpu.A) ^ shifted @@ -135,7 +134,7 @@ def sre_indexed_indirect_x_0x43(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_indirect_indexed_y_0x53(cpu: MOS6502CPU) -> None: +def sre_indirect_indexed_y_0x53(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - (Indirect),Y addressing mode. Opcode: 0x53 @@ -157,12 +156,12 @@ def sre_indirect_indexed_y_0x53(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) cpu.A = int(cpu.A) ^ shifted @@ -172,7 +171,7 @@ def sre_indirect_indexed_y_0x53(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_absolute_0x4f(cpu: MOS6502CPU) -> None: +def sre_absolute_0x4f(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Absolute addressing mode. Opcode: 0x4F @@ -193,13 +192,13 @@ def sre_absolute_0x4f(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) cpu.A = int(cpu.A) ^ shifted @@ -209,7 +208,7 @@ def sre_absolute_0x4f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_absolute_x_0x5f(cpu: MOS6502CPU) -> None: +def sre_absolute_x_0x5f(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Absolute,X addressing mode. Opcode: 0x5F @@ -230,12 +229,12 @@ def sre_absolute_x_0x5f(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -243,7 +242,7 @@ def sre_absolute_x_0x5f(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) cpu.A = int(cpu.A) ^ shifted @@ -253,7 +252,7 @@ def sre_absolute_x_0x5f(cpu: MOS6502CPU) -> None: cpu.log.info("ax") -def sre_absolute_y_0x5b(cpu: MOS6502CPU) -> None: +def sre_absolute_y_0x5b(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Absolute,Y addressing mode. Opcode: 0x5B @@ -274,12 +273,12 @@ def sre_absolute_y_0x5b(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Read-Modify-Write with Absolute,Y always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address & 0xFFFF) + value: int = cpu.read_byte(address & 0xFFFF) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -287,7 +286,7 @@ def sre_absolute_y_0x5b(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x01) else 0 shifted: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=shifted) + cpu.write_byte(address & 0xFFFF, shifted) cpu.A = int(cpu.A) ^ shifted diff --git a/mos6502/instructions/illegal/_sre/_sre_65c02.py b/mos6502/instructions/illegal/_sre/_sre_65c02.py index f8eee71..306393b 100644 --- a/mos6502/instructions/illegal/_sre/_sre_65c02.py +++ b/mos6502/instructions/illegal/_sre/_sre_65c02.py @@ -11,15 +11,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sre_zeropage_0x47(cpu: MOS6502CPU) -> None: +def sre_zeropage_0x47(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Zero Page addressing mode. Opcode: 0x47 @@ -41,7 +40,7 @@ def sre_zeropage_0x47(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_zeropage_x_0x57(cpu: MOS6502CPU) -> None: +def sre_zeropage_x_0x57(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Zero Page,X addressing mode. Opcode: 0x57 @@ -64,7 +63,7 @@ def sre_zeropage_x_0x57(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_indexed_indirect_x_0x43(cpu: MOS6502CPU) -> None: +def sre_indexed_indirect_x_0x43(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - (Indirect,X) addressing mode. Opcode: 0x43 @@ -89,7 +88,7 @@ def sre_indexed_indirect_x_0x43(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_indirect_indexed_y_0x53(cpu: MOS6502CPU) -> None: +def sre_indirect_indexed_y_0x53(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - (Indirect),Y addressing mode. Opcode: 0x53 @@ -114,7 +113,7 @@ def sre_indirect_indexed_y_0x53(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_absolute_0x4f(cpu: MOS6502CPU) -> None: +def sre_absolute_0x4f(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Absolute addressing mode. Opcode: 0x4F @@ -137,7 +136,7 @@ def sre_absolute_0x4f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_absolute_x_0x5f(cpu: MOS6502CPU) -> None: +def sre_absolute_x_0x5f(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Absolute,X addressing mode. Opcode: 0x5F @@ -161,7 +160,7 @@ def sre_absolute_x_0x5f(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def sre_absolute_y_0x5b(cpu: MOS6502CPU) -> None: +def sre_absolute_y_0x5b(cpu: "MOS6502CPU") -> None: """Execute SRE (Shift Right and EOR) - Absolute,Y addressing mode. Opcode: 0x5B diff --git a/mos6502/instructions/illegal/_tas/__init__.py b/mos6502/instructions/illegal/_tas/__init__.py index d618404..54b1e24 100644 --- a/mos6502/instructions/illegal/_tas/__init__.py +++ b/mos6502/instructions/illegal/_tas/__init__.py @@ -12,7 +12,6 @@ - http://www.oxyron.de/html/opcodes02.html - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations from mos6502.instructions import InstructionOpcode from mos6502.memory import Byte @@ -28,21 +27,33 @@ def add_tas_to_instruction_set_enum(instruction_set_class) -> None: """Add TAS instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + member = PseudoEnumMember(TAS_ABSOLUTE_Y_0x9B, "TAS_ABSOLUTE_Y_0x9B") instruction_set_class._value2member_map_[TAS_ABSOLUTE_Y_0x9B] = member setattr(instruction_set_class, "TAS_ABSOLUTE_Y_0x9B", TAS_ABSOLUTE_Y_0x9B) diff --git a/mos6502/instructions/illegal/_tas/_tas_6502.py b/mos6502/instructions/illegal/_tas/_tas_6502.py index dd75777..54b000b 100644 --- a/mos6502/instructions/illegal/_tas/_tas_6502.py +++ b/mos6502/instructions/illegal/_tas/_tas_6502.py @@ -17,15 +17,14 @@ - https://www.nesdev.org/wiki/CPU_unofficial_opcodes """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def tas_absolute_y_0x9b(cpu: MOS6502CPU) -> None: +def tas_absolute_y_0x9b(cpu: "MOS6502CPU") -> None: """Execute TAS (XAS, SHS) - Absolute Y addressing mode. Opcode: 0x9B @@ -70,6 +69,6 @@ def tas_absolute_y_0x9b(cpu: MOS6502CPU) -> None: value: int = a_and_x & ((high_byte + 1) & 0xFF) # Store the value - cpu.write_byte(address=effective_address, data=value) + cpu.write_byte(effective_address, value) cpu.log.info("i") diff --git a/mos6502/instructions/illegal/_tas/_tas_65c02.py b/mos6502/instructions/illegal/_tas/_tas_65c02.py index 25fd08c..56e9652 100644 --- a/mos6502/instructions/illegal/_tas/_tas_65c02.py +++ b/mos6502/instructions/illegal/_tas/_tas_65c02.py @@ -9,15 +9,14 @@ - http://www.oxyron.de/html/opcodes02.html """ -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def tas_absolute_y_0x9b(cpu: MOS6502CPU) -> None: +def tas_absolute_y_0x9b(cpu: "MOS6502CPU") -> None: """Execute TAS (XAS, SHS) - Absolute Y addressing mode - 65C02 variant. Opcode: 0x9B diff --git a/mos6502/instructions/instruction.py b/mos6502/instructions/instruction.py index 45b0337..231be16 100644 --- a/mos6502/instructions/instruction.py +++ b/mos6502/instructions/instruction.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Pydantic model for 6502 instruction metadata.""" -from typing import Literal, Self +from mos6502.compat import Literal, List from pydantic import BaseModel, Field, computed_field, model_validator @@ -82,7 +82,7 @@ class Instruction(BaseModel): } @model_validator(mode="after") - def validate_assembler_format(self: Self) -> Self: + def validate_assembler_format(self) -> "Instruction": """Validate assembler format string matches addressing mode.""" # Implied and accumulator modes shouldn't have {oper} if self.addressing in ("implied", "accumulator"): @@ -98,7 +98,7 @@ def validate_assembler_format(self: Self) -> Self: @computed_field @property - def name(self: Self) -> str: + def name(self) -> str: """Generate canonical name like LDA_IMMEDIATE_0xA9.""" # Convert addressing mode to name format addr_map = { @@ -121,7 +121,7 @@ def name(self: Self) -> str: @computed_field @property - def cycles_display(self: Self) -> str: + def cycles_display(self) -> str: """Return cycles as display string (e.g., '4' or '4*' for page penalty).""" if self.page_boundary_penalty: return f"{self.base_cycles}*" @@ -129,41 +129,41 @@ def cycles_display(self: Self) -> str: @computed_field @property - def affects_n(self: Self) -> bool: + def affects_n(self) -> bool: """True if instruction affects Negative flag.""" return "N" in self.affected_flags @computed_field @property - def affects_v(self: Self) -> bool: + def affects_v(self) -> bool: """True if instruction affects Overflow flag.""" return "V" in self.affected_flags @computed_field @property - def affects_z(self: Self) -> bool: + def affects_z(self) -> bool: """True if instruction affects Zero flag.""" return "Z" in self.affected_flags @computed_field @property - def affects_c(self: Self) -> bool: + def affects_c(self) -> bool: """True if instruction affects Carry flag.""" return "C" in self.affected_flags @computed_field @property - def affects_i(self: Self) -> bool: + def affects_i(self) -> bool: """True if instruction affects Interrupt Disable flag.""" return "I" in self.affected_flags @computed_field @property - def affects_d(self: Self) -> bool: + def affects_d(self) -> bool: """True if instruction affects Decimal flag.""" return "D" in self.affected_flags - def to_legacy_dict(self: Self) -> dict: + def to_legacy_dict(self) -> dict: """Convert to legacy instruction_map dictionary format. This allows gradual migration by supporting the old dict-based API. @@ -195,39 +195,55 @@ def to_legacy_dict(self: Self) -> dict: "flags": flags_byte, } - def __hash__(self: Self) -> int: + def __hash__(self) -> int: """Hash by opcode for use in sets and as dict keys.""" return hash(self.opcode) - def __int__(self: Self) -> int: + def __int__(self) -> int: """Allow using Instruction where an int opcode is expected.""" return self.opcode -class PseudoEnumMember(int): +class PseudoEnumMember: """Allows dynamic addition of members to IntEnum classes. This class is used to add instruction opcodes to the InstructionSet enum at runtime, since Python's IntEnum doesn't support dynamic member addition. + + Note: Does not inherit from int for MicroPython compatibility. """ - def __new__(cls, value: int, name: str) -> "PseudoEnumMember": + __slots__ = ('_value_', '_name') + + def __init__(self, value: int, name: str) -> None: """Create a pseudo-enum member with the given value and name.""" - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + self._value_ = value + self._name = name @property - def name(self: Self) -> str: + def name(self) -> str: """Return the member name.""" return self._name @property - def value(self: Self) -> int: + def value(self) -> int: """Return the member value.""" return self._value_ + def __int__(self) -> int: + """Return the integer value.""" + return self._value_ + + def __eq__(self, other: object) -> bool: + """Compare equality with int or another PseudoEnumMember.""" + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self) -> int: + """Hash by value.""" + return hash(self._value_) + def register_instruction( instruction: Instruction, @@ -253,7 +269,7 @@ def register_instruction( def register_instructions( - instructions: list[Instruction], + instructions: List[Instruction], instruction_set_class: type, instruction_map: dict, ) -> None: diff --git a/mos6502/instructions/load/_lda/__init__.py b/mos6502/instructions/load/_lda/__init__.py index 7f95de6..febb722 100644 --- a/mos6502/instructions/load/_lda/__init__.py +++ b/mos6502/instructions/load/_lda/__init__.py @@ -26,10 +26,10 @@ CPUInstruction(0xA5, "LDA", "zeropage", "LDA {oper}", 2, 3, "NZ", _PKG, "lda_zeropage_0xa5"), CPUInstruction(0xB5, "LDA", "zeropage,X", "LDA {oper},X", 2, 4, "NZ", _PKG, "lda_zeropage_x_0xb5"), CPUInstruction(0xAD, "LDA", "absolute", "LDA {oper}", 3, 4, "NZ", _PKG, "lda_absolute_0xad"), - CPUInstruction(0xBD, "LDA", "absolute,X", "LDA {oper},X", 3, 4, "NZ", _PKG, "lda_absolute_x_0xbd", page_boundary_penalty=True), - CPUInstruction(0xB9, "LDA", "absolute,Y", "LDA {oper},Y", 3, 4, "NZ", _PKG, "lda_absolute_y_0xb9", page_boundary_penalty=True), + CPUInstruction(0xBD, "LDA", "absolute,X", "LDA {oper},X", 3, 4, "NZ", _PKG, "lda_absolute_x_0xbd", True), + CPUInstruction(0xB9, "LDA", "absolute,Y", "LDA {oper},Y", 3, 4, "NZ", _PKG, "lda_absolute_y_0xb9", True), CPUInstruction(0xA1, "LDA", "(indirect,X)", "LDA ({oper},X)", 2, 6, "NZ", _PKG, "lda_indexed_indirect_x_0xa1"), - CPUInstruction(0xB1, "LDA", "(indirect),Y", "LDA ({oper}),Y", 2, 5, "NZ", _PKG, "lda_indirect_indexed_y_0xb1", page_boundary_penalty=True), + CPUInstruction(0xB1, "LDA", "(indirect),Y", "LDA ({oper}),Y", 2, 5, "NZ", _PKG, "lda_indirect_indexed_y_0xb1", True), ] # InstructionOpcode instances for variant dispatch (backward compatibility) diff --git a/mos6502/instructions/load/_lda/_lda_6502.py b/mos6502/instructions/load/_lda/_lda_6502.py index d007351..9fe4338 100644 --- a/mos6502/instructions/load/_lda/_lda_6502.py +++ b/mos6502/instructions/load/_lda/_lda_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """LDA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def lda_immediate_0xa9(cpu: MOS6502CPU) -> None: +def lda_immediate_0xa9(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - Immediate addressing mode. Opcode: 0xA9 @@ -29,11 +28,11 @@ def lda_immediate_0xa9(cpu: MOS6502CPU) -> None: data: int = int(cpu.fetch_immediate_mode_address()) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_zeropage_0xa5(cpu: MOS6502CPU) -> None: +def lda_zeropage_0xa5(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - Zeropage addressing mode. Opcode: 0xA5 @@ -51,14 +50,14 @@ def lda_zeropage_0xa5(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_zeropage_x_0xb5(cpu: MOS6502CPU) -> None: +def lda_zeropage_x_0xb5(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - Zeropage,X addressing mode. Opcode: 0xB5 @@ -76,14 +75,14 @@ def lda_zeropage_x_0xb5(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_absolute_0xad(cpu: MOS6502CPU) -> None: +def lda_absolute_0xad(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - Absolute addressing mode. Opcode: 0xAD @@ -101,14 +100,14 @@ def lda_absolute_0xad(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_absolute_x_0xbd(cpu: MOS6502CPU) -> None: +def lda_absolute_x_0xbd(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - Absolute,X addressing mode. Opcode: 0xBD @@ -126,14 +125,14 @@ def lda_absolute_x_0xbd(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_absolute_y_0xb9(cpu: MOS6502CPU) -> None: +def lda_absolute_y_0xb9(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - Absolute,Y addressing mode. Opcode: 0xB9 @@ -151,14 +150,14 @@ def lda_absolute_y_0xb9(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_indexed_indirect_x_0xa1(cpu: MOS6502CPU) -> None: +def lda_indexed_indirect_x_0xa1(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - (Indirect,X) addressing mode. Opcode: 0xA1 @@ -177,13 +176,13 @@ def lda_indexed_indirect_x_0xa1(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indexed_indirect_mode_address() - data: int = cpu.read_byte(address=address) + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def lda_indirect_indexed_y_0xb1(cpu: MOS6502CPU) -> None: +def lda_indirect_indexed_y_0xb1(cpu: "MOS6502CPU") -> None: """Execute LDA (Load Accumulator with Memory) - (Indirect),Y addressing mode. Opcode: 0xB1 @@ -202,7 +201,7 @@ def lda_indirect_indexed_y_0xb1(cpu: MOS6502CPU) -> None: from mos6502 import flags address: int = cpu.fetch_indirect_indexed_mode_address() - data: int = cpu.read_byte(address=address) + data: int = cpu.read_byte(address) cpu.A = data - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") diff --git a/mos6502/instructions/load/_ldx/__init__.py b/mos6502/instructions/load/_ldx/__init__.py index 7d3cc8c..0431afd 100644 --- a/mos6502/instructions/load/_ldx/__init__.py +++ b/mos6502/instructions/load/_ldx/__init__.py @@ -50,21 +50,33 @@ def add_ldx_to_instruction_set_enum(instruction_set_class) -> None: """Add LDX instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (LDX_IMMEDIATE_0xA2, "LDX_IMMEDIATE_0xA2"), (LDX_ZEROPAGE_0xA6, "LDX_ZEROPAGE_0xA6"), diff --git a/mos6502/instructions/load/_ldx/_ldx_6502.py b/mos6502/instructions/load/_ldx/_ldx_6502.py index 3752d5c..d2b22e2 100644 --- a/mos6502/instructions/load/_ldx/_ldx_6502.py +++ b/mos6502/instructions/load/_ldx/_ldx_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """LDX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def ldx_immediate_0xa2(cpu: MOS6502CPU) -> None: +def ldx_immediate_0xa2(cpu: "MOS6502CPU") -> None: """Execute LDX (Load X Register with Memory) - Immediate addressing mode. Opcode: 0xA2 @@ -29,11 +28,11 @@ def ldx_immediate_0xa2(cpu: MOS6502CPU) -> None: data: int = int(cpu.fetch_immediate_mode_address()) cpu.X = data - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") -def ldx_zeropage_0xa6(cpu: MOS6502CPU) -> None: +def ldx_zeropage_0xa6(cpu: "MOS6502CPU") -> None: """Execute LDX (Load X Register with Memory) - Zeropage addressing mode. Opcode: 0xA6 @@ -51,14 +50,14 @@ def ldx_zeropage_0xa6(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + data: int = cpu.read_byte(address) cpu.X = data - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") -def ldx_zeropage_y_0xb6(cpu: MOS6502CPU) -> None: +def ldx_zeropage_y_0xb6(cpu: "MOS6502CPU") -> None: """Execute LDX (Load X Register with Memory) - Zeropage,Y addressing mode. Opcode: 0xB6 @@ -76,14 +75,14 @@ def ldx_zeropage_y_0xb6(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="Y") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("Y") + data: int = cpu.read_byte(address) cpu.X = data - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") -def ldx_absolute_0xae(cpu: MOS6502CPU) -> None: +def ldx_absolute_0xae(cpu: "MOS6502CPU") -> None: """Execute LDX (Load X Register with Memory) - Absolute addressing mode. Opcode: 0xAE @@ -101,14 +100,14 @@ def ldx_absolute_0xae(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + data: int = cpu.read_byte(address) cpu.X = data - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") -def ldx_absolute_y_0xbe(cpu: MOS6502CPU) -> None: +def ldx_absolute_y_0xbe(cpu: "MOS6502CPU") -> None: """Execute LDX (Load X Register with Memory) - Absolute,Y addressing mode. Opcode: 0xBE @@ -126,8 +125,8 @@ def ldx_absolute_y_0xbe(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + data: int = cpu.read_byte(address) cpu.X = data - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") diff --git a/mos6502/instructions/load/_ldy/__init__.py b/mos6502/instructions/load/_ldy/__init__.py index 2a95e21..637f3df 100644 --- a/mos6502/instructions/load/_ldy/__init__.py +++ b/mos6502/instructions/load/_ldy/__init__.py @@ -50,21 +50,33 @@ def add_ldy_to_instruction_set_enum(instruction_set_class) -> None: """Add LDY instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (LDY_IMMEDIATE_0xA0, "LDY_IMMEDIATE_0xA0"), (LDY_ZEROPAGE_0xA4, "LDY_ZEROPAGE_0xA4"), diff --git a/mos6502/instructions/load/_ldy/_ldy_6502.py b/mos6502/instructions/load/_ldy/_ldy_6502.py index 6534a35..0ed41e2 100644 --- a/mos6502/instructions/load/_ldy/_ldy_6502.py +++ b/mos6502/instructions/load/_ldy/_ldy_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """LDY instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def ldy_immediate_0xa0(cpu: MOS6502CPU) -> None: +def ldy_immediate_0xa0(cpu: "MOS6502CPU") -> None: """Execute LDY (Load Y Register with Memory) - Immediate addressing mode. Opcode: 0xA0 @@ -29,11 +28,11 @@ def ldy_immediate_0xa0(cpu: MOS6502CPU) -> None: data: int = int(cpu.fetch_immediate_mode_address()) cpu.Y = data - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") -def ldy_zeropage_0xa4(cpu: MOS6502CPU) -> None: +def ldy_zeropage_0xa4(cpu: "MOS6502CPU") -> None: """Execute LDY (Load Y Register with Memory) - Zeropage addressing mode. Opcode: 0xA4 @@ -51,14 +50,14 @@ def ldy_zeropage_0xa4(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + data: int = cpu.read_byte(address) cpu.Y = data - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") -def ldy_zeropage_x_0xb4(cpu: MOS6502CPU) -> None: +def ldy_zeropage_x_0xb4(cpu: "MOS6502CPU") -> None: """Execute LDY (Load Y Register with Memory) - Zeropage,X addressing mode. Opcode: 0xB4 @@ -76,14 +75,14 @@ def ldy_zeropage_x_0xb4(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + data: int = cpu.read_byte(address) cpu.Y = data - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") -def ldy_absolute_0xac(cpu: MOS6502CPU) -> None: +def ldy_absolute_0xac(cpu: "MOS6502CPU") -> None: """Execute LDY (Load Y Register with Memory) - Absolute addressing mode. Opcode: 0xAC @@ -101,14 +100,14 @@ def ldy_absolute_0xac(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + data: int = cpu.read_byte(address) cpu.Y = data - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") -def ldy_absolute_x_0xbc(cpu: MOS6502CPU) -> None: +def ldy_absolute_x_0xbc(cpu: "MOS6502CPU") -> None: """Execute LDY (Load Y Register with Memory) - Absolute,X addressing mode. Opcode: 0xBC @@ -126,8 +125,8 @@ def ldy_absolute_x_0xbc(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - data: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + data: int = cpu.read_byte(address) cpu.Y = data - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") diff --git a/mos6502/instructions/logic/_and/__init__.py b/mos6502/instructions/logic/_and/__init__.py index 39ae7fb..e590613 100644 --- a/mos6502/instructions/logic/_and/__init__.py +++ b/mos6502/instructions/logic/_and/__init__.py @@ -69,21 +69,33 @@ def add_and_to_instruction_set_enum(instruction_set_class) -> None: """Add AND instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (AND_IMMEDIATE_0x29, "AND_IMMEDIATE_0x29"), (AND_ZEROPAGE_0x25, "AND_ZEROPAGE_0x25"), diff --git a/mos6502/instructions/logic/_and/_and_6502.py b/mos6502/instructions/logic/_and/_and_6502.py index f69da2b..88b0ef7 100644 --- a/mos6502/instructions/logic/_and/_and_6502.py +++ b/mos6502/instructions/logic/_and/_and_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """AND instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def and_immediate_0x29(cpu: MOS6502CPU) -> None: +def and_immediate_0x29(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - Immediate addressing mode. Opcode: 0x29 @@ -27,11 +26,11 @@ def and_immediate_0x29(cpu: MOS6502CPU) -> None: """ value: int = cpu.fetch_byte() cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_zeropage_0x25(cpu: MOS6502CPU) -> None: +def and_zeropage_0x25(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - Zeropage addressing mode. Opcode: 0x25 @@ -47,14 +46,14 @@ def and_zeropage_0x25(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_zeropage_x_0x35(cpu: MOS6502CPU) -> None: +def and_zeropage_x_0x35(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - Zeropage,X addressing mode. Opcode: 0x35 @@ -70,14 +69,14 @@ def and_zeropage_x_0x35(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_absolute_0x2d(cpu: MOS6502CPU) -> None: +def and_absolute_0x2d(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - Absolute addressing mode. Opcode: 0x2D @@ -93,14 +92,14 @@ def and_absolute_0x2d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_absolute_x_0x3d(cpu: MOS6502CPU) -> None: +def and_absolute_x_0x3d(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - Absolute,X addressing mode. Opcode: 0x3D @@ -116,14 +115,14 @@ def and_absolute_x_0x3d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_absolute_y_0x39(cpu: MOS6502CPU) -> None: +def and_absolute_y_0x39(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - Absolute,Y addressing mode. Opcode: 0x39 @@ -139,14 +138,14 @@ def and_absolute_y_0x39(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_indexed_indirect_x_0x21(cpu: MOS6502CPU) -> None: +def and_indexed_indirect_x_0x21(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - (Indirect,X) addressing mode. Opcode: 0x21 @@ -163,13 +162,13 @@ def and_indexed_indirect_x_0x21(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def and_indirect_indexed_y_0x31(cpu: MOS6502CPU) -> None: +def and_indirect_indexed_y_0x31(cpu: "MOS6502CPU") -> None: """Execute AND (Logical AND with Accumulator) - (Indirect),Y addressing mode. Opcode: 0x31 @@ -186,7 +185,7 @@ def and_indirect_indexed_y_0x31(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.A = cpu.A & value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") diff --git a/mos6502/instructions/logic/_eor/__init__.py b/mos6502/instructions/logic/_eor/__init__.py index 978ad1c..378de1c 100644 --- a/mos6502/instructions/logic/_eor/__init__.py +++ b/mos6502/instructions/logic/_eor/__init__.py @@ -69,21 +69,33 @@ def add_eor_to_instruction_set_enum(instruction_set_class) -> None: """Add EOR instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (EOR_IMMEDIATE_0x49, "EOR_IMMEDIATE_0x49"), (EOR_ZEROPAGE_0x45, "EOR_ZEROPAGE_0x45"), diff --git a/mos6502/instructions/logic/_eor/_eor_6502.py b/mos6502/instructions/logic/_eor/_eor_6502.py index 577fb26..dc68410 100644 --- a/mos6502/instructions/logic/_eor/_eor_6502.py +++ b/mos6502/instructions/logic/_eor/_eor_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """EOR instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def eor_immediate_0x49(cpu: MOS6502CPU) -> None: +def eor_immediate_0x49(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - Immediate addressing mode. Opcode: 0x49 @@ -27,11 +26,11 @@ def eor_immediate_0x49(cpu: MOS6502CPU) -> None: """ value: int = cpu.fetch_byte() cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_zeropage_0x45(cpu: MOS6502CPU) -> None: +def eor_zeropage_0x45(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - Zeropage addressing mode. Opcode: 0x45 @@ -47,14 +46,14 @@ def eor_zeropage_0x45(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_zeropage_x_0x55(cpu: MOS6502CPU) -> None: +def eor_zeropage_x_0x55(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - Zeropage,X addressing mode. Opcode: 0x55 @@ -70,14 +69,14 @@ def eor_zeropage_x_0x55(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_absolute_0x4d(cpu: MOS6502CPU) -> None: +def eor_absolute_0x4d(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - Absolute addressing mode. Opcode: 0x4D @@ -93,14 +92,14 @@ def eor_absolute_0x4d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_absolute_x_0x5d(cpu: MOS6502CPU) -> None: +def eor_absolute_x_0x5d(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - Absolute,X addressing mode. Opcode: 0x5D @@ -116,14 +115,14 @@ def eor_absolute_x_0x5d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_absolute_y_0x59(cpu: MOS6502CPU) -> None: +def eor_absolute_y_0x59(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - Absolute,Y addressing mode. Opcode: 0x59 @@ -139,14 +138,14 @@ def eor_absolute_y_0x59(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_indexed_indirect_x_0x41(cpu: MOS6502CPU) -> None: +def eor_indexed_indirect_x_0x41(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - (Indirect,X) addressing mode. Opcode: 0x41 @@ -163,13 +162,13 @@ def eor_indexed_indirect_x_0x41(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def eor_indirect_indexed_y_0x51(cpu: MOS6502CPU) -> None: +def eor_indirect_indexed_y_0x51(cpu: "MOS6502CPU") -> None: """Execute EOR (Exclusive OR with Accumulator) - (Indirect),Y addressing mode. Opcode: 0x51 @@ -186,7 +185,7 @@ def eor_indirect_indexed_y_0x51(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.A = cpu.A ^ value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") diff --git a/mos6502/instructions/logic/_ora/__init__.py b/mos6502/instructions/logic/_ora/__init__.py index 21be49c..485e961 100644 --- a/mos6502/instructions/logic/_ora/__init__.py +++ b/mos6502/instructions/logic/_ora/__init__.py @@ -69,21 +69,33 @@ def add_ora_to_instruction_set_enum(instruction_set_class) -> None: """Add ORA instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (ORA_IMMEDIATE_0x09, "ORA_IMMEDIATE_0x09"), (ORA_ZEROPAGE_0x05, "ORA_ZEROPAGE_0x05"), diff --git a/mos6502/instructions/logic/_ora/_ora_6502.py b/mos6502/instructions/logic/_ora/_ora_6502.py index 890bd64..71a377b 100644 --- a/mos6502/instructions/logic/_ora/_ora_6502.py +++ b/mos6502/instructions/logic/_ora/_ora_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """ORA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def ora_immediate_0x09(cpu: MOS6502CPU) -> None: +def ora_immediate_0x09(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - Immediate addressing mode. Opcode: 0x09 @@ -27,11 +26,11 @@ def ora_immediate_0x09(cpu: MOS6502CPU) -> None: """ value: int = cpu.fetch_byte() cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_zeropage_0x05(cpu: MOS6502CPU) -> None: +def ora_zeropage_0x05(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - Zeropage addressing mode. Opcode: 0x05 @@ -47,14 +46,14 @@ def ora_zeropage_0x05(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_zeropage_x_0x15(cpu: MOS6502CPU) -> None: +def ora_zeropage_x_0x15(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - Zeropage,X addressing mode. Opcode: 0x15 @@ -70,14 +69,14 @@ def ora_zeropage_x_0x15(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_absolute_0x0d(cpu: MOS6502CPU) -> None: +def ora_absolute_0x0d(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - Absolute addressing mode. Opcode: 0x0D @@ -93,14 +92,14 @@ def ora_absolute_0x0d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_absolute_x_0x1d(cpu: MOS6502CPU) -> None: +def ora_absolute_x_0x1d(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - Absolute,X addressing mode. Opcode: 0x1D @@ -116,14 +115,14 @@ def ora_absolute_x_0x1d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("X") + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_absolute_y_0x19(cpu: MOS6502CPU) -> None: +def ora_absolute_y_0x19(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - Absolute,Y addressing mode. Opcode: 0x19 @@ -139,14 +138,14 @@ def ora_absolute_y_0x19(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address("Y") + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_indexed_indirect_x_0x01(cpu: MOS6502CPU) -> None: +def ora_indexed_indirect_x_0x01(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - (Indirect,X) addressing mode. Opcode: 0x01 @@ -163,13 +162,13 @@ def ora_indexed_indirect_x_0x01(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indexed_indirect_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") -def ora_indirect_indexed_y_0x11(cpu: MOS6502CPU) -> None: +def ora_indirect_indexed_y_0x11(cpu: "MOS6502CPU") -> None: """Execute ORA (Bitwise OR with Accumulator) - (Indirect),Y addressing mode. Opcode: 0x11 @@ -186,7 +185,7 @@ def ora_indirect_indexed_y_0x11(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indirect_indexed_mode_address() - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) cpu.A = cpu.A | value - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") diff --git a/mos6502/instructions/shift/_asl/__init__.py b/mos6502/instructions/shift/_asl/__init__.py index adb4d1e..4361405 100644 --- a/mos6502/instructions/shift/_asl/__init__.py +++ b/mos6502/instructions/shift/_asl/__init__.py @@ -50,21 +50,33 @@ def add_asl_to_instruction_set_enum(instruction_set_class) -> None: """Add ASL instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (ASL_ACCUMULATOR_0x0A, "ASL_ACCUMULATOR_0x0A"), (ASL_ZEROPAGE_0x06, "ASL_ZEROPAGE_0x06"), diff --git a/mos6502/instructions/shift/_asl/_asl_6502.py b/mos6502/instructions/shift/_asl/_asl_6502.py index 40f198a..6611e1d 100644 --- a/mos6502/instructions/shift/_asl/_asl_6502.py +++ b/mos6502/instructions/shift/_asl/_asl_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """ASL instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def asl_accumulator_0x0a(cpu: MOS6502CPU) -> None: +def asl_accumulator_0x0a(cpu: "MOS6502CPU") -> None: """Execute ASL (Arithmetic Shift Left) - Accumulator addressing mode. Opcode: 0x0A @@ -39,7 +38,7 @@ def asl_accumulator_0x0a(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def asl_zeropage_0x06(cpu: MOS6502CPU) -> None: +def asl_zeropage_0x06(cpu: "MOS6502CPU") -> None: """Execute ASL (Arithmetic Shift Left) - Zero Page addressing mode. Opcode: 0x06 @@ -48,14 +47,14 @@ def asl_zeropage_0x06(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) # Bit 7 goes to carry flag cpu.flags[flags.C] = 1 if (value & 0x80) else 0 result: int = (value << 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -64,7 +63,7 @@ def asl_zeropage_0x06(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def asl_zeropage_x_0x16(cpu: MOS6502CPU) -> None: +def asl_zeropage_x_0x16(cpu: "MOS6502CPU") -> None: """Execute ASL (Arithmetic Shift Left) - Zero Page,X addressing mode. Opcode: 0x16 @@ -73,14 +72,14 @@ def asl_zeropage_x_0x16(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) # Bit 7 goes to carry flag cpu.flags[flags.C] = 1 if (value & 0x80) else 0 result: int = (value << 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -89,7 +88,7 @@ def asl_zeropage_x_0x16(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def asl_absolute_0x0e(cpu: MOS6502CPU) -> None: +def asl_absolute_0x0e(cpu: "MOS6502CPU") -> None: """Execute ASL (Arithmetic Shift Left) - Absolute addressing mode. Opcode: 0x0E @@ -98,8 +97,8 @@ def asl_absolute_0x0e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) # Read-Modify-Write operations have an internal processing cycle cpu.spend_cpu_cycles(1) @@ -108,7 +107,7 @@ def asl_absolute_0x0e(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x80) else 0 result: int = (value << 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -117,7 +116,7 @@ def asl_absolute_0x0e(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def asl_absolute_x_0x1e(cpu: MOS6502CPU) -> None: +def asl_absolute_x_0x1e(cpu: "MOS6502CPU") -> None: """Execute ASL (Arithmetic Shift Left) - Absolute,X addressing mode. Opcode: 0x1E @@ -126,12 +125,12 @@ def asl_absolute_x_0x1e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -140,7 +139,7 @@ def asl_absolute_x_0x1e(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x80) else 0 result: int = (value << 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 diff --git a/mos6502/instructions/shift/_lsr/__init__.py b/mos6502/instructions/shift/_lsr/__init__.py index 6b7ba52..5ab11e2 100644 --- a/mos6502/instructions/shift/_lsr/__init__.py +++ b/mos6502/instructions/shift/_lsr/__init__.py @@ -50,21 +50,33 @@ def add_lsr_to_instruction_set_enum(instruction_set_class) -> None: """Add LSR instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (LSR_ACCUMULATOR_0x4A, "LSR_ACCUMULATOR_0x4A"), (LSR_ZEROPAGE_0x46, "LSR_ZEROPAGE_0x46"), diff --git a/mos6502/instructions/shift/_lsr/_lsr_6502.py b/mos6502/instructions/shift/_lsr/_lsr_6502.py index 9d45d33..a23875a 100644 --- a/mos6502/instructions/shift/_lsr/_lsr_6502.py +++ b/mos6502/instructions/shift/_lsr/_lsr_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """LSR instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def lsr_accumulator_0x4a(cpu: MOS6502CPU) -> None: +def lsr_accumulator_0x4a(cpu: "MOS6502CPU") -> None: """Execute LSR (Logical Shift Right) - Accumulator addressing mode. Opcode: 0x4A @@ -39,7 +38,7 @@ def lsr_accumulator_0x4a(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def lsr_zeropage_0x46(cpu: MOS6502CPU) -> None: +def lsr_zeropage_0x46(cpu: "MOS6502CPU") -> None: """Execute LSR (Logical Shift Right) - Zero Page addressing mode. Opcode: 0x46 @@ -48,14 +47,14 @@ def lsr_zeropage_0x46(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) # Bit 0 goes to carry flag cpu.flags[flags.C] = 1 if (value & 0x01) else 0 result: int = (value >> 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags (N is always 0 for LSR since bit 7 becomes 0) cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -64,7 +63,7 @@ def lsr_zeropage_0x46(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def lsr_zeropage_x_0x56(cpu: MOS6502CPU) -> None: +def lsr_zeropage_x_0x56(cpu: "MOS6502CPU") -> None: """Execute LSR (Logical Shift Right) - Zero Page,X addressing mode. Opcode: 0x56 @@ -73,14 +72,14 @@ def lsr_zeropage_x_0x56(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) # Bit 0 goes to carry flag cpu.flags[flags.C] = 1 if (value & 0x01) else 0 result: int = (value >> 1) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags (N is always 0 for LSR since bit 7 becomes 0) cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -89,7 +88,7 @@ def lsr_zeropage_x_0x56(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def lsr_absolute_0x4e(cpu: MOS6502CPU) -> None: +def lsr_absolute_0x4e(cpu: "MOS6502CPU") -> None: """Execute LSR (Logical Shift Right) - Absolute addressing mode. Opcode: 0x4E @@ -98,8 +97,8 @@ def lsr_absolute_0x4e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) # Read-Modify-Write operations have an internal processing cycle cpu.spend_cpu_cycles(1) @@ -108,7 +107,7 @@ def lsr_absolute_0x4e(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x01) else 0 result: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags (N is always 0 for LSR since bit 7 becomes 0) cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -117,7 +116,7 @@ def lsr_absolute_0x4e(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def lsr_absolute_x_0x5e(cpu: MOS6502CPU) -> None: +def lsr_absolute_x_0x5e(cpu: "MOS6502CPU") -> None: """Execute LSR (Logical Shift Right) - Absolute,X addressing mode. Opcode: 0x5E @@ -126,12 +125,12 @@ def lsr_absolute_x_0x5e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) # Internal processing cycle for RMW operation cpu.spend_cpu_cycles(1) @@ -140,7 +139,7 @@ def lsr_absolute_x_0x5e(cpu: MOS6502CPU) -> None: cpu.flags[flags.C] = 1 if (value & 0x01) else 0 result: int = (value >> 1) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags (N is always 0 for LSR since bit 7 becomes 0) cpu.flags[flags.Z] = 1 if result == 0 else 0 diff --git a/mos6502/instructions/shift/_rol/__init__.py b/mos6502/instructions/shift/_rol/__init__.py index d360105..f48b1f1 100644 --- a/mos6502/instructions/shift/_rol/__init__.py +++ b/mos6502/instructions/shift/_rol/__init__.py @@ -50,21 +50,33 @@ def add_rol_to_instruction_set_enum(instruction_set_class) -> None: """Add ROL instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (ROL_ACCUMULATOR_0x2A, "ROL_ACCUMULATOR_0x2A"), (ROL_ZEROPAGE_0x26, "ROL_ZEROPAGE_0x26"), diff --git a/mos6502/instructions/shift/_rol/_rol_6502.py b/mos6502/instructions/shift/_rol/_rol_6502.py index c9e1e2c..05d00de 100644 --- a/mos6502/instructions/shift/_rol/_rol_6502.py +++ b/mos6502/instructions/shift/_rol/_rol_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """ROL instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rol_accumulator_0x2a(cpu: MOS6502CPU) -> None: +def rol_accumulator_0x2a(cpu: "MOS6502CPU") -> None: """Execute ROL (Rotate Left) - Accumulator addressing mode. Opcode: 0x2A @@ -41,7 +40,7 @@ def rol_accumulator_0x2a(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def rol_zeropage_0x26(cpu: MOS6502CPU) -> None: +def rol_zeropage_0x26(cpu: "MOS6502CPU") -> None: """Execute ROL (Rotate Left) - Zero Page addressing mode. Opcode: 0x26 @@ -50,8 +49,8 @@ def rol_zeropage_0x26(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Bit 7 goes to carry flag @@ -59,7 +58,7 @@ def rol_zeropage_0x26(cpu: MOS6502CPU) -> None: # Rotate left: shift left and add carry to bit 0 result: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -68,7 +67,7 @@ def rol_zeropage_0x26(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def rol_zeropage_x_0x36(cpu: MOS6502CPU) -> None: +def rol_zeropage_x_0x36(cpu: "MOS6502CPU") -> None: """Execute ROL (Rotate Left) - Zero Page,X addressing mode. Opcode: 0x36 @@ -77,8 +76,8 @@ def rol_zeropage_x_0x36(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Bit 7 goes to carry flag @@ -86,7 +85,7 @@ def rol_zeropage_x_0x36(cpu: MOS6502CPU) -> None: # Rotate left: shift left and add carry to bit 0 result: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -95,7 +94,7 @@ def rol_zeropage_x_0x36(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def rol_absolute_0x2e(cpu: MOS6502CPU) -> None: +def rol_absolute_0x2e(cpu: "MOS6502CPU") -> None: """Execute ROL (Rotate Left) - Absolute addressing mode. Opcode: 0x2E @@ -104,8 +103,8 @@ def rol_absolute_0x2e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Read-Modify-Write operations have an internal processing cycle @@ -116,7 +115,7 @@ def rol_absolute_0x2e(cpu: MOS6502CPU) -> None: # Rotate left: shift left and add carry to bit 0 result: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -125,7 +124,7 @@ def rol_absolute_0x2e(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def rol_absolute_x_0x3e(cpu: MOS6502CPU) -> None: +def rol_absolute_x_0x3e(cpu: "MOS6502CPU") -> None: """Execute ROL (Rotate Left) - Absolute,X addressing mode. Opcode: 0x3E @@ -134,12 +133,12 @@ def rol_absolute_x_0x3e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Internal processing cycle for RMW operation @@ -150,7 +149,7 @@ def rol_absolute_x_0x3e(cpu: MOS6502CPU) -> None: # Rotate left: shift left and add carry to bit 0 result: int = ((value << 1) | carry_in) & 0xFF - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 diff --git a/mos6502/instructions/shift/_ror/__init__.py b/mos6502/instructions/shift/_ror/__init__.py index d40f7ef..4344577 100644 --- a/mos6502/instructions/shift/_ror/__init__.py +++ b/mos6502/instructions/shift/_ror/__init__.py @@ -50,21 +50,33 @@ def add_ror_to_instruction_set_enum(instruction_set_class) -> None: """Add ROR instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (ROR_ACCUMULATOR_0x6A, "ROR_ACCUMULATOR_0x6A"), (ROR_ZEROPAGE_0x66, "ROR_ZEROPAGE_0x66"), diff --git a/mos6502/instructions/shift/_ror/_ror_6502.py b/mos6502/instructions/shift/_ror/_ror_6502.py index a91aeba..278a704 100644 --- a/mos6502/instructions/shift/_ror/_ror_6502.py +++ b/mos6502/instructions/shift/_ror/_ror_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """ROR instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def ror_accumulator_0x6a(cpu: MOS6502CPU) -> None: +def ror_accumulator_0x6a(cpu: "MOS6502CPU") -> None: """Execute ROR (Rotate Right) - Accumulator addressing mode. Opcode: 0x6A @@ -41,7 +40,7 @@ def ror_accumulator_0x6a(cpu: MOS6502CPU) -> None: cpu.log.info("i") -def ror_zeropage_0x66(cpu: MOS6502CPU) -> None: +def ror_zeropage_0x66(cpu: "MOS6502CPU") -> None: """Execute ROR (Rotate Right) - Zero Page addressing mode. Opcode: 0x66 @@ -50,8 +49,8 @@ def ror_zeropage_0x66(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Bit 0 goes to carry flag @@ -59,7 +58,7 @@ def ror_zeropage_0x66(cpu: MOS6502CPU) -> None: # Rotate right: shift right and add carry to bit 7 result: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -68,7 +67,7 @@ def ror_zeropage_0x66(cpu: MOS6502CPU) -> None: cpu.log.info("z") -def ror_zeropage_x_0x76(cpu: MOS6502CPU) -> None: +def ror_zeropage_x_0x76(cpu: "MOS6502CPU") -> None: """Execute ROR (Rotate Right) - Zero Page,X addressing mode. Opcode: 0x76 @@ -77,8 +76,8 @@ def ror_zeropage_x_0x76(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_zeropage_mode_address("X") + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Bit 0 goes to carry flag @@ -86,7 +85,7 @@ def ror_zeropage_x_0x76(cpu: MOS6502CPU) -> None: # Rotate right: shift right and add carry to bit 7 result: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address, data=result) + cpu.write_byte(address, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -95,7 +94,7 @@ def ror_zeropage_x_0x76(cpu: MOS6502CPU) -> None: cpu.log.info("zx") -def ror_absolute_0x6e(cpu: MOS6502CPU) -> None: +def ror_absolute_0x6e(cpu: "MOS6502CPU") -> None: """Execute ROR (Rotate Right) - Absolute addressing mode. Opcode: 0x6E @@ -104,8 +103,8 @@ def ror_absolute_0x6e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - value: int = cpu.read_byte(address=address) + address: int = cpu.fetch_absolute_mode_address(None) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Read-Modify-Write operations have an internal processing cycle @@ -116,7 +115,7 @@ def ror_absolute_0x6e(cpu: MOS6502CPU) -> None: # Rotate right: shift right and add carry to bit 7 result: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 @@ -125,7 +124,7 @@ def ror_absolute_0x6e(cpu: MOS6502CPU) -> None: cpu.log.info("a") -def ror_absolute_x_0x7e(cpu: MOS6502CPU) -> None: +def ror_absolute_x_0x7e(cpu: "MOS6502CPU") -> None: """Execute ROR (Rotate Right) - Absolute,X addressing mode. Opcode: 0x7E @@ -134,12 +133,12 @@ def ror_absolute_x_0x7e(cpu: MOS6502CPU) -> None: """ from mos6502 import flags - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Read-Modify-Write with Absolute,X always does a dummy read regardless of page crossing cpu.spend_cpu_cycles(1) - value: int = cpu.read_byte(address=address) + value: int = cpu.read_byte(address) carry_in: int = cpu.flags[flags.C] # Internal processing cycle for RMW operation @@ -150,7 +149,7 @@ def ror_absolute_x_0x7e(cpu: MOS6502CPU) -> None: # Rotate right: shift right and add carry to bit 7 result: int = (value >> 1) | (carry_in << 7) - cpu.write_byte(address=address & 0xFFFF, data=result) + cpu.write_byte(address & 0xFFFF, result) # Set N and Z flags cpu.flags[flags.Z] = 1 if result == 0 else 0 diff --git a/mos6502/instructions/stack/_pha/__init__.py b/mos6502/instructions/stack/_pha/__init__.py index fcc6eeb..30e60d4 100644 --- a/mos6502/instructions/stack/_pha/__init__.py +++ b/mos6502/instructions/stack/_pha/__init__.py @@ -20,21 +20,33 @@ def add_pha_to_instruction_set_enum(instruction_set_class) -> None: """Add PHA instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + pha_member = PseudoEnumMember(PHA_IMPLIED_0x48, "PHA_IMPLIED_0x48") instruction_set_class._value2member_map_[PHA_IMPLIED_0x48] = pha_member setattr(instruction_set_class, "PHA_IMPLIED_0x48", PHA_IMPLIED_0x48) diff --git a/mos6502/instructions/stack/_pha/_pha_6502.py b/mos6502/instructions/stack/_pha/_pha_6502.py index e4f5c66..9ec2885 100644 --- a/mos6502/instructions/stack/_pha/_pha_6502.py +++ b/mos6502/instructions/stack/_pha/_pha_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """PHA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def pha_implied_0x48(cpu: MOS6502CPU) -> None: +def pha_implied_0x48(cpu: "MOS6502CPU") -> None: """Execute PHA (Push Accumulator on Stack) - Implied addressing mode. Opcode: 0x48 @@ -25,7 +24,7 @@ def pha_implied_0x48(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - cpu.write_byte(address=cpu.S, data=cpu.A) + cpu.write_byte(cpu.S, cpu.A) cpu.S -= 1 cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/stack/_php/__init__.py b/mos6502/instructions/stack/_php/__init__.py index 302e0f2..db8dfb5 100644 --- a/mos6502/instructions/stack/_php/__init__.py +++ b/mos6502/instructions/stack/_php/__init__.py @@ -20,21 +20,33 @@ def add_php_to_instruction_set_enum(instruction_set_class) -> None: """Add PHP instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + php_member = PseudoEnumMember(PHP_IMPLIED_0x08, "PHP_IMPLIED_0x08") instruction_set_class._value2member_map_[PHP_IMPLIED_0x08] = php_member setattr(instruction_set_class, "PHP_IMPLIED_0x08", PHP_IMPLIED_0x08) diff --git a/mos6502/instructions/stack/_php/_php_6502.py b/mos6502/instructions/stack/_php/_php_6502.py index 867647d..5772f3e 100644 --- a/mos6502/instructions/stack/_php/_php_6502.py +++ b/mos6502/instructions/stack/_php/_php_6502.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 """PHP instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU from mos6502.memory import Byte -def php_implied_0x08(cpu: MOS6502CPU) -> None: +def php_implied_0x08(cpu: "MOS6502CPU") -> None: """Execute PHP (Push Processor Status on Stack) - Implied addressing mode. Opcode: 0x08 @@ -32,8 +31,8 @@ def php_implied_0x08(cpu: MOS6502CPU) -> None: from mos6502.memory import Byte # Push processor status with B flag set - status_with_b: Byte = Byte(cpu.flags.value | 0b00110000) - cpu.write_byte(address=cpu.S, data=status_with_b) + status_with_b: "Byte" = Byte(cpu.flags.value | 0b00110000) + cpu.write_byte(cpu.S, status_with_b) cpu.S -= 1 cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/stack/_pla/__init__.py b/mos6502/instructions/stack/_pla/__init__.py index 5f83562..6697a27 100644 --- a/mos6502/instructions/stack/_pla/__init__.py +++ b/mos6502/instructions/stack/_pla/__init__.py @@ -21,21 +21,33 @@ def add_pla_to_instruction_set_enum(instruction_set_class) -> None: """Add PLA instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + pla_member = PseudoEnumMember(PLA_IMPLIED_0x68, "PLA_IMPLIED_0x68") instruction_set_class._value2member_map_[PLA_IMPLIED_0x68] = pla_member setattr(instruction_set_class, "PLA_IMPLIED_0x68", PLA_IMPLIED_0x68) diff --git a/mos6502/instructions/stack/_pla/_pla_6502.py b/mos6502/instructions/stack/_pla/_pla_6502.py index d49165a..9e84124 100644 --- a/mos6502/instructions/stack/_pla/_pla_6502.py +++ b/mos6502/instructions/stack/_pla/_pla_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """PLA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def pla_implied_0x68(cpu: MOS6502CPU) -> None: +def pla_implied_0x68(cpu: "MOS6502CPU") -> None: """Execute PLA (Pull Accumulator from Stack) - Implied addressing mode. Opcode: 0x68 @@ -26,7 +25,7 @@ def pla_implied_0x68(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.S += 1 - cpu.A = cpu.read_byte(address=cpu.S) - cpu.set_load_status_flags(register_name="A") + cpu.A = cpu.read_byte(cpu.S) + cpu.set_load_status_flags("A") cpu.log.info("i") cpu.spend_cpu_cycles(2) diff --git a/mos6502/instructions/stack/_plp/__init__.py b/mos6502/instructions/stack/_plp/__init__.py index 9956b3a..35e282c 100644 --- a/mos6502/instructions/stack/_plp/__init__.py +++ b/mos6502/instructions/stack/_plp/__init__.py @@ -20,21 +20,33 @@ def add_plp_to_instruction_set_enum(instruction_set_class) -> None: """Add PLP instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + plp_member = PseudoEnumMember(PLP_IMPLIED_0x28, "PLP_IMPLIED_0x28") instruction_set_class._value2member_map_[PLP_IMPLIED_0x28] = plp_member setattr(instruction_set_class, "PLP_IMPLIED_0x28", PLP_IMPLIED_0x28) diff --git a/mos6502/instructions/stack/_plp/_plp_6502.py b/mos6502/instructions/stack/_plp/_plp_6502.py index c89e233..cd2f3ed 100644 --- a/mos6502/instructions/stack/_plp/_plp_6502.py +++ b/mos6502/instructions/stack/_plp/_plp_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """PLP instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def plp_implied_0x28(cpu: MOS6502CPU) -> None: +def plp_implied_0x28(cpu: "MOS6502CPU") -> None: """Execute PLP (Pull Processor Status from Stack) - Implied addressing mode. Opcode: 0x28 @@ -28,7 +27,7 @@ def plp_implied_0x28(cpu: MOS6502CPU) -> None: from mos6502.flags import FlagsRegister cpu.S += 1 - status_byte: int = cpu.read_byte(address=cpu.S) + status_byte: int = cpu.read_byte(cpu.S) # Restore all flags from stack - must use FlagsRegister to preserve logging cpu._flags = FlagsRegister(status_byte) cpu.log.info("i") diff --git a/mos6502/instructions/store/_sta/__init__.py b/mos6502/instructions/store/_sta/__init__.py index 871f8bf..6ccb571 100644 --- a/mos6502/instructions/store/_sta/__init__.py +++ b/mos6502/instructions/store/_sta/__init__.py @@ -62,21 +62,33 @@ def add_sta_to_instruction_set_enum(instruction_set_class) -> None: """Add STA instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (STA_ZEROPAGE_0x85, "STA_ZEROPAGE_0x85"), (STA_ZEROPAGE_X_0x95, "STA_ZEROPAGE_X_0x95"), diff --git a/mos6502/instructions/store/_sta/_sta_6502.py b/mos6502/instructions/store/_sta/_sta_6502.py index baf1b59..46eb879 100644 --- a/mos6502/instructions/store/_sta/_sta_6502.py +++ b/mos6502/instructions/store/_sta/_sta_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """STA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sta_zeropage_0x85(cpu: MOS6502CPU) -> None: +def sta_zeropage_0x85(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - Zeropage addressing mode. Opcode: 0x85 @@ -25,12 +24,12 @@ def sta_zeropage_0x85(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - cpu.write_byte(address=address, data=cpu.A) + address: int = cpu.fetch_zeropage_mode_address(None) + cpu.write_byte(address, cpu.A) cpu.log.info("i") -def sta_zeropage_x_0x95(cpu: MOS6502CPU) -> None: +def sta_zeropage_x_0x95(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - Zeropage,X addressing mode. Opcode: 0x95 @@ -46,12 +45,12 @@ def sta_zeropage_x_0x95(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - cpu.write_byte(address=address, data=cpu.A) + address: int = cpu.fetch_zeropage_mode_address("X") + cpu.write_byte(address, cpu.A) cpu.log.info("i") -def sta_absolute_0x8d(cpu: MOS6502CPU) -> None: +def sta_absolute_0x8d(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - Absolute addressing mode. Opcode: 0x8D @@ -67,12 +66,12 @@ def sta_absolute_0x8d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - cpu.write_byte(address=address & 0xFFFF, data=cpu.A) + address: int = cpu.fetch_absolute_mode_address(None) + cpu.write_byte(address & 0xFFFF, cpu.A) cpu.log.info("i") -def sta_absolute_x_0x9d(cpu: MOS6502CPU) -> None: +def sta_absolute_x_0x9d(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - Absolute,X addressing mode. Opcode: 0x9D @@ -88,17 +87,17 @@ def sta_absolute_x_0x9d(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="X") + address: int = cpu.fetch_absolute_mode_address("X") # Store operations always take 5 cycles due to a dummy read that occurs # before the write, regardless of page boundary crossing cpu.spend_cpu_cycles(1) - cpu.write_byte(address=address & 0xFFFF, data=cpu.A) + cpu.write_byte(address & 0xFFFF, cpu.A) cpu.log.info("i") -def sta_absolute_y_0x99(cpu: MOS6502CPU) -> None: +def sta_absolute_y_0x99(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - Absolute,Y addressing mode. Opcode: 0x99 @@ -114,17 +113,17 @@ def sta_absolute_y_0x99(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name="Y") + address: int = cpu.fetch_absolute_mode_address("Y") # Store operations always take 5 cycles due to a dummy read that occurs # before the write, regardless of page boundary crossing cpu.spend_cpu_cycles(1) - cpu.write_byte(address=address & 0xFFFF, data=cpu.A) + cpu.write_byte(address & 0xFFFF, cpu.A) cpu.log.info("i") -def sta_indexed_indirect_x_0x81(cpu: MOS6502CPU) -> None: +def sta_indexed_indirect_x_0x81(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - (Indirect,X) addressing mode. Opcode: 0x81 @@ -141,11 +140,11 @@ def sta_indexed_indirect_x_0x81(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indexed_indirect_mode_address() - cpu.write_byte(address=address & 0xFFFF, data=cpu.A) + cpu.write_byte(address & 0xFFFF, cpu.A) cpu.log.info("i") -def sta_indirect_indexed_y_0x91(cpu: MOS6502CPU) -> None: +def sta_indirect_indexed_y_0x91(cpu: "MOS6502CPU") -> None: """Execute STA (Store Accumulator in Memory) - (Indirect),Y addressing mode. Opcode: 0x91 @@ -162,5 +161,5 @@ def sta_indirect_indexed_y_0x91(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ address: int = cpu.fetch_indirect_indexed_mode_address() - cpu.write_byte(address=address & 0xFFFF, data=cpu.A) + cpu.write_byte(address & 0xFFFF, cpu.A) cpu.log.info("i") diff --git a/mos6502/instructions/store/_stx/__init__.py b/mos6502/instructions/store/_stx/__init__.py index 34a6ec4..66f400e 100644 --- a/mos6502/instructions/store/_stx/__init__.py +++ b/mos6502/instructions/store/_stx/__init__.py @@ -34,21 +34,33 @@ def add_stx_to_instruction_set_enum(instruction_set_class) -> None: """Add STX instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (STX_ZEROPAGE_0x86, "STX_ZEROPAGE_0x86"), (STX_ZEROPAGE_Y_0x96, "STX_ZEROPAGE_Y_0x96"), diff --git a/mos6502/instructions/store/_stx/_stx_6502.py b/mos6502/instructions/store/_stx/_stx_6502.py index 24b266a..f1c51ed 100644 --- a/mos6502/instructions/store/_stx/_stx_6502.py +++ b/mos6502/instructions/store/_stx/_stx_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """STX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def stx_zeropage_0x86(cpu: MOS6502CPU) -> None: +def stx_zeropage_0x86(cpu: "MOS6502CPU") -> None: """Execute STX (Store X Register in Memory) - Zeropage addressing mode. Opcode: 0x86 @@ -25,12 +24,12 @@ def stx_zeropage_0x86(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - cpu.write_byte(address=address, data=cpu.X) + address: int = cpu.fetch_zeropage_mode_address(None) + cpu.write_byte(address, cpu.X) cpu.log.info("i") -def stx_zeropage_y_0x96(cpu: MOS6502CPU) -> None: +def stx_zeropage_y_0x96(cpu: "MOS6502CPU") -> None: """Execute STX (Store X Register in Memory) - Zeropage,Y addressing mode. Opcode: 0x96 @@ -46,12 +45,12 @@ def stx_zeropage_y_0x96(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="Y") - cpu.write_byte(address=address, data=cpu.X) + address: int = cpu.fetch_zeropage_mode_address("Y") + cpu.write_byte(address, cpu.X) cpu.log.info("i") -def stx_absolute_0x8e(cpu: MOS6502CPU) -> None: +def stx_absolute_0x8e(cpu: "MOS6502CPU") -> None: """Execute STX (Store X Register in Memory) - Absolute addressing mode. Opcode: 0x8E @@ -67,6 +66,6 @@ def stx_absolute_0x8e(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - cpu.write_byte(address=address & 0xFFFF, data=cpu.X) + address: int = cpu.fetch_absolute_mode_address(None) + cpu.write_byte(address & 0xFFFF, cpu.X) cpu.log.info("i") diff --git a/mos6502/instructions/store/_sty/__init__.py b/mos6502/instructions/store/_sty/__init__.py index 199da75..cd7c94d 100644 --- a/mos6502/instructions/store/_sty/__init__.py +++ b/mos6502/instructions/store/_sty/__init__.py @@ -34,21 +34,33 @@ def add_sty_to_instruction_set_enum(instruction_set_class) -> None: """Add STY instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + for value, name in [ (STY_ZEROPAGE_0x84, "STY_ZEROPAGE_0x84"), (STY_ZEROPAGE_X_0x94, "STY_ZEROPAGE_X_0x94"), diff --git a/mos6502/instructions/store/_sty/_sty_6502.py b/mos6502/instructions/store/_sty/_sty_6502.py index d1bd0c3..b78570c 100644 --- a/mos6502/instructions/store/_sty/_sty_6502.py +++ b/mos6502/instructions/store/_sty/_sty_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """STY instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def sty_zeropage_0x84(cpu: MOS6502CPU) -> None: +def sty_zeropage_0x84(cpu: "MOS6502CPU") -> None: """Execute STY (Store Y Register in Memory) - Zeropage addressing mode. Opcode: 0x84 @@ -25,12 +24,12 @@ def sty_zeropage_0x84(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name=None) - cpu.write_byte(address=address, data=cpu.Y) + address: int = cpu.fetch_zeropage_mode_address(None) + cpu.write_byte(address, cpu.Y) cpu.log.info("i") -def sty_zeropage_x_0x94(cpu: MOS6502CPU) -> None: +def sty_zeropage_x_0x94(cpu: "MOS6502CPU") -> None: """Execute STY (Store Y Register in Memory) - Zeropage,X addressing mode. Opcode: 0x94 @@ -46,12 +45,12 @@ def sty_zeropage_x_0x94(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_zeropage_mode_address(offset_register_name="X") - cpu.write_byte(address=address, data=cpu.Y) + address: int = cpu.fetch_zeropage_mode_address("X") + cpu.write_byte(address, cpu.Y) cpu.log.info("i") -def sty_absolute_0x8c(cpu: MOS6502CPU) -> None: +def sty_absolute_0x8c(cpu: "MOS6502CPU") -> None: """Execute STY (Store Y Register in Memory) - Absolute addressing mode. Opcode: 0x8C @@ -67,6 +66,6 @@ def sty_absolute_0x8c(cpu: MOS6502CPU) -> None: --------- cpu: The CPU instance to operate on """ - address: int = cpu.fetch_absolute_mode_address(offset_register_name=None) - cpu.write_byte(address=address & 0xFFFF, data=cpu.Y) + address: int = cpu.fetch_absolute_mode_address(None) + cpu.write_byte(address & 0xFFFF, cpu.Y) cpu.log.info("i") diff --git a/mos6502/instructions/subroutines/_jmp/__init__.py b/mos6502/instructions/subroutines/_jmp/__init__.py index a9b7b5e..0cc8c56 100644 --- a/mos6502/instructions/subroutines/_jmp/__init__.py +++ b/mos6502/instructions/subroutines/_jmp/__init__.py @@ -28,21 +28,33 @@ def add_jmp_to_instruction_set_enum(instruction_set_class) -> None: """Add JMP instructions to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + jmp_absolute_member = PseudoEnumMember(JMP_ABSOLUTE_0x4C, "JMP_ABSOLUTE_0x4C") instruction_set_class._value2member_map_[JMP_ABSOLUTE_0x4C] = jmp_absolute_member setattr(instruction_set_class, "JMP_ABSOLUTE_0x4C", JMP_ABSOLUTE_0x4C) diff --git a/mos6502/instructions/subroutines/_jmp/_jmp_6502.py b/mos6502/instructions/subroutines/_jmp/_jmp_6502.py index bda90d3..231046d 100644 --- a/mos6502/instructions/subroutines/_jmp/_jmp_6502.py +++ b/mos6502/instructions/subroutines/_jmp/_jmp_6502.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 """JMP instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU from mos6502.memory import Word -def jmp_absolute_0x4c(cpu: MOS6502CPU) -> None: +def jmp_absolute_0x4c(cpu: "MOS6502CPU") -> None: """Execute JMP (Jump to New Location) - Absolute addressing mode. Opcode: 0x4C @@ -28,13 +27,13 @@ def jmp_absolute_0x4c(cpu: MOS6502CPU) -> None: """ from mos6502.memory import Word - jump_address: Word = cpu.fetch_word() + jump_address: "Word" = cpu.fetch_word() cpu.PC = jump_address cpu.log.info("i") # No additional cycles - fetch_word already spent 2 -def jmp_indirect_0x6c(cpu: MOS6502CPU) -> None: +def jmp_indirect_0x6c(cpu: "MOS6502CPU") -> None: """Execute JMP (Jump to New Location) - Indirect addressing mode. Opcode: 0x6C @@ -53,7 +52,7 @@ def jmp_indirect_0x6c(cpu: MOS6502CPU) -> None: """ from mos6502.memory import Word - indirect_address: Word = cpu.fetch_word() + indirect_address: "Word" = cpu.fetch_word() # VARIANT: 6502/6502A/6502C - Page boundary bug # If indirect_address is 0xXXFF, the 6502 wraps within the page @@ -64,13 +63,13 @@ def jmp_indirect_0x6c(cpu: MOS6502CPU) -> None: if (indirect_address & 0xFF) == 0xFF: # On page boundary - 6502 has the bug - low_byte = cpu.read_byte(address=indirect_address) + low_byte = cpu.read_byte(indirect_address) # Wrap to start of same page instead of next page - high_byte = cpu.read_byte(address=indirect_address & 0xFF00) + high_byte = cpu.read_byte(indirect_address & 0xFF00) jump_address: int = (high_byte << 8) | low_byte else: # Not on page boundary - all variants behave the same - jump_address: Word = cpu.read_word(address=indirect_address) + jump_address: "Word" = cpu.read_word(indirect_address) cpu.PC = jump_address cpu.log.info("i") diff --git a/mos6502/instructions/subroutines/_jmp/_jmp_65c02.py b/mos6502/instructions/subroutines/_jmp/_jmp_65c02.py index 84aa4c2..fffcbdf 100644 --- a/mos6502/instructions/subroutines/_jmp/_jmp_65c02.py +++ b/mos6502/instructions/subroutines/_jmp/_jmp_65c02.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 """JMP instruction implementation for 65C02 variant.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU from mos6502.memory import Word -def jmp_absolute_0x4c(cpu: MOS6502CPU) -> None: +def jmp_absolute_0x4c(cpu: "MOS6502CPU") -> None: """Execute JMP (Jump to New Location) - Absolute addressing mode. Opcode: 0x4C @@ -25,13 +24,13 @@ def jmp_absolute_0x4c(cpu: MOS6502CPU) -> None: """ from mos6502.memory import Word - jump_address: Word = cpu.fetch_word() + jump_address: "Word" = cpu.fetch_word() cpu.PC = jump_address cpu.log.info("i") # No additional cycles - fetch_word already spent 2 -def jmp_indirect_0x6c(cpu: MOS6502CPU) -> None: +def jmp_indirect_0x6c(cpu: "MOS6502CPU") -> None: """Execute JMP (Jump to New Location) - Indirect addressing mode - 65C02 variant. Opcode: 0x6C @@ -54,11 +53,11 @@ def jmp_indirect_0x6c(cpu: MOS6502CPU) -> None: """ from mos6502.memory import Word - indirect_address: Word = cpu.fetch_word() + indirect_address: "Word" = cpu.fetch_word() # VARIANT: 65C02 - Bug fixed, always correctly reads across page boundary # No special handling needed - just read the word normally - jump_address: Word = cpu.read_word(address=indirect_address) + jump_address: "Word" = cpu.read_word(indirect_address) cpu.PC = jump_address cpu.log.info("i") diff --git a/mos6502/instructions/subroutines/_jsr/__init__.py b/mos6502/instructions/subroutines/_jsr/__init__.py index 84cf60a..e15cb7e 100644 --- a/mos6502/instructions/subroutines/_jsr/__init__.py +++ b/mos6502/instructions/subroutines/_jsr/__init__.py @@ -22,21 +22,33 @@ def add_jsr_to_instruction_set_enum(instruction_set_class) -> None: """Add JSR instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + jsr_member = PseudoEnumMember(JSR_ABSOLUTE_0x20, "JSR_ABSOLUTE_0x20") instruction_set_class._value2member_map_[JSR_ABSOLUTE_0x20] = jsr_member setattr(instruction_set_class, "JSR_ABSOLUTE_0x20", JSR_ABSOLUTE_0x20) diff --git a/mos6502/instructions/subroutines/_jsr/_jsr_6502.py b/mos6502/instructions/subroutines/_jsr/_jsr_6502.py index 63d8ba6..794b9eb 100644 --- a/mos6502/instructions/subroutines/_jsr/_jsr_6502.py +++ b/mos6502/instructions/subroutines/_jsr/_jsr_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """JSR instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def jsr_absolute_0x20(cpu: MOS6502CPU) -> None: +def jsr_absolute_0x20(cpu: "MOS6502CPU") -> None: """Execute JSR (Jump to New Location Saving Return Address) - Absolute addressing mode. Opcode: 0x20 @@ -41,9 +40,9 @@ def jsr_absolute_0x20(cpu: MOS6502CPU) -> None: pc_low = return_addr & 0xFF # Push high byte first, then low byte - cpu.write_byte(address=cpu.S, data=pc_high) + cpu.write_byte(cpu.S, pc_high) cpu.S -= 1 - cpu.write_byte(address=cpu.S, data=pc_low) + cpu.write_byte(cpu.S, pc_low) cpu.S -= 1 cpu.PC = subroutine_address cpu.log.info("i") diff --git a/mos6502/instructions/subroutines/_rti/__init__.py b/mos6502/instructions/subroutines/_rti/__init__.py index 534c1bc..94fd79b 100644 --- a/mos6502/instructions/subroutines/_rti/__init__.py +++ b/mos6502/instructions/subroutines/_rti/__init__.py @@ -20,21 +20,33 @@ def add_rti_to_instruction_set_enum(instruction_set_class) -> None: """Add RTI instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + rti_member = PseudoEnumMember(RTI_IMPLIED_0x40, "RTI_IMPLIED_0x40") instruction_set_class._value2member_map_[RTI_IMPLIED_0x40] = rti_member setattr(instruction_set_class, "RTI_IMPLIED_0x40", RTI_IMPLIED_0x40) diff --git a/mos6502/instructions/subroutines/_rti/_rti_6502.py b/mos6502/instructions/subroutines/_rti/_rti_6502.py index cfe6314..da57018 100644 --- a/mos6502/instructions/subroutines/_rti/_rti_6502.py +++ b/mos6502/instructions/subroutines/_rti/_rti_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """RTI instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rti_implied_0x40(cpu: MOS6502CPU) -> None: +def rti_implied_0x40(cpu: "MOS6502CPU") -> None: """Execute RTI (Return from Interrupt) - Implied addressing mode. Opcode: 0x40 @@ -31,15 +30,15 @@ def rti_implied_0x40(cpu: MOS6502CPU) -> None: # S is incremented first, then we read from the new S location. # The S setter ensures S stays in page 1 ($0100-$01FF), so cpu.S is always valid. cpu.S += 1 - cpu._flags = FlagsRegister(cpu.read_byte(address=cpu.S)) + cpu._flags = FlagsRegister(cpu.read_byte(cpu.S)) # Pull PC from stack (2 bytes: low byte first, then high byte) # IMPORTANT: Stack always wraps within page 1 ($0100-$01FF) # We must read byte-by-byte to ensure proper wrapping at page boundary cpu.S += 1 - pc_low = cpu.read_byte(address=cpu.S) + pc_low = cpu.read_byte(cpu.S) cpu.S += 1 - pc_high = cpu.read_byte(address=cpu.S) + pc_high = cpu.read_byte(cpu.S) return_pc = (pc_high << 8) | pc_low cpu.PC = return_pc diff --git a/mos6502/instructions/subroutines/_rts/__init__.py b/mos6502/instructions/subroutines/_rts/__init__.py index 95c979e..ccdb2b9 100644 --- a/mos6502/instructions/subroutines/_rts/__init__.py +++ b/mos6502/instructions/subroutines/_rts/__init__.py @@ -20,21 +20,33 @@ def add_rts_to_instruction_set_enum(instruction_set_class) -> None: """Add RTS instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + rts_member = PseudoEnumMember(RTS_IMPLIED_0x60, "RTS_IMPLIED_0x60") instruction_set_class._value2member_map_[RTS_IMPLIED_0x60] = rts_member setattr(instruction_set_class, "RTS_IMPLIED_0x60", RTS_IMPLIED_0x60) diff --git a/mos6502/instructions/subroutines/_rts/_rts_6502.py b/mos6502/instructions/subroutines/_rts/_rts_6502.py index 06fed69..b6fabd2 100644 --- a/mos6502/instructions/subroutines/_rts/_rts_6502.py +++ b/mos6502/instructions/subroutines/_rts/_rts_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """RTS instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def rts_implied_0x60(cpu: MOS6502CPU) -> None: +def rts_implied_0x60(cpu: "MOS6502CPU") -> None: """Execute RTS (Return from Subroutine) - Implied addressing mode. Opcode: 0x60 @@ -29,7 +28,7 @@ def rts_implied_0x60(cpu: MOS6502CPU) -> None: # RTS takes 6 cycles total: # 1. Opcode fetch (already done by execute()) # 2. Read and increment stack pointer (1 cycle) - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) # 3-4. Read return address from stack (2 cycles) # RTS pops the return address minus 1 from the stack and adds 1 to get the actual return address # JSR pushes PC-1, so RTS must add 1 back @@ -37,12 +36,12 @@ def rts_implied_0x60(cpu: MOS6502CPU) -> None: # IMPORTANT: Stack always wraps within page 1 ($0100-$01FF) # We must read byte-by-byte to ensure proper wrapping at page boundary cpu.S += 1 - pc_low = cpu.read_byte(address=cpu.S) + pc_low = cpu.read_byte(cpu.S) cpu.S += 1 - pc_high = cpu.read_byte(address=cpu.S) + pc_high = cpu.read_byte(cpu.S) return_address = ((pc_high << 8) | pc_low) + 1 # 5. Increment PC (1 cycle) - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) # 6. Final PC increment (1 cycle) - cpu.spend_cpu_cycles(cost=1) + cpu.spend_cpu_cycles(1) cpu.PC = return_address diff --git a/mos6502/instructions/transfer/_tax/__init__.py b/mos6502/instructions/transfer/_tax/__init__.py index bd4bce1..b25de22 100644 --- a/mos6502/instructions/transfer/_tax/__init__.py +++ b/mos6502/instructions/transfer/_tax/__init__.py @@ -24,21 +24,33 @@ def add_tax_to_instruction_set_enum(instruction_set_class) -> None: # Create a pseudo-enum member that has a .name attribute # We can't create a true enum member after class definition, but we can # create an object that behaves like one for lookup purposes - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + tax_member = PseudoEnumMember(TAX_IMPLIED_0xAA, "TAX_IMPLIED_0xAA") instruction_set_class._value2member_map_[TAX_IMPLIED_0xAA] = tax_member setattr(instruction_set_class, "TAX_IMPLIED_0xAA", TAX_IMPLIED_0xAA) diff --git a/mos6502/instructions/transfer/_tax/_tax_6502.py b/mos6502/instructions/transfer/_tax/_tax_6502.py index 7eeaa60..60150a2 100644 --- a/mos6502/instructions/transfer/_tax/_tax_6502.py +++ b/mos6502/instructions/transfer/_tax/_tax_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """TAX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def tax_implied_0xaa(cpu: MOS6502CPU) -> None: +def tax_implied_0xaa(cpu: "MOS6502CPU") -> None: """Execute TAX (Transfer Accumulator to Index X) - Implied addressing mode. Opcode: 0xAA @@ -26,6 +25,6 @@ def tax_implied_0xaa(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.X = cpu.A - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/transfer/_tay/__init__.py b/mos6502/instructions/transfer/_tay/__init__.py index b34c6c0..008bce2 100644 --- a/mos6502/instructions/transfer/_tay/__init__.py +++ b/mos6502/instructions/transfer/_tay/__init__.py @@ -21,21 +21,33 @@ def add_tay_to_instruction_set_enum(instruction_set_class) -> None: """Add TAY instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + tay_member = PseudoEnumMember(TAY_IMPLIED_0xA8, "TAY_IMPLIED_0xA8") instruction_set_class._value2member_map_[TAY_IMPLIED_0xA8] = tay_member setattr(instruction_set_class, "TAY_IMPLIED_0xA8", TAY_IMPLIED_0xA8) diff --git a/mos6502/instructions/transfer/_tay/_tay_6502.py b/mos6502/instructions/transfer/_tay/_tay_6502.py index 2274ae9..3efc183 100644 --- a/mos6502/instructions/transfer/_tay/_tay_6502.py +++ b/mos6502/instructions/transfer/_tay/_tay_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """TAY instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def tay_implied_0xa8(cpu: MOS6502CPU) -> None: +def tay_implied_0xa8(cpu: "MOS6502CPU") -> None: """Execute TAY (Transfer Accumulator to Index Y) - Implied addressing mode. Opcode: 0xA8 @@ -26,6 +25,6 @@ def tay_implied_0xa8(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.Y = cpu.A - cpu.set_load_status_flags(register_name="Y") + cpu.set_load_status_flags("Y") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/transfer/_tsx/__init__.py b/mos6502/instructions/transfer/_tsx/__init__.py index c6a9028..5d7477b 100644 --- a/mos6502/instructions/transfer/_tsx/__init__.py +++ b/mos6502/instructions/transfer/_tsx/__init__.py @@ -21,21 +21,33 @@ def add_tsx_to_instruction_set_enum(instruction_set_class) -> None: """Add TSX instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + tsx_member = PseudoEnumMember(TSX_IMPLIED_0xBA, "TSX_IMPLIED_0xBA") instruction_set_class._value2member_map_[TSX_IMPLIED_0xBA] = tsx_member setattr(instruction_set_class, "TSX_IMPLIED_0xBA", TSX_IMPLIED_0xBA) diff --git a/mos6502/instructions/transfer/_tsx/_tsx_6502.py b/mos6502/instructions/transfer/_tsx/_tsx_6502.py index 9b59051..1d62390 100644 --- a/mos6502/instructions/transfer/_tsx/_tsx_6502.py +++ b/mos6502/instructions/transfer/_tsx/_tsx_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """TSX instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def tsx_implied_0xba(cpu: MOS6502CPU) -> None: +def tsx_implied_0xba(cpu: "MOS6502CPU") -> None: """Execute TSX (Transfer Stack Pointer to Index X) - Implied addressing mode. Opcode: 0xBA @@ -26,6 +25,6 @@ def tsx_implied_0xba(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.X = cpu.S & 0xFF - cpu.set_load_status_flags(register_name="X") + cpu.set_load_status_flags("X") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/transfer/_txa/__init__.py b/mos6502/instructions/transfer/_txa/__init__.py index 8637793..80a446a 100644 --- a/mos6502/instructions/transfer/_txa/__init__.py +++ b/mos6502/instructions/transfer/_txa/__init__.py @@ -21,21 +21,33 @@ def add_txa_to_instruction_set_enum(instruction_set_class) -> None: """Add TXA instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + txa_member = PseudoEnumMember(TXA_IMPLIED_0x8A, "TXA_IMPLIED_0x8A") instruction_set_class._value2member_map_[TXA_IMPLIED_0x8A] = txa_member setattr(instruction_set_class, "TXA_IMPLIED_0x8A", TXA_IMPLIED_0x8A) diff --git a/mos6502/instructions/transfer/_txa/_txa_6502.py b/mos6502/instructions/transfer/_txa/_txa_6502.py index d74cb4c..b63f67a 100644 --- a/mos6502/instructions/transfer/_txa/_txa_6502.py +++ b/mos6502/instructions/transfer/_txa/_txa_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """TXA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def txa_implied_0x8a(cpu: MOS6502CPU) -> None: +def txa_implied_0x8a(cpu: "MOS6502CPU") -> None: """Execute TXA (Transfer Index X to Accumulator) - Implied addressing mode. Opcode: 0x8A @@ -26,6 +25,6 @@ def txa_implied_0x8a(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.A = cpu.X - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/instructions/transfer/_txs/__init__.py b/mos6502/instructions/transfer/_txs/__init__.py index a17224b..51c9adc 100644 --- a/mos6502/instructions/transfer/_txs/__init__.py +++ b/mos6502/instructions/transfer/_txs/__init__.py @@ -20,21 +20,33 @@ def add_txs_to_instruction_set_enum(instruction_set_class) -> None: """Add TXS instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + txs_member = PseudoEnumMember(TXS_IMPLIED_0x9A, "TXS_IMPLIED_0x9A") instruction_set_class._value2member_map_[TXS_IMPLIED_0x9A] = txs_member setattr(instruction_set_class, "TXS_IMPLIED_0x9A", TXS_IMPLIED_0x9A) diff --git a/mos6502/instructions/transfer/_txs/_txs_6502.py b/mos6502/instructions/transfer/_txs/_txs_6502.py index acfde26..26159d3 100644 --- a/mos6502/instructions/transfer/_txs/_txs_6502.py +++ b/mos6502/instructions/transfer/_txs/_txs_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """TXS instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def txs_implied_0x9a(cpu: MOS6502CPU) -> None: +def txs_implied_0x9a(cpu: "MOS6502CPU") -> None: """Execute TXS (Transfer Index X to Stack Pointer) - Implied addressing mode. Opcode: 0x9A diff --git a/mos6502/instructions/transfer/_tya/__init__.py b/mos6502/instructions/transfer/_tya/__init__.py index 660910c..b939cd6 100644 --- a/mos6502/instructions/transfer/_tya/__init__.py +++ b/mos6502/instructions/transfer/_tya/__init__.py @@ -21,21 +21,33 @@ def add_tya_to_instruction_set_enum(instruction_set_class) -> None: """Add TYA instruction to the InstructionSet enum dynamically.""" - class PseudoEnumMember(int): - def __new__(cls, value, name) -> "InstructionSet": - obj = int.__new__(cls, value) - obj._name = name - obj._value_ = value - return obj + class PseudoEnumMember: + """MicroPython-compatible pseudo-enum member.""" + __slots__ = ('_value_', '_name') + + def __init__(self, value, name): + self._value_ = int(value) + self._name = name @property - def name(self) -> str: + def name(self): return self._name @property - def value(self) -> int: + def value(self): + return self._value_ + + def __int__(self): return self._value_ + def __eq__(self, other): + if isinstance(other, int): + return self._value_ == other + return NotImplemented + + def __hash__(self): + return hash(self._value_) + tya_member = PseudoEnumMember(TYA_IMPLIED_0x98, "TYA_IMPLIED_0x98") instruction_set_class._value2member_map_[TYA_IMPLIED_0x98] = tya_member setattr(instruction_set_class, "TYA_IMPLIED_0x98", TYA_IMPLIED_0x98) diff --git a/mos6502/instructions/transfer/_tya/_tya_6502.py b/mos6502/instructions/transfer/_tya/_tya_6502.py index 577a0be..c7374aa 100644 --- a/mos6502/instructions/transfer/_tya/_tya_6502.py +++ b/mos6502/instructions/transfer/_tya/_tya_6502.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """TYA instruction implementation for all 6502 variants.""" -from __future__ import annotations -from typing import TYPE_CHECKING +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU -def tya_implied_0x98(cpu: MOS6502CPU) -> None: +def tya_implied_0x98(cpu: "MOS6502CPU") -> None: """Execute TYA (Transfer Index Y to Accumulator) - Implied addressing mode. Opcode: 0x98 @@ -26,6 +25,6 @@ def tya_implied_0x98(cpu: MOS6502CPU) -> None: cpu: The CPU instance to operate on """ cpu.A = cpu.Y - cpu.set_load_status_flags(register_name="A") + cpu.set_load_status_flags("A") cpu.log.info("i") cpu.spend_cpu_cycles(1) diff --git a/mos6502/memory.py b/mos6502/memory.py index 23787a0..6a8ff2f 100755 --- a/mos6502/memory.py +++ b/mos6502/memory.py @@ -4,9 +4,9 @@ Performance optimization: MemoryUnit stores values as plain ints, not bitarrays. Bitarray conversion only happens when explicitly needed for bit operations. """ -import logging -from collections.abc import MutableSequence -from typing import Literal, Self +from mos6502.compat import logging +from mos6502.compat import MutableSequence +from mos6502.compat import Literal, Union, List from mos6502.bitarray_factory import ba2int, bitarray, int2ba, is_bitarray @@ -27,7 +27,7 @@ class MemoryUnit: # Use __slots__ for memory efficiency __slots__ = ('_value', '_endianness', '_overflow', '_underflow') - def __init__(self: Self, value: int | bitarray = 0, + def __init__(self, value: Union[int, bitarray]= 0, endianness: str = ENDIANNESS) -> None: """Initialize a MemoryUnit. @@ -51,94 +51,94 @@ def __init__(self: Self, value: int | bitarray = 0, self._value: int = int(value) & self._mask @property - def _mask(self: Self) -> int: + def _mask(self) -> int: """Return the bit mask for this MemoryUnit type.""" return (1 << self.size_bits) - 1 if self.size_bits > 0 else 0 @property - def size_bits(self: Self) -> Literal[0]: + def size_bits(self) -> Literal[0]: """Return the size of bits of this MemoryUnit.""" return 0 @property - def endianness(self: Self) -> str: + def endianness(self) -> str: """Return endianness of the architecture - "big" or "little" (default=little).""" return self._endianness @property - def value(self: Self) -> int: + def value(self) -> int: """Return value of the MemoryUnit as an integer.""" return self._value @value.setter - def value(self: Self, new_value: int) -> None: + def value(self, new_value: int) -> None: """Set the value of the MemoryUnit.""" self._value = int(new_value) & self._mask @property - def overflow(self: Self) -> bool: + def overflow(self) -> bool: """Return True if a MemoryUnit + overflow occurred on addition.""" return self._overflow @overflow.setter - def overflow(self: Self, overflow: bool) -> None: + def overflow(self, overflow: bool) -> None: """Set overflow status.""" self._overflow = overflow @property - def underflow(self: Self) -> bool: + def underflow(self) -> bool: """Return True if a MemoryUnit - overflow occurred on subtraction.""" return self._underflow @underflow.setter - def underflow(self: Self, underflow: bool) -> None: + def underflow(self, underflow: bool) -> None: """Set underflow status.""" self._underflow = underflow - def bits(self: Self) -> bitarray: + def bits(self) -> bitarray: """Return a bitarray of size self.size_bits with endianness self.endianness.""" - return int2ba(self._value, length=self.size_bits, endian=self._endianness) + return int2ba(self._value, self.size_bits, self._endianness) @property - def lowbyte(self: Self) -> int: + def lowbyte(self) -> int: """Return the low byte as an int.""" return self._value & 0xFF @property - def lowbyte_bits(self: Self) -> bitarray: + def lowbyte_bits(self) -> bitarray: """Return the low byte of this data type as a bitarray.""" - return int2ba(self._value & 0xFF, length=8, endian=self._endianness) + return int2ba(self._value & 0xFF, 8, self._endianness) @property - def highbyte(self: Self) -> int | None: + def highbyte(self) -> Union[int, None]: """Return the high byte as an int.""" if self.size_bits > 8: return (self._value >> 8) & 0xFF return None @property - def highbyte_bits(self: Self) -> bitarray | None: + def highbyte_bits(self) -> Union[bitarray, None]: """Return the high byte of this data type as a bitarray or None.""" if self.size_bits > 8: - return int2ba((self._value >> 8) & 0xFF, length=8, endian=self._endianness) + return int2ba((self._value >> 8) & 0xFF, 8, self._endianness) return None """ Math Operators""" - def __add__(self: Self, rvalue: int | bitarray) -> "MemoryUnit": + def __add__(self, rvalue: Union[int, bitarray]) -> "MemoryUnit": """Add an integer or bitarray to a MemoryUnit.""" # Get int value from rvalue if isinstance(rvalue, int): if rvalue == 0: - return type(self)(self._value, endianness=self._endianness) + return type(self)(self._value, self._endianness) local_rvalue = rvalue elif isinstance(rvalue, MemoryUnit): if rvalue._value == 0: # noqa: SLF001 - return type(self)(self._value, endianness=self._endianness) + return type(self)(self._value, self._endianness) local_rvalue = rvalue._value # noqa: SLF001 elif is_bitarray(rvalue): local_rvalue = ba2int(rvalue) if local_rvalue == 0: - return type(self)(self._value, endianness=self._endianness) + return type(self)(self._value, self._endianness) else: local_rvalue = int(rvalue) @@ -148,7 +148,7 @@ def __add__(self: Self, rvalue: int | bitarray) -> "MemoryUnit": if isinstance(self, Word): initial_msb = (self._value >> 8) & 0xFF new_value = (self._value + local_rvalue) & 0xFFFF - result = Word(new_value, endianness=self._endianness) + result = Word(new_value, self._endianness) result_msb = (new_value >> 8) & 0xFF if result_msb > initial_msb: @@ -163,33 +163,33 @@ def __add__(self: Self, rvalue: int | bitarray) -> "MemoryUnit": elif isinstance(self, Byte): new_value = (self._value + local_rvalue) & 0xFF - result = Byte(new_value, endianness=self._endianness) + result = Byte(new_value, self._endianness) return result - def __sub__(self: Self, rvalue: int | bitarray) -> "MemoryUnit": + def __sub__(self, rvalue: Union[int, bitarray]) -> "MemoryUnit": """Subtract an integer or bitarray from a MemoryUnit.""" # Get int value from rvalue if isinstance(rvalue, int): if rvalue == 0: - return type(self)(self._value, endianness=self._endianness) + return type(self)(self._value, self._endianness) local_rvalue = rvalue elif isinstance(rvalue, MemoryUnit): if rvalue._value == 0: # noqa: SLF001 - return type(self)(self._value, endianness=self._endianness) + return type(self)(self._value, self._endianness) local_rvalue = rvalue._value # noqa: SLF001 elif is_bitarray(rvalue): local_rvalue = ba2int(rvalue) if local_rvalue == 0: - return type(self)(self._value, endianness=self._endianness) + return type(self)(self._value, self._endianness) else: local_rvalue = int(rvalue) new_value = (self._value - local_rvalue) & self._mask - return type(self)(new_value, endianness=self._endianness) + return type(self)(new_value, self._endianness) """ Bitwise Operators """ - def __or__(self: Self, rvalue: int | bitarray) -> int: + def __or__(self, rvalue: Union[int, bitarray]) -> int: """Bitwise-OR this MemoryUnit with Byte|Word, bitarray, or int types.""" if isinstance(rvalue, MemoryUnit): return self._value | rvalue._value # noqa: SLF001 @@ -200,7 +200,7 @@ def __or__(self: Self, rvalue: int | bitarray) -> int: else: raise TypeError(f"Cannot perform bitwise OR on types {type(self)} and {type(rvalue)}") - def __and__(self: Self, rvalue: int | bitarray) -> int: + def __and__(self, rvalue: Union[int, bitarray]) -> int: """Bitwise-AND this MemoryUnit with Byte|Word, bitarray, or int types.""" if isinstance(rvalue, MemoryUnit): return self._value & rvalue._value # noqa: SLF001 @@ -211,16 +211,16 @@ def __and__(self: Self, rvalue: int | bitarray) -> int: else: raise TypeError(f"Cannot perform bitwise AND on types {type(self)} and {type(rvalue)}") - def __lshift__(self: Self, rvalue: int) -> int: + def __lshift__(self, rvalue: int) -> int: """Left-shift by rvalue bits. Returns int.""" return self._value << rvalue - def __rshift__(self: Self, rvalue: int) -> int: + def __rshift__(self, rvalue: int) -> int: """Right-shift by rvalue bits. Returns int.""" return self._value >> rvalue """ Equality Operators """ - def __lt__(self: Self, value: int | bitarray) -> bool: + def __lt__(self, value: Union[int, bitarray]) -> bool: """Less than comparison with MemoryUnit.""" if isinstance(value, MemoryUnit): return self._value < value._value # noqa: SLF001 @@ -228,7 +228,7 @@ def __lt__(self: Self, value: int | bitarray) -> bool: return self._value < ba2int(value) return self._value < value - def __gt__(self: Self, value: int | bitarray) -> bool: + def __gt__(self, value: Union[int, bitarray]) -> bool: """Greater than comparison with MemoryUnit.""" if isinstance(value, MemoryUnit): return self._value > value._value # noqa: SLF001 @@ -236,7 +236,7 @@ def __gt__(self: Self, value: int | bitarray) -> bool: return self._value > ba2int(value) return self._value > value - def __eq__(self: Self, value: int | bitarray) -> bool: + def __eq__(self, value: Union[int, bitarray]) -> bool: """Equality comparison with MemoryUnit.""" if isinstance(value, MemoryUnit): return self._value == value._value # noqa: SLF001 @@ -244,7 +244,7 @@ def __eq__(self: Self, value: int | bitarray) -> bool: return self._value == ba2int(value) return self._value == value - def __le__(self: Self, value: int | bitarray) -> bool: + def __le__(self, value: Union[int, bitarray]) -> bool: """Less than or equal to comparison with MemoryUnit.""" if isinstance(value, MemoryUnit): return self._value <= value._value # noqa: SLF001 @@ -252,7 +252,7 @@ def __le__(self: Self, value: int | bitarray) -> bool: return self._value <= ba2int(value) return self._value <= value - def __ge__(self: Self, value: int | bitarray) -> bool: + def __ge__(self, value: Union[int, bitarray]) -> bool: """Greater than or equal to comparison with MemoryUnit.""" if isinstance(value, MemoryUnit): return self._value >= value._value # noqa: SLF001 @@ -260,15 +260,15 @@ def __ge__(self: Self, value: int | bitarray) -> bool: return self._value >= ba2int(value) return self._value >= value - def __int__(self: Self) -> int: + def __int__(self) -> int: """Return this MemoryUnit's value as an integer.""" return self._value - def __format__(self: Self, specifier: str) -> str: + def __format__(self, specifier: str) -> str: """Return this MemoryUnit as indicated by the format specifier.""" return f"{self._value:{specifier}}" - def __repr__(self: Self) -> str: + def __repr__(self) -> str: """Describe this MemoryUnit in code.""" return str( f"{self.__class__.__name__}(" @@ -278,23 +278,23 @@ def __repr__(self: Self) -> str: ")", ) - def __str__(self: Self) -> str: + def __str__(self) -> str: """Describe this MemoryUnit as a hexadecimal value.""" return str(hex(self._value)) """ Index Operators """ - def __index__(self: Self) -> int: + def __index__(self) -> int: """Describe this MemoryUnit as an index.""" return self._value - def __getitem__(self: Self, index: int) -> int: + def __getitem__(self, index: int) -> int: """Return the specified bit value (0 or 1).""" return (self._value >> index) & 1 - def __delitem__(self: Self, index: int) -> None: + def __delitem__(self, index: int) -> None: """Not implemented.""" - def __setitem__(self: Self, index: int, value: int) -> None: + def __setitem__(self, index: int, value: int) -> None: """Set a bit on this MemoryUnit.""" if isinstance(value, MemoryUnit): bit_value = value._value & 1 # noqa: SLF001 @@ -319,7 +319,7 @@ class Byte(MemoryUnit): # Flyweight cache: pre-created Byte instances for values 0-255 # Populated lazily on first access to avoid circular import issues - _flyweight_cache: list["Byte"] | None = None + _flyweight_cache: Union[List["Byte"], None]= None def __new__(cls, value: int = 0, endianness: str = ENDIANNESS) -> "Byte": """Return cached Byte for common values, or create new instance. @@ -359,7 +359,7 @@ def _initialize_flyweight_cache(cls) -> None: instance._underflow = False cls._flyweight_cache[i] = instance - def __init__(self: Self, value: int = 0, endianness: str = ENDIANNESS) -> None: + def __init__(self, value: int = 0, endianness: str = ENDIANNESS) -> None: """Initialize a Byte(). Note: For flyweight instances (default endianness, values 0-255), @@ -379,15 +379,15 @@ def __init__(self: Self, value: int = 0, endianness: str = ENDIANNESS) -> None: return # Non-cached instance: full initialization - super().__init__(value=value, endianness=endianness) + super().__init__(value, endianness) @property - def size_bits(self: Self) -> Literal[8]: + def size_bits(self) -> Literal[8]: """Return the size in bits of this MemoryUnit (8 for Byte()).""" return 8 @property - def _mask(self: Self) -> int: + def _mask(self) -> int: """Return the bit mask for Byte (0xFF).""" return 0xFF @@ -397,7 +397,7 @@ class Word(MemoryUnit): log: logging.Logger = logging.getLogger("mos6502.memory.Word") - def __init__(self: Self, value: int = 0, endianness: str = ENDIANNESS) -> None: + def __init__(self, value: int = 0, endianness: str = ENDIANNESS) -> None: """Initialize a Word(). Arguments: @@ -405,15 +405,15 @@ def __init__(self: Self, value: int = 0, endianness: str = ENDIANNESS) -> None: value: a value between -32767 and 65535 (automatically masked to 16 bits) endianness: 'big' or 'little' (default: mos6502.memory.ENDIANNESS) """ - super().__init__(value=value, endianness=endianness) + super().__init__(value, endianness) @property - def size_bits(self: Self) -> Literal[16]: + def size_bits(self) -> Literal[16]: """Return the size in bits of this MemoryUnit (16 for Word()).""" return 16 @property - def _mask(self: Self) -> int: + def _mask(self) -> int: """Return the bit mask for Word (0xFFFF).""" return 0xFFFF @@ -423,15 +423,25 @@ class RAM(MutableSequence): log: logging.Logger = logging.getLogger("mos6502.memory.RAM") - def __init__(self: Self, endianness: str = ENDIANNESS, save_state: list[list] = None) -> None: - """Instantiate a mos6502 RAM bank.""" + def __init__(self, endianness: str = ENDIANNESS, save_state: List[list] = None, + preallocated_buffer: bytearray = None) -> None: + """Instantiate a mos6502 RAM bank. + + Arguments: + endianness: Byte order ('little' or 'big') + save_state: Optional saved state to restore + preallocated_buffer: Optional pre-allocated 64KB bytearray to use. + Useful on memory-constrained systems like Pico + where we need to allocate before imports fragment the heap. + """ super().__init__() self.endianness: str = endianness self.memory_handler = None # Optional external memory handler (for C64 banking, etc.) + self._preallocated = preallocated_buffer self._data: bytearray = bytearray() # Will be initialized in initialize() self.initialize() - def initialize(self: Self) -> None: + def initialize(self) -> None: """Initialize RAM to 0xFF (typical power-on state).""" # Real 6502 RAM contains unpredictable values on power-up # Using 0xFF avoids accidental BRK (0x00) execution @@ -441,38 +451,48 @@ def initialize(self: Self) -> None: if self.memory_handler is not None: return + # Use pre-allocated buffer if provided (for memory-constrained systems) + if self._preallocated is not None: + self._data = self._preallocated + # Fill with 0xFF + for i in range(len(self._data)): + self._data[i] = 0xFF + return + # Flat bytearray for performance - eliminates branching on every access - self._data: bytearray = bytearray([0xFF] * 0x10000) + # Use bytes multiplication to avoid creating a huge temporary list + # (bytearray([0xFF] * 0x10000) would create a 262KB list first!) + self._data: bytearray = bytearray(b'\xff' * 0x10000) @property - def zeropage(self: Self) -> memoryview: + def zeropage(self) -> memoryview: """Zero page ($0000-$00FF) as a view into the flat RAM array.""" return memoryview(self._data)[0x0000:0x0100] @property - def stack(self: Self) -> memoryview: + def stack(self) -> memoryview: """Stack page ($0100-$01FF) as a view into the flat RAM array.""" return memoryview(self._data)[0x0100:0x0200] @property - def heap(self: Self) -> memoryview: + def heap(self) -> memoryview: """Main memory ($0200-$FFFF) as a view into the flat RAM array.""" return memoryview(self._data)[0x0200:0x10000] @property - def data(self: Self) -> bytearray: + def data(self) -> bytearray: """Direct access to flat RAM array for performance-critical code.""" return self._data - def __repr__(self: Self) -> str: + def __repr__(self) -> str: """Return the code representation of the RAM.""" return f"<{self.__class__.__name__} {len(self)} bytes>" - def __len__(self: Self) -> int: + def __len__(self) -> int: """Return the length of the RAM.""" return 0x10000 # 64KB - def __getitem__(self: Self, index: int) -> int: + def __getitem__(self, index: int) -> int: """Get the RAM item at index {index}.""" # Delegate to external memory handler if set (e.g., C64 banking) if self.memory_handler is not None: @@ -481,10 +501,10 @@ def __getitem__(self: Self, index: int) -> int: # Direct flat array access - no branching return self._data[index] - def __delitem__(self: Self, index: int) -> None: + def __delitem__(self, index: int) -> None: """Not implemented.""" - def __setitem__(self: Self, index: int, value: int, length: int = 8) -> None: + def __setitem__(self, index: int, value: int, length: int = 8) -> None: """Set the RAM item at index {index} to value {value}.""" # Delegate to external memory handler if set (e.g., C64 banking) if self.memory_handler is not None: @@ -500,13 +520,13 @@ def __setitem__(self: Self, index: int, value: int, length: int = 8) -> None: # Direct flat array access - no branching self._data[index] = int_value - def insert(self: Self, index: int, value: int) -> None: + def insert(self, index: int, value: int) -> None: """Not implemented.""" - def append(self: Self, value: int) -> None: + def append(self, value: int) -> None: """Not implemented.""" - def memory_section(self: Self, address: int) -> Literal["zeropage", "stack", "heap"]: + def memory_section(self, address: int) -> Literal["zeropage", "stack", "heap"]: """Return the name of the memory section at location {address}.""" if address >= 0 and address < 256: return "zeropage" @@ -519,17 +539,17 @@ def memory_section(self: Self, address: int) -> Literal["zeropage", "stack", "he f"Invalid memory location: {address} should be between 0 and 65535", ) - def fill(self: Self, data: int) -> None: + def fill(self, data: int) -> None: """Fill the RAM with {data}.""" int_data = data._value if isinstance(data, MemoryUnit) else int(data) & 0xFF # noqa: SLF001 # Use efficient slice assignment instead of per-byte loop self._data[:] = bytes([int_data]) * 0x10000 - def read(self: Self, address: int, size: int = 1) -> list[int]: + def read(self, address: int, size: int = 1) -> List[int]: """Read size Bytes starting from address.""" return [self[address + offset] for offset in range(size)] - def write(self: Self, address: int, data: int) -> None: + def write(self, address: int, data: int) -> None: """Write a byte to address.""" # Extract int value if passed a MemoryUnit if isinstance(data, MemoryUnit): diff --git a/mos6502/pybitarray.py b/mos6502/pybitarray.py index e6f3a93..ed12a95 100644 --- a/mos6502/pybitarray.py +++ b/mos6502/pybitarray.py @@ -9,9 +9,8 @@ from mos6502.pybitarray import ba2int, int2ba """ -from __future__ import annotations -from typing import Literal, Self +from mos6502.compat import Literal, Union, List class pybitarray: @@ -29,10 +28,9 @@ class pybitarray: """ def __init__( - self: Self, - initializer: int | list[int] | "pybitarray" = 0, - *, - length: int | None = None, + self, + initializer: Union[int, List[int], "pybitarray"]= 0, + length: Union[int, None]= None, endian: Literal["little", "big"] = "little", ) -> None: """Initialize a pybitarray. @@ -47,7 +45,7 @@ def __init__( self._endian: Literal["little", "big"] = endian if isinstance(initializer, pybitarray): - self._bits: list[int] = initializer._bits[:] # noqa: SLF001 + self._bits: List[int] = initializer._bits[:] # noqa: SLF001 self._endian = initializer._endian # noqa: SLF001 elif isinstance(initializer, list): self._bits = initializer[:] @@ -60,15 +58,15 @@ def __init__( self._bits = [0] * (length or 8) @property - def endian(self: Self) -> Literal["little", "big"]: + def endian(self) -> Literal["little", "big"]: """Return the endianness of this bitarray.""" return self._endian - def __len__(self: Self) -> int: + def __len__(self) -> int: """Return the number of bits.""" return len(self._bits) - def __getitem__(self: Self, index: int | slice) -> "pybitarray | int": + def __getitem__(self, index: Union[int, slice]) -> Union["pybitarray, int"]: """Get bit(s) at index or slice. Returns: @@ -77,19 +75,19 @@ def __getitem__(self: Self, index: int | slice) -> "pybitarray | int": """ if isinstance(index, slice): - return pybitarray(self._bits[index], endian=self._endian) + return pybitarray(self._bits[index], None, self._endian) return self._bits[index] - def __setitem__(self: Self, index: int, value: int) -> None: + def __setitem__(self, index: int, value: int) -> None: """Set bit at index to value (0 or 1).""" self._bits[index] = value & 1 - def __lshift__(self: Self, count: int) -> "pybitarray": + def __lshift__(self, count: int) -> "pybitarray": """Left shift by count bits, preserving length.""" if count <= 0: - return pybitarray(self._bits[:], endian=self._endian) + return pybitarray(self._bits[:], None, self._endian) if count >= len(self._bits): - return pybitarray([0] * len(self._bits), endian=self._endian) + return pybitarray([0] * len(self._bits), None, self._endian) # For little-endian: shift means moving bits to higher indices # New low bits become 0, high bits fall off @@ -99,14 +97,14 @@ def __lshift__(self: Self, count: int) -> "pybitarray": # For big-endian: shift means moving bits to lower indices new_bits = self._bits[count:] + [0] * count - return pybitarray(new_bits, endian=self._endian) + return pybitarray(new_bits, None, self._endian) - def __rshift__(self: Self, count: int) -> "pybitarray": + def __rshift__(self, count: int) -> "pybitarray": """Right shift by count bits, preserving length.""" if count <= 0: - return pybitarray(self._bits[:], endian=self._endian) + return pybitarray(self._bits[:], None, self._endian) if count >= len(self._bits): - return pybitarray([0] * len(self._bits), endian=self._endian) + return pybitarray([0] * len(self._bits), None, self._endian) # For little-endian: shift means moving bits to lower indices # New high bits become 0, low bits fall off @@ -116,9 +114,9 @@ def __rshift__(self: Self, count: int) -> "pybitarray": # For big-endian: shift means moving bits to higher indices new_bits = [0] * count + self._bits[: len(self._bits) - count] - return pybitarray(new_bits, endian=self._endian) + return pybitarray(new_bits, None, self._endian) - def __eq__(self: Self, other: object) -> bool: + def __eq__(self, other: object) -> bool: """Equality comparison.""" if isinstance(other, pybitarray): return self._bits == other._bits @@ -126,31 +124,31 @@ def __eq__(self: Self, other: object) -> bool: return self._bits == other return NotImplemented - def __lt__(self: Self, other: "pybitarray") -> bool: + def __lt__(self, other: "pybitarray") -> bool: """Less than comparison (compares integer values).""" if isinstance(other, pybitarray): return ba2int(self) < ba2int(other) return NotImplemented - def __le__(self: Self, other: "pybitarray") -> bool: + def __le__(self, other: "pybitarray") -> bool: """Less than or equal comparison.""" if isinstance(other, pybitarray): return ba2int(self) <= ba2int(other) return NotImplemented - def __gt__(self: Self, other: "pybitarray") -> bool: + def __gt__(self, other: "pybitarray") -> bool: """Greater than comparison.""" if isinstance(other, pybitarray): return ba2int(self) > ba2int(other) return NotImplemented - def __ge__(self: Self, other: "pybitarray") -> bool: + def __ge__(self, other: "pybitarray") -> bool: """Greater than or equal comparison.""" if isinstance(other, pybitarray): return ba2int(self) >= ba2int(other) return NotImplemented - def tobytes(self: Self) -> bytes: + def tobytes(self) -> bytes: """Convert bitarray to bytes. Returns: @@ -176,17 +174,17 @@ def tobytes(self: Self) -> bytes: return bytes(result) - def __repr__(self: Self) -> str: + def __repr__(self) -> str: """Return string representation.""" bits_str = "".join(str(b) for b in self._bits) return f"pybitarray('{bits_str}', endian='{self._endian}')" - def __str__(self: Self) -> str: + def __str__(self) -> str: """Return string representation of bits.""" return "".join(str(b) for b in self._bits) -def _int_to_bits(value: int, length: int, endian: Literal["little", "big"]) -> list[int]: +def _int_to_bits(value: int, length: int, endian: Literal["little", "big"]) -> List[int]: """Convert an integer to a list of bits. Args: @@ -233,7 +231,7 @@ def int2ba( pybitarray representing the value """ - return pybitarray(value, length=length, endian=endian) + return pybitarray(value, length, endian) def ba2int(ba: pybitarray) -> int: diff --git a/mos6502/registers.py b/mos6502/registers.py index 858f1d5..2fb3cbb 100755 --- a/mos6502/registers.py +++ b/mos6502/registers.py @@ -7,7 +7,6 @@ Performance optimization: Registers are stored as plain ints, not Byte/Word objects. This eliminates object creation and bitarray conversion overhead on every access. """ -from typing import Self class Registers: @@ -23,7 +22,7 @@ class Registers: __slots__ = ('_PC', '_S', '_A', '_X', '_Y') def __init__( - self: Self, + self, endianness: str = "little", # Kept for API compatibility, not used PC: int = 0x0000, # noqa: N803 S: int = 0x0000, # noqa: N803 @@ -51,17 +50,17 @@ def __init__( self._Y: int = Y & 0xFF @property - def PC(self: Self) -> int: # noqa: N802 + def PC(self) -> int: # noqa: N802 """Return the PC register as an int (16-bit).""" return self._PC @PC.setter - def PC(self: Self, value: int) -> None: # noqa: N802 + def PC(self, value: int) -> None: # noqa: N802 """Set the PC register (16-bit, masked to 0x0000-0xFFFF).""" self._PC = value & 0xFFFF @property - def S(self: Self) -> int: # noqa: N802 + def S(self) -> int: # noqa: N802 """Return the S (stack pointer) register as an int. The stack pointer is 8-bit in hardware but stored with the page 1 @@ -70,36 +69,36 @@ def S(self: Self) -> int: # noqa: N802 return self._S @S.setter - def S(self: Self, value: int) -> None: # noqa: N802 + def S(self, value: int) -> None: # noqa: N802 """Set the S register (masked to 9 bits: 0x0100-0x01FF range).""" self._S = value & 0x1FF @property - def A(self: Self) -> int: # noqa: N802 + def A(self) -> int: # noqa: N802 """Return the A (accumulator) register as an int (8-bit).""" return self._A @A.setter - def A(self: Self, value: int) -> None: # noqa: N802 + def A(self, value: int) -> None: # noqa: N802 """Set the A register (8-bit, masked to 0x00-0xFF).""" self._A = value & 0xFF @property - def X(self: Self) -> int: # noqa: N802 + def X(self) -> int: # noqa: N802 """Return the X index register as an int (8-bit).""" return self._X @X.setter - def X(self: Self, value: int) -> None: # noqa: N802 + def X(self, value: int) -> None: # noqa: N802 """Set the X register (8-bit, masked to 0x00-0xFF).""" self._X = value & 0xFF @property - def Y(self: Self) -> int: # noqa: N802 + def Y(self) -> int: # noqa: N802 """Return the Y index register as an int (8-bit).""" return self._Y @Y.setter - def Y(self: Self, value: int) -> None: # noqa: N802 + def Y(self, value: int) -> None: # noqa: N802 """Set the Y register (8-bit, masked to 0x00-0xFF).""" self._Y = value & 0xFF diff --git a/mos6502/timing.py b/mos6502/timing.py index c3d58a1..2a34425 100644 --- a/mos6502/timing.py +++ b/mos6502/timing.py @@ -24,10 +24,10 @@ """ import ctypes -import logging +from mos6502.compat import logging import sys import time -from typing import Protocol, Optional +from mos6502.compat import Protocol, Optional log = logging.getLogger(__name__) @@ -67,7 +67,11 @@ def name(self) -> str: @property def resolution(self) -> float: - return time.get_clock_info('monotonic').resolution + # MicroPython doesn't have get_clock_info, use a reasonable default + try: + return time.get_clock_info('monotonic').resolution + except AttributeError: + return 0.001 # 1ms default for MicroPython def now(self) -> float: return time.monotonic() diff --git a/mos6502/variants.py b/mos6502/variants.py index 56cedb3..21d0b12 100644 --- a/mos6502/variants.py +++ b/mos6502/variants.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """CPU variant definitions for the MOS 6502 family.""" -import enum -from typing import Self +from mos6502.compat import enum +from mos6502.compat import Union, Dict # ============================================================================= @@ -28,7 +28,9 @@ class UnstableOpcodeConfig: unstable_stores_enabled: Whether SHA/SHX/SHY/TAS are enabled (False for CMOS). """ - def __init__(self, ane_const: int | None, unstable_stores_enabled: bool = True) -> None: + __slots__ = ('ane_const', 'unstable_stores_enabled') + + def __init__(self, ane_const: Union[int, None], unstable_stores_enabled: bool = True) -> None: """Initialize unstable opcode configuration. Arguments: @@ -42,11 +44,11 @@ def __init__(self, ane_const: int | None, unstable_stores_enabled: bool = True) # Default configurations for each CPU variant # Users can override these by modifying cpu.unstable_config after creation -UNSTABLE_OPCODE_DEFAULTS: dict[str, UnstableOpcodeConfig] = { - "6502": UnstableOpcodeConfig(ane_const=0xFF, unstable_stores_enabled=True), - "6502A": UnstableOpcodeConfig(ane_const=0xFF, unstable_stores_enabled=True), - "6502C": UnstableOpcodeConfig(ane_const=0xEE, unstable_stores_enabled=True), # Some 6502C chips use 0xEE - "65C02": UnstableOpcodeConfig(ane_const=None, unstable_stores_enabled=False), # CMOS: all illegal opcodes are NOPs +UNSTABLE_OPCODE_DEFAULTS: Dict[str, UnstableOpcodeConfig] = { + "6502": UnstableOpcodeConfig(0xFF, True), + "6502A": UnstableOpcodeConfig(0xFF, True), + "6502C": UnstableOpcodeConfig(0xEE, True), # Some 6502C chips use 0xEE + "65C02": UnstableOpcodeConfig(None, False), # CMOS: all illegal opcodes are NOPs } @@ -59,7 +61,7 @@ class CPUVariant(enum.Enum): CMOS_65C02 = "65C02" # CMOS 65C02 with bug fixes and new instructions @classmethod - def from_string(cls: type[Self], variant: str) -> Self: + def from_string(cls, variant: str) -> "CPUVariant": """Parse variant string to enum value. Arguments: @@ -74,12 +76,18 @@ def from_string(cls: type[Self], variant: str) -> Self: ------ ValueError: If variant string is not recognized """ - variant_map = {v.value: v for v in cls} + # Explicit mapping for MicroPython compatibility (can't iterate over Enum) + variant_map = { + "6502": cls.NMOS_6502, + "6502A": cls.NMOS_6502A, + "6502C": cls.NMOS_6502C, + "65C02": cls.CMOS_65C02, + } if variant not in variant_map: valid_variants = ", ".join(variant_map.keys()) raise ValueError(f"Unknown CPU variant: {variant}. Valid variants: {valid_variants}") return variant_map[variant] - def __str__(self: Self) -> str: + def __str__(self) -> str: """Return string representation of variant.""" return self.value diff --git a/poetry.lock b/poetry.lock index 0639bd6..4c5d812 100644 --- a/poetry.lock +++ b/poetry.lock @@ -181,8 +181,10 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} packaging = ">=19.1" pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] uv = ["uv (>=0.1.18)"] @@ -350,6 +352,7 @@ files = [ [package.dependencies] packaging = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] cli = ["tomli ; python_version < \"3.11\""] @@ -366,6 +369,25 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["test"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "execnet" version = "2.1.2" @@ -408,6 +430,31 @@ files = [ [package.extras] tests = ["freezegun", "pytest", "pytest-cov"] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["test"] +markers = "python_full_version < \"3.10.2\"" +files = [ + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.3.0" @@ -475,6 +522,7 @@ colorlog = ">=2.6.1,<7" dependency-groups = ">=1.1" humanize = ">=4" packaging = {version = ">=21", markers = "python_version >= \"3.10\""} +tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} virtualenv = {version = ">=20.15", markers = "python_version >= \"3.10\""} [package.extras] @@ -646,10 +694,12 @@ files = [ [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1.0.1" packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] @@ -812,31 +862,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.14.8" +version = "0.14.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["test"] files = [ - {file = "ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb"}, - {file = "ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9"}, - {file = "ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2"}, - {file = "ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a"}, - {file = "ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b"}, - {file = "ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf"}, - {file = "ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6"}, - {file = "ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e"}, - {file = "ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7"}, - {file = "ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097"}, - {file = "ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99"}, - {file = "ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed"}, + {file = "ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75"}, + {file = "ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2"}, + {file = "ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a"}, + {file = "ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed"}, + {file = "ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b"}, + {file = "ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567"}, + {file = "ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a"}, + {file = "ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8"}, + {file = "ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197"}, + {file = "ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2"}, + {file = "ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84"}, + {file = "ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b"}, ] [[package]] @@ -875,6 +925,59 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +[[package]] +name = "tomli" +version = "2.3.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["test"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, +] + [[package]] name = "tomlkit" version = "0.13.3" @@ -887,6 +990,19 @@ files = [ {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["test"] +markers = "python_version == \"3.10\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + [[package]] name = "virtualenv" version = "20.35.4" @@ -903,11 +1019,33 @@ files = [ distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["test"] +markers = "python_full_version < \"3.10.2\"" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [extras] display = ["pygame-ce"] full = ["bitarray", "pygame-ce"] @@ -915,5 +1053,5 @@ native = ["bitarray"] [metadata] lock-version = "2.1" -python-versions = ">=3.11,<4.0" -content-hash = "17a622ce68b6941238bc344ac9b5536d2fe1e5f4f534ae401590d46fb24d39f1" +python-versions = ">=3.10,<4.0" +content-hash = "15b531f37e607080caf1642eaa6ba2210f6c0fa41e4107b1391fd726a4197211" diff --git a/pyproject.toml b/pyproject.toml index 19e0913..fc0af95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,11 @@ authors = ["Terry Simons "] packages = [ { include = "mos6502" }, { include = "c64", from = "systems" }, + { include = "scripts" }, ] [tool.poetry.dependencies] -python = ">=3.11,<4.0" +python = ">=3.10,<4.0" bitarray = {version = "*", optional = true} pygame-ce = {version = "*", optional = true} @@ -21,6 +22,8 @@ full = ["bitarray", "pygame-ce"] [tool.poetry.scripts] c64 = "c64:main" c64-benchmark = "c64.benchmark:main" +build-pico = "scripts.build_pico:main" +deploy-pico = "scripts.deploy_pico:main" [tool.poetry.group.test.dependencies] @@ -38,6 +41,10 @@ pyupgrade = "*" bump2version = "*" toml = "*" +[tool.poetry.group.pico.dependencies] +mpy-cross = "*" +mpremote = "*" + [tool.ruff] line-length = 100 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..13e43f5 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Build and deployment scripts for mos6502.""" diff --git a/scripts/build_firmware.py b/scripts/build_firmware.py new file mode 100644 index 0000000..70500e7 --- /dev/null +++ b/scripts/build_firmware.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 +"""Build MicroPython firmware with frozen mos6502/c64 modules. + +This builds custom MicroPython firmware (.uf2) with the emulator modules +frozen into flash. Frozen modules don't consume heap RAM, solving the +memory fragmentation issues on Pico. + +Prerequisites: + - Git (to clone MicroPython) + - ARM toolchain: arm-none-eabi-gcc (for ARM builds) + - RISC-V toolchain: riscv32-unknown-elf-gcc (for RISC-V builds) + - CMake, Make + +On macOS: + brew install cmake arm-none-eabi-gcc + # For RISC-V: brew tap riscv/riscv && brew install riscv-gnu-toolchain + +On Ubuntu/Debian: + sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi + # For RISC-V: sudo apt install gcc-riscv64-unknown-elf + +Usage: + python scripts/build_firmware.py # Build both ARM and RISC-V + python scripts/build_firmware.py --arch arm # Build ARM only + python scripts/build_firmware.py --arch riscv # Build RISC-V only + python scripts/build_firmware.py --clean # Clean and rebuild +""" + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + +from pico_config import ( + ROOT, + MOS6502_SRC, + C64_SRC, + DIST_FIRMWARE, + PICO2_BOARDS, + PICO_EXCLUDE, +) + +# MicroPython repository +MICROPYTHON_REPO = "https://github.com/micropython/micropython.git" +MICROPYTHON_DIR = ROOT / "micropython" + +# Manifest template +# We freeze from a single parent directory to avoid C symbol conflicts +# (both mos6502/memory.py and c64/memory.py would conflict if frozen separately) +MANIFEST_TEMPLATE = '''# Frozen modules for C64 emulator +# Auto-generated by build_firmware.py + +# Include default Pico modules +include("$(PORT_DIR)/boards/manifest.py") + +# Freeze micropython-lib dependencies (single files) +freeze("{staging_path}", "ucontextlib.py", opt=0) + +# Freeze mos6502 and c64 packages from the staging directory +# Using package() with base_path ensures correct module names (mos6502, c64, not frozen_modules/...) +package("{staging_path}/mos6502", base_path="{staging_path}", opt=0) +package("{staging_path}/c64", base_path="{staging_path}", opt=0) + +# Freeze embedded ROMs if present +import os +if os.path.exists("{staging_path}/roms/__init__.py"): + package("{staging_path}/roms", base_path="{staging_path}", opt=3) +''' + + +def run_command(cmd, cwd=None, check=True, env=None): + """Run a command and print output.""" + print(f" Running: {' '.join(cmd)}") + result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, env=env) + if result.stdout: + for line in result.stdout.strip().split('\n'): + print(f" {line}") + if result.returncode != 0: + print(f" ERROR: Command failed with code {result.returncode}") + if result.stderr: + for line in result.stderr.strip().split('\n'): + print(f" {line}") + if check: + sys.exit(1) + return result + + +def check_toolchain(arch): + """Check if required toolchain is installed.""" + if arch == "arm": + compiler = "arm-none-eabi-gcc" + else: # riscv + # Try different RISC-V compiler names + for name in ["riscv64-elf-gcc", "riscv32-unknown-elf-gcc", "riscv64-unknown-elf-gcc", "riscv-none-elf-gcc"]: + if shutil.which(name): + return True + print(f"ERROR: RISC-V toolchain not found.") + print("Install with:") + print(" macOS: brew tap riscv-software-src/riscv && brew install riscv-tools") + print(" Ubuntu: sudo apt install gcc-riscv64-unknown-elf") + return False + + if not shutil.which(compiler): + print(f"ERROR: {compiler} not found.") + print("Install with:") + print(" macOS: brew install arm-none-eabi-gcc") + print(" Ubuntu: sudo apt install gcc-arm-none-eabi") + return False + return True + + +def clone_micropython(): + """Clone MicroPython repository if not present.""" + if MICROPYTHON_DIR.exists(): + print(f"MicroPython already cloned at {MICROPYTHON_DIR}") + # Update to latest + print("Updating MicroPython...") + run_command(["git", "pull"], cwd=MICROPYTHON_DIR, check=False) + return + + print(f"Cloning MicroPython to {MICROPYTHON_DIR}...") + run_command(["git", "clone", "--depth", "1", MICROPYTHON_REPO, str(MICROPYTHON_DIR)]) + + +def build_mpy_cross(): + """Build mpy-cross if not already built.""" + mpy_cross = MICROPYTHON_DIR / "mpy-cross" / "build" / "mpy-cross" + if mpy_cross.exists(): + print("mpy-cross already built") + return + + print("Building mpy-cross...") + run_command(["make"], cwd=MICROPYTHON_DIR / "mpy-cross") + + +def prepare_frozen_modules(): + """Prepare modules directory for freezing. + + We copy modules to a staging directory, excluding files not needed on Pico. + """ + staging_dir = DIST_FIRMWARE / "frozen_modules" + + # Clean staging directory + if staging_dir.exists(): + shutil.rmtree(staging_dir) + staging_dir.mkdir(parents=True) + + def should_exclude(rel_path): + rel_str = str(rel_path) + for pattern in PICO_EXCLUDE: + if rel_str.endswith(pattern) or pattern in rel_str: + return True + return False + + # Copy mos6502 + print("Staging mos6502 modules...") + mos6502_staging = staging_dir / "mos6502" + count = 0 + for py_file in MOS6502_SRC.rglob("*.py"): + if "__pycache__" in str(py_file) or "/tests/" in str(py_file): + continue + rel_path = py_file.relative_to(MOS6502_SRC) + if should_exclude(rel_path): + continue + dest = mos6502_staging / rel_path + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(py_file, dest) + count += 1 + print(f" Staged {count} mos6502 files") + + # Copy c64 + print("Staging c64 modules...") + c64_staging = staging_dir / "c64" + count = 0 + for py_file in C64_SRC.rglob("*.py"): + if "__pycache__" in str(py_file) or "/tests/" in str(py_file): + continue + rel_path = py_file.relative_to(C64_SRC) + if should_exclude(rel_path): + continue + dest = c64_staging / rel_path + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(py_file, dest) + count += 1 + print(f" Staged {count} c64 files") + + # Download micropython-lib dependencies + print("Downloading micropython-lib dependencies...") + micropython_lib_modules = [ + # (module_name, url) + ("ucontextlib", "https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/ucontextlib/ucontextlib.py"), + ] + for module_name, url in micropython_lib_modules: + dest = staging_dir / f"{module_name}.py" + try: + import urllib.request + print(f" Downloading {module_name}...") + urllib.request.urlretrieve(url, dest) + print(f" Downloaded {module_name}.py") + except Exception as e: + print(f" WARNING: Failed to download {module_name}: {e}") + + # Copy ROM files if they exist (embed as Python module) + # Search in multiple directories for ROMs + rom_search_dirs = [ + ROOT / "tests" / "fixtures" / "c64" / "roms", + ROOT / "roms", + ROOT / "systems" / "c64" / "roms", + ] + + # Map of ROM variable name to possible filenames (in priority order) + rom_filenames = { + "BASIC_ROM": ["basic.bin", "basic.rom", "basic", "basic.901226-01.bin"], + "KERNAL_ROM": ["kernal.bin", "kernal.rom", "kernal", "kernal.901227-03.bin"], + "CHAR_ROM": ["characters.bin", "characters.rom", "char.bin", "char.rom", + "char", "characters.901225-01.bin", "chargen"], + } + + roms_found = {} + for varname, filenames in rom_filenames.items(): + for search_dir in rom_search_dirs: + if not search_dir.exists(): + continue + for filename in filenames: + rom_path = search_dir / filename + if rom_path.exists(): + roms_found[varname] = rom_path.read_bytes() + print(f" Found {varname}: {rom_path}") + break + if varname in roms_found: + break + + if roms_found: + print(f"Embedding ROMs: {', '.join(roms_found.keys())}") + # Create a roms.py module with embedded ROM data + roms_staging = staging_dir / "roms" + roms_staging.mkdir(parents=True, exist_ok=True) + + # Create __init__.py with ROM data + roms_py = roms_staging / "__init__.py" + with open(roms_py, "w") as f: + f.write('"""Embedded C64 ROM data for Pico firmware."""\n\n') + for varname, data in roms_found.items(): + # Write as hex-escaped bytes literal + hex_str = data.hex() + f.write(f"{varname} = bytes.fromhex(\n") + # Split into 64-char chunks for readability + for i in range(0, len(hex_str), 64): + chunk = hex_str[i:i+64] + f.write(f' "{chunk}"\n') + f.write(")\n\n") + print(f" Created roms module ({sum(len(d) for d in roms_found.values())} bytes)") + else: + print("No ROM files found - ROMs not embedded") + print(" Searched directories:") + for search_dir in rom_search_dirs: + exists = "exists" if search_dir.exists() else "not found" + print(f" {search_dir} ({exists})") + + return staging_dir + + +def create_manifest(staging_dir): + """Create manifest.py for freezing modules.""" + manifest_content = MANIFEST_TEMPLATE.format( + staging_path=str(staging_dir), + ) + + manifest_path = DIST_FIRMWARE / "manifest.py" + manifest_path.write_text(manifest_content) + print(f"Created manifest at {manifest_path}") + return manifest_path + + +def build_firmware(arch, manifest_path, clean=False): + """Build MicroPython firmware for specified architecture.""" + config = PICO2_BOARDS[arch] + board = config["board"] + platform = config["platform"] + port_dir = MICROPYTHON_DIR / "ports" / "rp2" + + print(f"\nBuilding firmware for {board} ({arch})...") + + # Set up environment - use current Python (respects virtualenv/pyenv) + env = os.environ.copy() + env["PYTHON"] = sys.executable + # Put current Python's directory first in PATH so CMake finds it + python_dir = str(Path(sys.executable).parent) + env["PATH"] = python_dir + os.pathsep + env.get("PATH", "") + env["FROZEN_MANIFEST"] = str(manifest_path) + print(f" Using Python: {sys.executable}") + + # CMake arguments to force our Python + cmake_args = f"-DPython3_EXECUTABLE={sys.executable}" + + # Build make arguments + make_args = [f"BOARD={board}"] + if platform: + # For RISC-V, use MicroPython's BOARD_VARIANT mechanism + # This loads mpconfigvariant_RISCV.cmake which sets PICO_PLATFORM correctly + make_args.append("BOARD_VARIANT=RISCV") + # Build dir is automatically set to build-RPI_PICO2-RISCV by Makefile + build_dir_name = f"build-{board}-RISCV" + + # Find and set RISC-V toolchain path so cmake finds the right compiler + riscv_compiler = None + for name in ["riscv64-elf-gcc", "riscv32-unknown-elf-gcc", "riscv64-unknown-elf-gcc", "riscv-none-elf-gcc"]: + path = shutil.which(name) + if path: + riscv_compiler = path + break + if riscv_compiler: + toolchain_path = str(Path(riscv_compiler).parent.parent) + env["PICO_TOOLCHAIN_PATH"] = toolchain_path + # Also set the GCC triple for pico-sdk + compiler_name = Path(riscv_compiler).name + gcc_triple = compiler_name.replace("-gcc", "") + env["PICO_GCC_TRIPLE"] = gcc_triple + cmake_args += f" -DPICO_GCC_TRIPLE={gcc_triple}" + print(f" Using RISC-V toolchain: {toolchain_path} ({gcc_triple})") + else: + # ARM build directory (e.g., build-RPI_PICO2) + build_dir_name = f"build-{board}" + + # Set CMAKE_ARGS in environment (not command line) so Makefile can append to it + # If we pass it on command line, it overrides the Makefile's += assignments + env["CMAKE_ARGS"] = cmake_args + + # Clean if requested - must delete build dir to clear CMake cache + # Do this BEFORE submodules since submodules runs cmake + if clean: + print("Cleaning previous build...") + build_dir_to_clean = port_dir / build_dir_name + if build_dir_to_clean.exists(): + shutil.rmtree(build_dir_to_clean) + print(f" Removed {build_dir_to_clean}") + + # Initialize submodules if needed + print("Initializing submodules...") + run_command(["make"] + make_args + ["submodules"], cwd=port_dir, env=env) + + # Build with custom manifest + num_cores = os.cpu_count() or 4 + build_cmd = ["make"] + make_args + [f"-j{num_cores}"] + print(f"Building firmware: {' '.join(build_cmd)}") + result = subprocess.run( + build_cmd, + cwd=port_dir, + env=env, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + print(f"ERROR: Build failed for {board} ({arch})") + print("STDOUT:") + print(result.stdout[-5000:] if len(result.stdout) > 5000 else result.stdout) + print("STDERR:") + print(result.stderr[-5000:] if len(result.stderr) > 5000 else result.stderr) + return None + + # Print last part of build output for debugging + if result.stdout: + lines = result.stdout.strip().split('\n') + print(f" Build output ({len(lines)} lines, showing last 10):") + for line in lines[-10:]: + print(f" {line}") + + # Find the output firmware (build dir is e.g., build-RPI_PICO2) + build_dir = port_dir / build_dir_name + firmware_uf2 = build_dir / "firmware.uf2" + + if not firmware_uf2.exists(): + # Try alternate build directory naming (without -RISCV suffix) + alt_build_dir = port_dir / f"build-{board}" + alt_firmware = alt_build_dir / "firmware.uf2" + if alt_firmware.exists(): + firmware_uf2 = alt_firmware + else: + print(f"ERROR: Firmware not found at {firmware_uf2}") + print(f" Also checked: {alt_firmware}") + # List available build directories + print(f" Available in {port_dir}:") + for item in port_dir.iterdir(): + if item.is_dir() and item.name.startswith("build"): + print(f" {item.name}/") + return None + + # Copy to dist directory + output_name = f"micropython-c64-pico2-{arch}.uf2" + output_path = DIST_FIRMWARE / output_name + shutil.copy2(firmware_uf2, output_path) + + # Get file size + size_kb = output_path.stat().st_size / 1024 + print(f" Built: {output_path} ({size_kb:.0f} KB)") + + return output_path + + +def clean_build_dirs(architectures, clean_all=False): + """Clean build directories without building. + + Args: + architectures: List of architectures to clean + clean_all: If True, also clean mpy-cross and __pycache__ directories + """ + port_dir = MICROPYTHON_DIR / "ports" / "rp2" + + # Clean MicroPython build directories + print("MicroPython build directories:") + for arch in architectures: + board_info = PICO2_BOARDS.get(arch) + if not board_info: + continue + board = board_info["board"] + platform = board_info.get("platform") + + if platform: + build_dir_name = f"build-{board}-RISCV" + else: + build_dir_name = f"build-{board}" + + build_dir = port_dir / build_dir_name + if build_dir.exists(): + shutil.rmtree(build_dir) + print(f" Removed {build_dir}") + else: + print(f" {build_dir} (not present)") + + # Clean entire dist/firmware directory (staging, .uf2, .mpy files) + print("\nDist directory:") + if DIST_FIRMWARE.exists(): + shutil.rmtree(DIST_FIRMWARE) + print(f" Removed {DIST_FIRMWARE}") + else: + print(f" {DIST_FIRMWARE} (not present)") + + # Also clean dist/pico if it exists (.mpy deployment files) + from pico_config import DIST_PICO + print("\nPico deployment directory:") + if DIST_PICO.exists(): + shutil.rmtree(DIST_PICO) + print(f" Removed {DIST_PICO}") + else: + print(f" {DIST_PICO} (not present)") + + if clean_all: + # Clean mpy-cross build + mpy_cross_build = MICROPYTHON_DIR / "mpy-cross" / "build" + print("\nmpy-cross build directory:") + if mpy_cross_build.exists(): + shutil.rmtree(mpy_cross_build) + print(f" Removed {mpy_cross_build}") + else: + print(f" {mpy_cross_build} (not present)") + + # Clean __pycache__ directories in mos6502 and systems/c64 + print("\n__pycache__ directories:") + for search_dir in [ROOT / "mos6502", ROOT / "systems" / "c64"]: + if search_dir.exists(): + for pycache in search_dir.rglob("__pycache__"): + shutil.rmtree(pycache) + print(f" Removed {pycache}") + + # Clean /lib on connected Pico device (removes filesystem overrides) + print("\nPico device /lib directory:") + mpremote = shutil.which("mpremote") + if mpremote: + try: + # Check if device is connected and /lib exists + result = subprocess.run( + [mpremote, "fs", "ls", "/lib/"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0 and result.stdout.strip(): + # Remove /lib directory recursively + subprocess.run( + [mpremote, "exec", "import os; [os.remove('/lib/' + f) if not os.stat('/lib/' + f)[0] & 0x4000 else None for f in os.listdir('/lib/')]"], + capture_output=True, + timeout=10, + ) + # Try to remove subdirectories (c64, mos6502, etc.) + for subdir in ["c64", "mos6502", "roms"]: + subprocess.run( + [mpremote, "exec", f"import os\ntry:\n for f in os.listdir('/lib/{subdir}'):\n try: os.remove('/lib/{subdir}/' + f)\n except: pass\n os.rmdir('/lib/{subdir}')\nexcept: pass"], + capture_output=True, + timeout=10, + ) + print(" Cleaned /lib on device") + else: + print(" /lib not present or empty on device") + except subprocess.TimeoutExpired: + print(" No device connected (timeout)") + except Exception as e: + print(f" Could not clean device: {e}") + else: + print(" mpremote not found, skipping device clean") + + +def main(): + parser = argparse.ArgumentParser( + description="Build MicroPython firmware with frozen C64 emulator modules" + ) + parser.add_argument( + "--arch", + choices=["arm", "riscv", "both"], + default="both", + help="Target architecture (default: both)" + ) + parser.add_argument( + "--build", + action="store_true", + help="Build firmware (required unless --clean-only)" + ) + parser.add_argument( + "--clean", + action="store_true", + help="Clean build directories before building (use with --build)" + ) + parser.add_argument( + "--clean-only", + action="store_true", + help="Only clean build directories, don't build" + ) + parser.add_argument( + "--clean-all", + action="store_true", + help="Deep clean: also remove mpy-cross build and __pycache__ dirs" + ) + parser.add_argument( + "--skip-clone", + action="store_true", + help="Skip cloning/updating MicroPython (use existing)" + ) + args = parser.parse_args() + + # Determine architectures + if args.arch == "both": + architectures = ["arm", "riscv"] + else: + architectures = [args.arch] + + # Handle clean-only mode + if args.clean_only: + print("=" * 60) + print("Cleaning Build Directories" + (" (deep clean)" if args.clean_all else "")) + print("=" * 60) + print("\nCleaning...") + clean_build_dirs(architectures, args.clean_all) + print("\nClean complete.") + return + + # Require --build flag to actually build + if not args.build: + print("Usage: build_firmware.py --build [--arch arm|riscv|both] [--clean] [--clean-all]") + print(" build_firmware.py --clean-only [--clean-all] [--arch arm|riscv|both]") + print("\nOptions:") + print(" --build Build firmware") + print(" --clean Clean before building (use with --build)") + print(" --clean-only Only clean, don't build") + print(" --clean-all Deep clean: mpy-cross, __pycache__ (use with --clean or --clean-only)") + print(" --arch Target architecture (default: both)") + print(" --skip-clone Skip MicroPython clone/update") + sys.exit(0) + + print("=" * 60) + print("Building MicroPython Firmware with Frozen Modules") + print("=" * 60) + + # If --clean flag, clean everything first + if args.clean: + print("\nCleaning all build directories first" + (" (deep clean)" if args.clean_all else "") + "...") + clean_build_dirs(architectures, args.clean_all) + + # Check toolchains + print("\nChecking toolchains...") + for arch in architectures: + if not check_toolchain(arch): + sys.exit(1) + print(f" {arch}: OK") + + # Clone/update MicroPython + if not args.skip_clone: + print("\nPreparing MicroPython...") + clone_micropython() + build_mpy_cross() + + # Create output directory + DIST_FIRMWARE.mkdir(parents=True, exist_ok=True) + + # Prepare frozen modules + print("\nPreparing frozen modules...") + staging_dir = prepare_frozen_modules() + + # Create manifest + manifest_path = create_manifest(staging_dir) + + # Build firmware for each architecture + results = {} + for arch in architectures: + result = build_firmware(arch, manifest_path, clean=args.clean) + results[arch] = result + + # Summary + print("\n" + "=" * 60) + print("Build Summary") + print("=" * 60) + + success = True + for arch, path in results.items(): + if path: + print(f" {arch}: {path}") + else: + print(f" {arch}: FAILED") + success = False + + if success: + print("\nTo flash firmware:") + print(" 1. Hold BOOTSEL button on Pico 2") + print(" 2. Connect USB while holding BOOTSEL") + print(" 3. Copy .uf2 file to the RPI-RP2 drive") + print(f"\nFirmware files are in: {DIST_FIRMWARE}") + + print("=" * 60) + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build_pico.py b/scripts/build_pico.py new file mode 100644 index 0000000..8f9c655 --- /dev/null +++ b/scripts/build_pico.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Build MicroPython-compatible .mpy files for Pico deployment. + +This builds .mpy bytecode files that can be deployed to the Pico via mpremote. +For frozen firmware builds (better performance), use build_firmware.py instead. + +Usage: + python scripts/build_pico.py + poetry run build-pico +""" + +import shutil +import subprocess +import sys +from pathlib import Path + +from pico_config import ( + ROOT, + MOS6502_SRC, + C64_SRC, + DIST_PICO, + MPY_OPT, + is_excluded, +) + + +def find_mpy_cross(): + """Find mpy-cross executable.""" + # Check if in PATH + if shutil.which("mpy-cross"): + return "mpy-cross" + + # Check common locations + for path in [ + Path.home() / ".local" / "bin" / "mpy-cross", + Path("/usr/local/bin/mpy-cross"), + ]: + if path.exists(): + return str(path) + + print("ERROR: mpy-cross not found. Install with: pip install mpy-cross") + sys.exit(1) + + +def compile_file(src: Path, dst: Path, mpy_cross: str) -> bool: + """Compile a .py file to .mpy.""" + dst.parent.mkdir(parents=True, exist_ok=True) + + # Output file is .mpy + mpy_file = dst.with_suffix(".mpy") + + try: + result = subprocess.run( + [mpy_cross, MPY_OPT, "-o", str(mpy_file), str(src)], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f" ERROR: {src.name}: {result.stderr.strip()}") + return False + return True + except Exception as e: + print(f" ERROR: {src.name}: {e}") + return False + + +def compile_directory(src_dir: Path, dst_dir: Path, mpy_cross: str, name: str) -> int: + """Compile all .py files in a directory tree.""" + print(f"\nCompiling {name}...") + + count = 0 + errors = 0 + skipped = 0 + + for py_file in src_dir.rglob("*.py"): + # Skip __pycache__ and test files + if "__pycache__" in str(py_file) or "/tests/" in str(py_file): + continue + + # Calculate relative path and destination + rel_path = py_file.relative_to(src_dir) + + # Skip excluded files + if is_excluded(rel_path): + skipped += 1 + continue + + dst_file = dst_dir / rel_path + + if compile_file(py_file, dst_file, mpy_cross): + count += 1 + else: + errors += 1 + + status = f" Compiled {count} files" + if skipped: + status += f", skipped {skipped}" + if errors: + status += f" ({errors} errors)" + print(status) + return errors + + +def clean(): + """Remove existing dist/pico directory.""" + if DIST_PICO.exists(): + print(f"Cleaning {DIST_PICO}...") + shutil.rmtree(DIST_PICO) + + +def clean_source_mpy(): + """Remove .mpy files from source directories. + + mpy-cross sometimes leaves .mpy files in the source directories. + This cleans them up to avoid confusion. + """ + removed = 0 + for src_dir in [MOS6502_SRC, C64_SRC]: + for mpy_file in src_dir.rglob("*.mpy"): + if "__pycache__" not in str(mpy_file): + mpy_file.unlink() + removed += 1 + if removed: + print(f"Cleaned up {removed} .mpy files from source directories") + + +def main(): + print("=" * 60) + print("Building MicroPython distribution for Pico") + print("=" * 60) + + # Find mpy-cross + mpy_cross = find_mpy_cross() + print(f"Using: {mpy_cross}") + + # Check version + result = subprocess.run([mpy_cross, "--version"], capture_output=True, text=True) + print(f"Version: {result.stdout.strip()}") + + # Clean and create output directory + clean() + DIST_PICO.mkdir(parents=True, exist_ok=True) + + # Compile mos6502 + errors = compile_directory( + MOS6502_SRC, + DIST_PICO / "mos6502", + mpy_cross, + "mos6502" + ) + + # Compile c64 + errors += compile_directory( + C64_SRC, + DIST_PICO / "c64", + mpy_cross, + "c64" + ) + + # Clean up .mpy files from source directories + clean_source_mpy() + + # Summary + print("\n" + "=" * 60) + if errors: + print(f"Build completed with {errors} errors") + print(f"Output: {DIST_PICO}") + sys.exit(1) + else: + print("Build successful!") + print(f"Output: {DIST_PICO}") + + # Count files + mpy_count = len(list(DIST_PICO.rglob("*.mpy"))) + print(f"Total: {mpy_count} .mpy files") + + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/scripts/deploy_pico.py b/scripts/deploy_pico.py new file mode 100644 index 0000000..19bb0df --- /dev/null +++ b/scripts/deploy_pico.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""Deploy compiled .mpy files to a Pico via mpremote. + +Usage: + python scripts/deploy_pico.py + python scripts/deploy_pico.py --port /dev/ttyUSB0 + python scripts/deploy_pico.py --with-roms --rom-dir ./roms + poetry run deploy-pico +""" + +import argparse +import subprocess +import sys +from pathlib import Path + +# Project root +ROOT = Path(__file__).parent.parent + +# Distribution directory +DIST_PICO = ROOT / "dist" / "pico" + +# Main script for Pico +PICO_MAIN = ROOT / "scripts" / "pico_main.py" + +# Default ROM directory +DEFAULT_ROM_DIR = ROOT / "systems" / "roms" + + +def find_mpremote(): + """Check if mpremote is available.""" + import shutil + if not shutil.which("mpremote"): + print("ERROR: mpremote not found. Install with: pip install mpremote") + sys.exit(1) + return "mpremote" + + +def run_mpremote(*args, port=None): + """Run mpremote command.""" + cmd = ["mpremote"] + if port: + cmd.extend(["connect", port]) + cmd.extend(args) + + result = subprocess.run(cmd, capture_output=True, text=True) + return result.returncode == 0, result.stdout, result.stderr + + +def create_directories(port=None): + """Create /lib directory structure on Pico.""" + print("Creating directories...") + + dirs = [ + ":/lib", + ":/lib/mos6502", + ":/lib/c64", + ] + + # Find all subdirectories in dist/pico + for subdir in DIST_PICO.rglob("*"): + if subdir.is_dir(): + rel = subdir.relative_to(DIST_PICO) + dirs.append(f":/lib/{rel}") + + for d in sorted(set(dirs)): + run_mpremote("fs", "mkdir", d, port=port) + print(f" {d}") + + +def deploy_files(port=None): + """Copy all .mpy files to Pico.""" + print("\nDeploying files...") + + count = 0 + errors = 0 + + for mpy_file in sorted(DIST_PICO.rglob("*.mpy")): + rel_path = mpy_file.relative_to(DIST_PICO) + remote_path = f":/lib/{rel_path}" + + success, stdout, stderr = run_mpremote( + "fs", "cp", str(mpy_file), remote_path, port=port + ) + + if success: + print(f" {rel_path}") + count += 1 + else: + print(f" ERROR: {rel_path}: {stderr.strip()}") + errors += 1 + + return count, errors + + +def deploy_main(port=None): + """Deploy main.py to Pico.""" + print("\nDeploying main.py...") + + if not PICO_MAIN.exists(): + print(f" WARNING: {PICO_MAIN} not found, skipping main.py") + return False + + success, stdout, stderr = run_mpremote( + "fs", "cp", str(PICO_MAIN), ":/main.py", port=port + ) + + if success: + print(f" main.py deployed") + return True + else: + print(f" ERROR: Failed to deploy main.py: {stderr.strip()}") + return False + + +def deploy_roms(rom_dir: Path, port=None): + """Deploy ROM files to Pico. + + Args: + rom_dir: Directory containing ROM files (basic, kernal, char) + port: Serial port for mpremote + """ + print("\nDeploying ROMs...") + + if not rom_dir.exists(): + print(f" ERROR: ROM directory not found: {rom_dir}") + return False + + # Create /roms directory + run_mpremote("fs", "mkdir", ":/roms", port=port) + print(" Created /roms directory") + + # Expected ROM files (try multiple names for each) + rom_files = { + "basic": ["basic", "basic.rom", "basic.901226-01.bin"], + "kernal": ["kernal", "kernal.rom", "kernal.901227-03.bin"], + "char": ["char", "char.rom", "characters.901225-01.bin", "chargen"], + } + + deployed = 0 + for rom_type, names in rom_files.items(): + for name in names: + rom_path = rom_dir / name + if rom_path.exists(): + success, stdout, stderr = run_mpremote( + "fs", "cp", str(rom_path), f":/roms/{rom_type}", port=port + ) + if success: + print(f" {rom_type}: {name} -> /roms/{rom_type}") + deployed += 1 + break + else: + print(f" ERROR: Failed to deploy {name}: {stderr.strip()}") + else: + print(f" WARNING: No {rom_type} ROM found (tried: {', '.join(names)})") + + return deployed == len(rom_files) + + +def install_dependencies(port=None): + """Install MicroPython dependencies via mip.""" + print("\nInstalling dependencies...") + + dependencies = [ + "contextlib", # Required by mos6502.compat + ] + + for dep in dependencies: + print(f" Installing {dep}...") + success, stdout, stderr = run_mpremote("mip", "install", dep, port=port) + if not success: + print(f" WARNING: Failed to install {dep}: {stderr.strip()}") + else: + print(f" OK") + + +def main(): + parser = argparse.ArgumentParser(description="Deploy to Pico") + parser.add_argument( + "--port", "-p", + help="Serial port (e.g., /dev/ttyUSB0, /dev/cu.usbmodem101)" + ) + parser.add_argument( + "--clean", "-c", + action="store_true", + help="Remove existing /lib before deploying" + ) + parser.add_argument( + "--skip-deps", + action="store_true", + help="Skip installing MicroPython dependencies" + ) + parser.add_argument( + "--with-main", + action="store_true", + help="Deploy main.py (C64 emulator startup script)" + ) + parser.add_argument( + "--with-roms", + action="store_true", + help="Deploy ROM files (basic, kernal, char) from --rom-dir" + ) + parser.add_argument( + "--rom-dir", + type=Path, + default=DEFAULT_ROM_DIR, + help=f"Directory containing ROM files (default: {DEFAULT_ROM_DIR})" + ) + args = parser.parse_args() + + print("=" * 60) + print("Deploying to Pico") + print("=" * 60) + + # Check prerequisites + if not DIST_PICO.exists(): + print(f"ERROR: {DIST_PICO} not found. Run build-pico first.") + sys.exit(1) + + find_mpremote() + + # Clean if requested + if args.clean: + print("Cleaning /lib on Pico...") + run_mpremote("fs", "rm", "-r", ":/lib", port=args.port) + if args.with_roms: + print("Cleaning /roms on Pico...") + run_mpremote("fs", "rm", "-r", ":/roms", port=args.port) + + # Install dependencies (requires network on Pico, or will use mpremote's mip) + if not args.skip_deps: + install_dependencies(port=args.port) + + # Create directories + create_directories(port=args.port) + + # Deploy files + count, errors = deploy_files(port=args.port) + + # Deploy main.py if requested + if args.with_main: + deploy_main(port=args.port) + + # Deploy ROMs if requested + if args.with_roms: + deploy_roms(args.rom_dir, port=args.port) + + # Summary + print("\n" + "=" * 60) + if errors: + print(f"Deployed {count} files with {errors} errors") + sys.exit(1) + else: + print(f"Successfully deployed {count} files to Pico") + if args.with_main: + print(" + main.py (emulator startup script)") + if args.with_roms: + print(" + ROMs in /roms/") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/scripts/pico_config.py b/scripts/pico_config.py new file mode 100644 index 0000000..e0b01b0 --- /dev/null +++ b/scripts/pico_config.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Shared configuration for Pico build scripts. + +This module contains common settings used by both: +- build_pico.py (builds .mpy files for mpremote deployment) +- build_firmware.py (builds frozen MicroPython firmware) +""" + +from pathlib import Path + +# Project root +ROOT = Path(__file__).parent.parent + +# Source directories +MOS6502_SRC = ROOT / "mos6502" +C64_SRC = ROOT / "systems" / "c64" + +# Output directories +DIST_PICO = ROOT / "dist" / "pico" +DIST_FIRMWARE = ROOT / "dist" / "firmware" + +# Default ROM directory +ROM_DIR = ROOT / "systems" / "roms" + +# Main script for Pico +PICO_MAIN = ROOT / "scripts" / "pico_main.py" + +# mpy-cross optimization level +MPY_OPT = "-O3" # Strip docstrings, assertions, and line numbers + +# Pico 2 board variants +# For RISC-V, we use the same board but set PICO_PLATFORM +PICO2_BOARDS = { + "arm": {"board": "RPI_PICO2", "platform": None}, + "riscv": {"board": "RPI_PICO2", "platform": "rp2350-riscv"}, +} + +# Files/patterns to exclude from Pico build (keep in source, skip for deployment) +PICO_EXCLUDE = [ + # Threaded/multiprocess drive code - not needed on Pico (uses synchronous) + "drive/threaded_drive.py", + "drive/threaded_iec_bus.py", + "drive/multiprocess_drive.py", + "drive/multiprocess_iec_bus.py", + # Benchmark module - not useful on Pico + "benchmark.py", + # Unimplemented cartridge types - only needed for test cart generation + # Keep implemented: type_00, type_01, type_03, type_04, type_05, type_10, type_13, type_15, type_17, type_19 + "cartridges/type_02_", + "cartridges/type_06_", + "cartridges/type_07_", + "cartridges/type_08_", + "cartridges/type_09_", + "cartridges/type_11_", + "cartridges/type_12_", + "cartridges/type_14_", + "cartridges/type_16_", + "cartridges/type_18_", + "cartridges/type_20_", + "cartridges/type_21_", + "cartridges/type_22_", + "cartridges/type_23_", + "cartridges/type_24_", + "cartridges/type_25_", + "cartridges/type_26_", + "cartridges/type_27_", + "cartridges/type_28_", + "cartridges/type_29_", + "cartridges/type_30_", + "cartridges/type_31_", + "cartridges/type_32_", + "cartridges/type_33_", + "cartridges/type_34_", + "cartridges/type_35_", + "cartridges/type_36_", + "cartridges/type_37_", + "cartridges/type_38_", + "cartridges/type_39_", + "cartridges/type_40_", + "cartridges/type_41_", + "cartridges/type_42_", + "cartridges/type_43_", + "cartridges/type_44_", + "cartridges/type_45_", + "cartridges/type_46_", + "cartridges/type_47_", + "cartridges/type_48_", + "cartridges/type_49_", + "cartridges/type_50_", + "cartridges/type_51_", + "cartridges/type_52_", + "cartridges/type_53_", + "cartridges/type_54_", + "cartridges/type_55_", + "cartridges/type_56_", + "cartridges/type_57_", + "cartridges/type_58_", + "cartridges/type_59_", + "cartridges/type_60_", + "cartridges/type_61_", + "cartridges/type_62_", + "cartridges/type_63_", + "cartridges/type_64_", + "cartridges/type_65_", + "cartridges/type_66_", + "cartridges/type_67_", + "cartridges/type_68_", + "cartridges/type_69_", + "cartridges/type_70_", + "cartridges/type_71_", + "cartridges/type_72_", + "cartridges/type_73_", + "cartridges/type_74_", + "cartridges/type_75_", + "cartridges/type_76_", + "cartridges/type_77_", + "cartridges/type_78_", + "cartridges/type_79_", + "cartridges/type_80_", + "cartridges/type_81_", + "cartridges/type_82_", + "cartridges/type_83_", + "cartridges/type_84_", + "cartridges/type_85_", + # Also exclude rom_builder - only needed for test cart generation + "cartridges/rom_builder.py", + # Exclude drive module entirely for Pico - saves 28K and memory + "drive/", +] + + +def is_excluded(rel_path: Path) -> bool: + """Check if a file should be excluded from Pico build. + + Arguments: + rel_path: Path relative to the source directory + + Returns: + True if the file should be excluded + """ + rel_str = str(rel_path) + for pattern in PICO_EXCLUDE: + if rel_str.endswith(pattern) or pattern in rel_str: + return True + return False diff --git a/scripts/pico_main.py b/scripts/pico_main.py new file mode 100644 index 0000000..740732d --- /dev/null +++ b/scripts/pico_main.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +"""Pico main script for running C64 emulator. + +This script is designed to run on a Raspberry Pi Pico 2 with MicroPython. +It displays the C64 screen over serial (USB) and accepts keyboard input +from the serial console. + +For best results, use frozen firmware (build_firmware.py) which doesn't +consume heap RAM for bytecode. If using mpremote deployment, this script +uses gc.threshold() to reduce heap fragmentation. + +Deploy this file as main.py on the Pico along with the ROMs in /roms/. +""" + +import gc + +# CRITICAL: Set aggressive GC threshold BEFORE any imports to prevent +# heap fragmentation. Without this, imports fragment the heap so badly +# that we can't allocate the 64KB RAM buffer later. +gc.threshold(4096) +gc.collect() + +import sys +import time + +# Compatibility: time.ticks_ms() is MicroPython-specific +try: + _ticks_ms = time.ticks_ms +except AttributeError: + # CPython fallback + def _ticks_ms(): + return int(time.time() * 1000) + +# Import the C64 emulator directly (avoid heavy c64/__init__.py) +gc.collect() +from c64.c64 import C64 +gc.collect() +from mos6502 import errors +gc.collect() + +# PETSCII to ASCII conversion table for screen output +# Screen codes are different from PETSCII keyboard codes +SCREEN_TO_ASCII = { + # @ A-Z (0-26) + 0: '@', 1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E', 6: 'F', 7: 'G', + 8: 'H', 9: 'I', 10: 'J', 11: 'K', 12: 'L', 13: 'M', 14: 'N', 15: 'O', + 16: 'P', 17: 'Q', 18: 'R', 19: 'S', 20: 'T', 21: 'U', 22: 'V', 23: 'W', + 24: 'X', 25: 'Y', 26: 'Z', + # Special chars (27-31) + 27: '[', 28: '\\', 29: ']', 30: '^', 31: '_', + # Space and symbols (32-63) + 32: ' ', 33: '!', 34: '"', 35: '#', 36: '$', 37: '%', 38: '&', 39: "'", + 40: '(', 41: ')', 42: '*', 43: '+', 44: ',', 45: '-', 46: '.', 47: '/', + 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', + 56: '8', 57: '9', 58: ':', 59: ';', 60: '<', 61: '=', 62: '>', 63: '?', +} + +# ASCII to C64 keyboard matrix mapping: char -> (row, col) +# Reference: https://www.c64-wiki.com/wiki/Keyboard +ASCII_TO_KEY = { + # Letters (directly mapped) + 'a': (1, 2), 'b': (3, 4), 'c': (2, 4), 'd': (2, 2), 'e': (1, 6), + 'f': (2, 5), 'g': (3, 2), 'h': (3, 5), 'i': (4, 1), 'j': (4, 2), + 'k': (4, 5), 'l': (5, 2), 'm': (4, 4), 'n': (4, 7), 'o': (4, 6), + 'p': (5, 1), 'q': (7, 6), 'r': (2, 1), 's': (1, 5), 't': (2, 6), + 'u': (3, 6), 'v': (3, 7), 'w': (1, 1), 'x': (2, 7), 'y': (3, 1), + 'z': (1, 4), + # Numbers + '0': (4, 3), '1': (7, 0), '2': (7, 3), '3': (1, 0), '4': (1, 3), + '5': (2, 0), '6': (2, 3), '7': (3, 0), '8': (3, 3), '9': (4, 0), + # Symbols + ' ': (7, 4), # SPACE + '\r': (0, 1), '\n': (0, 1), # RETURN + ',': (5, 7), '.': (5, 4), '/': (6, 7), ';': (6, 2), ':': (5, 5), + '=': (6, 5), '+': (5, 0), '-': (5, 3), '*': (6, 1), '@': (5, 6), + # Special + '\x7f': (0, 0), # Backspace -> DEL + '\x08': (0, 0), # Backspace -> DEL +} + + +def petscii_to_ascii(code: int) -> str: + """Convert a PETSCII screen code to ASCII character.""" + # Handle reversed characters (codes 128-255) - just use the base character + if code >= 128: + code -= 128 + return SCREEN_TO_ASCII.get(code, '?') + + +def render_screen(c64) -> None: + """Render the C64 screen to serial output.""" + screen_start = 0x0400 + cols = 40 + rows = 25 + + # Clear screen and move cursor to top (ANSI escape codes) + sys.stdout.write("\033[2J\033[H") + + # Header + print("=" * 42) + print(" C64 PICO EMULATOR") + print("=" * 42) + + # Render screen RAM + for row in range(rows): + line = "" + for col in range(cols): + addr = screen_start + (row * cols) + col + code = int(c64.cpu.ram[addr]) + line += petscii_to_ascii(code) + print(line) + + print("=" * 42) + print(f"Cycles: {c64.cpu.cycles_executed:,}") + + +def check_keyboard(c64) -> bool: + """Check for keyboard input from serial console. + + Returns: + True if Ctrl+C was pressed (exit), False otherwise. + """ + try: + # MicroPython: use select.poll() for non-blocking I/O + import select + poll = select.poll() + poll.register(sys.stdin, select.POLLIN) + + # Check if input is available (timeout=0 for non-blocking) + events = poll.poll(0) + if events: + char = sys.stdin.read(1) + if char: + return handle_key(c64, char) + poll.unregister(sys.stdin) + except (ImportError, OSError): + # Fallback: try to read without blocking (may not work on all platforms) + pass + + return False + + +def handle_key(c64, char: str) -> bool: + """Handle a keyboard character. + + Returns: + True if should exit (Ctrl+C), False otherwise. + """ + # Ctrl+C - exit + if char == '\x03': + return True + + # Convert to uppercase for C64 (C64 keyboard is uppercase by default) + if 'a' <= char <= 'z': + char_upper = char.upper() + key = ASCII_TO_KEY.get(char) + elif 'A' <= char <= 'Z': + # Shift + letter + char_lower = char.lower() + key = ASCII_TO_KEY.get(char_lower) + if key: + # Press shift first + c64.cia1.press_key(1, 7) # Left SHIFT + else: + key = ASCII_TO_KEY.get(char) + + if key: + row, col = key + c64.cia1.press_key(row, col) + # Schedule key release after a short delay (will be handled in main loop) + # For simplicity, release immediately after some CPU cycles + return False + + return False + + +def release_all_keys(c64) -> None: + """Release all pressed keys.""" + # Reset the entire keyboard matrix + for row in range(8): + for col in range(8): + c64.cia1.release_key(row, col) + + +def main(): + """Main entry point for Pico C64 emulator.""" + print("Initializing C64 emulator...") + gc.collect() + print(f"Free memory: {gc.mem_free():,} bytes") + + # Pre-allocate 64KB RAM buffer BEFORE creating C64 + # This ensures we get a contiguous block before heap fragmentation + # from C64 initialization makes it impossible + print("Pre-allocating 64KB RAM buffer...") + try: + ram_buffer = bytearray(131072) + except MemoryError: + print("ERROR: Not enough memory to allocate 64KB RAM buffer") + print("Try using frozen firmware (build_firmware.py) instead") + return + + gc.collect() + print(f"After RAM alloc: {gc.mem_free():,} bytes") + + # ROMs: First tries embedded ROMs (baked into firmware by build_firmware.py) + # Falls back to /roms/ filesystem if embedded ROMs not available + print("Initializing C64 (embedded ROMs preferred, fallback: /roms/ filesystem)") + + # Create C64 instance with headless display (no pygame) + # Use positional args - MicroPython frozen modules may not support kwargs + try: + # C64(rom_dir, display_mode, scale, enable_irq, video_chip, cpu_variant, verbose_cycles, preallocated_ram) + c64 = C64( + "/roms", # rom_dir + "headless", # display_mode + 1, # scale + True, # enable_irq + "6569", # video_chip + "6502", # cpu_variant + False, # verbose_cycles + ram_buffer # preallocated_ram + ) + #c64 = C64(rom_dir="/roms", display_mode="headless", scale=1, enable_irq=True, video_chip="6569", cpu_variant="6502", verbose_cycles=False, preallocated_ram=ram_buffer) + except OSError as e: + # MicroPython uses OSError for file not found (no FileNotFoundError) + print(f"ERROR: {e}") + print("Please copy ROM files (basic, kernal, char) to /roms/ on the Pico") + return + except MemoryError as e: + print(f"ERROR: Out of memory during C64 init: {e}") + print("Try using frozen firmware (build_firmware.py) instead") + return + + gc.collect() + print(f"C64 initialized. Free memory: {gc.mem_free():,} bytes") + print("Starting emulation... Press Ctrl+C to exit") + print() + + # Main emulation loop + cycles_per_iteration = 20000 # ~20ms worth of cycles at 1MHz + render_interval = 0.1 # Render every 100ms (10 FPS for serial) + last_render = 0 + key_release_countdown = 0 + + try: + while True: + # Execute CPU cycles + try: + c64.cpu.execute(cycles=cycles_per_iteration) + #c64.cpu.execute(cycles=cycles_per_iteration) + pass + except errors.CPUCycleExhaustionError: + pass # Normal - cycle budget exhausted + + # Check for keyboard input + if check_keyboard(c64): + break # Ctrl+C pressed + + # Handle key release after some cycles + if key_release_countdown > 0: + key_release_countdown -= 1 + if key_release_countdown == 0: + release_all_keys(c64) + else: + # If a key was just pressed, schedule release + key_release_countdown = 5 # Release after 5 iterations (~100ms) + + # Render screen periodically + now = _ticks_ms() / 1000.0 + if now - last_render >= render_interval: + render_screen(c64) + last_render = now + + # Garbage collect periodically to avoid memory fragmentation + gc.collect() + + except KeyboardInterrupt: + pass + + print("\nEmulation stopped.") + print(f"Total cycles: {c64.cpu.cycles_executed:,}") + gc.collect() + print(f"Free memory: {gc.mem_free():,} bytes") + + +if __name__ == "__main__": + main() diff --git a/systems/C64_README.md b/systems/C64_README.md new file mode 100644 index 0000000..0cb928a --- /dev/null +++ b/systems/C64_README.md @@ -0,0 +1,109 @@ +# C64 Emulator Architecture + +## Display Synchronization + +The emulator uses a non-blocking frame synchronization model between the CPU/VIC emulation and the display renderer (pygame). + +### Two Concurrent Threads + +1. **CPU Thread** - Runs the 6502 CPU and VIC-II at full speed +2. **Pygame Thread** - Renders frames to the display when it can + +### Frame Cycle + +``` +CPU Thread Pygame Thread +────────── ───────────── + │ │ + ▼ │ +[Execute instructions] │ + │ │ + ▼ │ +[VIC updates raster position] │ + │ │ + ▼ │ +[Raster wraps to 0?] │ + │ │ + YES │ + │ │ + ▼ │ +[Set frame_complete = True] ─────────► [Check: frame_complete?] + │ │ + │ (keeps running) YES + │ │ + ▼ ▼ +[More instructions...] [Snapshot RAM] + │ │ + │ ▼ + │ [Clear frame_complete] + │ │ + │ ▼ + │ [Render from snapshot] + │ │ + ▼ ▼ + (loop) (loop) +``` + +### Key Design Points + +1. **VIC signals "frame done"** - Sets `frame_complete` when the raster beam wraps from the last line (311 for PAL, 262/263 for NTSC) back to line 0. This is the vertical blank (VBlank) period. + +2. **Pygame grabs what it can** - Checks the flag, takes a quick RAM snapshot, clears the flag, then renders at its leisure from the snapshot. + +3. **No blocking** - CPU never waits for pygame, pygame never waits for CPU. They communicate through a single Event flag. + +4. **Snapshot for consistency** - `bytes(self.cpu.ram)` creates a copy of RAM so pygame renders a frozen moment in time, not RAM that's changing mid-render. This prevents visual tearing and partial updates. + +### Synchronization Primitive Choice + +The `frame_complete` flag uses `multiprocessing.Event()` rather than `threading.Event()` or a plain boolean: + +| Primitive | Cross-Thread | Cross-Process | Notes | +|-----------|--------------|---------------|-------| +| `bool` | No | No | Not memory-safe across threads | +| `threading.Event` | Yes | No | Only works within single process | +| `multiprocessing.Event` | Yes | Yes | Works everywhere, slight overhead | + +We use `multiprocessing.Event` because: +- **Thread visibility**: Plain booleans may not be visible across threads due to CPU caching and memory barriers +- **Future-proofing**: If we later move CPU execution to a separate process for true parallelism (bypassing Python's GIL), the synchronization will still work +- **Consistency**: One primitive that works in all scenarios + +The thread/process coordination (cpu_done, stop_display) uses `threading.Event` and `threading.Thread` because: +- They involve closures that capture `self` and local variables +- `multiprocessing.Process` requires pickling the target function and its closure, which fails for nested functions +- Threading is sufficient here since these are for coordination, not CPU-bound work + +### Performance Characteristics + +- If pygame is slow, it may miss frames - the emulation continues at full speed and pygame renders the next frame it catches +- If pygame is fast, it renders every frame +- Display may drop below 50/60 fps if rendering is slow, but emulation timing remains accurate +- Whatever pygame renders is always a consistent snapshot taken at or near VBlank + +## Video Timing + +The VIC-II chip has three variants with different timing: + +| Chip | Region | Raster Lines | Cycles/Line | Cycles/Frame | Refresh | +|------|--------|--------------|-------------|--------------|---------| +| 6569 | PAL | 312 | 63 | 19,656 | ~50 Hz | +| 6567R8 | NTSC | 263 | 65 | 17,095 | ~60 Hz | +| 6567R56A | NTSC (old) | 262 | 64 | 16,768 | ~60 Hz | + +## Cartridge Support + +### Supported Types + +- **Type 0: Normal Cartridge** - 8KB, 16KB, and Ultimax modes +- **Type 1: Action Replay** - 32KB banked cartridge with RAM + +### Cartridge Auto-Detection + +For raw `.bin` files, the loader auto-detects the cartridge type: + +1. **8KB with CBM80 signature** at offset 4 → Standard 8K cartridge at $8000 +2. **8KB with reset vector in $E000-$FFFF** → Ultimax cartridge at $E000 +3. **16KB file** → 16K cartridge at $8000-$BFFF + +CRT files contain header metadata specifying the exact hardware type and memory configuration. diff --git a/systems/CHECK64_v1.6_20220918_f64/1_ultimax_ram_checker.bin b/systems/CHECK64_v1.6_20220918_f64/1_ultimax_ram_checker.bin new file mode 100755 index 0000000..4e621d9 Binary files /dev/null and b/systems/CHECK64_v1.6_20220918_f64/1_ultimax_ram_checker.bin differ diff --git a/systems/CHECK64_v1.6_20220918_f64/2_dead_test_kinzi_006.bin b/systems/CHECK64_v1.6_20220918_f64/2_dead_test_kinzi_006.bin new file mode 100755 index 0000000..a7d18bc Binary files /dev/null and b/systems/CHECK64_v1.6_20220918_f64/2_dead_test_kinzi_006.bin differ diff --git a/systems/CHECK64_v1.6_20220918_f64/2_dead_test_kinzi_006.txt b/systems/CHECK64_v1.6_20220918_f64/2_dead_test_kinzi_006.txt new file mode 100755 index 0000000..57faace --- /dev/null +++ b/systems/CHECK64_v1.6_20220918_f64/2_dead_test_kinzi_006.txt @@ -0,0 +1,200 @@ +C-64 Dead Test Rev 781220 kinzi 006 +=================================== + +(Deutsche Version nachstehend / German version below.) + +I disassembled the original C-64 DEAD TEST REV 781220 using "WFDis" +(https://www.white-flame.com/wfdis/), commented the code (not yet fully +finished), relocated constants, strings, the charset etc., changed it to ACME +standard format and "improved" (I hope :-) a few things. + +I am neither an ACME expert nor a divine coder. Many things might be done in +more beauty or ellegance, since this is my first usage of ACME (or an +assembler at all, because I used only monitors for coding assembler in my +former life. :-) So please have mercy with me ... + +* Border color is now changed to light gray on second opcode after initial SEI + and thus as early as possible. If the CPU is able to read the reset vector + but crashes after a few opcodes the border (screen) stays light gray. + (Was black in original version = the VIC power-up default.) + +* Border color changes to cyan in initial RAM test. (Stayed black in original + version.) + +* Character set in original version is hard to read, especially digits. + Changed to standard C-64 character set. + +* Sound test now plays four wave forms including noise and "in right order" - + triangle, sawtooth, square, noise. + +* "BAD" on tests led to infinitive loop already in original verion. This is + now announced by cycling through the border colors in this loop, as done + in Easy Flash 3 when unable to reset. + +* The screen layout has been expanded slightly, data bit numbers were added + and also the IC identifiers for the 250466 and 250469 boards to ease + things when repairing. :-) "OK" results for tests are now green to clarify + things. + +* Two low-current LEDs can be connected to TAPE Port on lines "MOTOR" and + "SENSE" to show activity even on systems with no screen output. They should + be connected to ground via a 1,8 kOhm or greater resistor to protect to CPU + port. The LEDs will flip their status every time the TOD clock display is + updated (every 20 seconds or so) and will blink together with the screen + if RAM error are "blinked" out during initial RAM test. + + Check64 V2.0 from GMP@Forum64 is planned to incorporate these two LEDs. + + In former versions I proposed 1 kOhm resistors but that seems a too small + value right now. CPU die shots show that the CPU port is built very similar + to the 6522 outputs on Port A (push-pull), so I'd presume the outputs can + source/sink a similar amount of current safely resp. sinking max. 1.6 mA. + +Use the source code for whatever you want (it isn't even mine in large parts) +but be fair and give credits ;-) ... + +News in 003: + +* Border color is decremented during initial RAM test, providing a primitive + way of "progess status". + +* Bug fix: ram error blinking sequence was broken, always blinked just one + time not corresponding to regarding bit. + +News in 004: + +* Bug fix: LEDs were not blinking any more during tests because of a typo. + +* The LEDs flip their status now even during initial RAM test, just when the + border color is decremented. They also change their status after Zero Page + and Stack test. + +News in 005: + +* Bug fix: Color RAM was always tested with pattern "00" only. Fixed. + +News in 006: + +* It seems 28C64 EEPROMs sometimes lead to problems with Dead Test 005. No + idea why, couldn't reproduce it here. If you encounter troubles during + Initial RAM Test try using a 27(C)64 EPROM instead. + +* Bug fix: CIAs' real time clocks were not properly initialized, leading to + a wrong startup time (11-00-00 instead of 00-00-00). Bug was introduced + with 001. :-) + +* Bug fix: No Interrupt Service Routine (ISR) was defined before, the IRQ + vector pointed to $F000. Could lead to problems if IRQ is clamped to GND. + An ISR containing a simple "RTI" is now provided. + +* Changed initial color change method from DEC to STX in (rare) case of wrong + "read modify write" operations happening with a bad CPU. + + +------------ + +Ich habe das originale C-64 DEAD TEST REV 781220 mit dem "WFDis" Disassemler +(https://www.white-flame.com/wfdis/), den Code weitgehend kommentiert +sowie einige Dinge umgestellt (Strings, Tabellen, Zeichensatz usw.), ans +ACME-Format angepasst und (hoffentlich :-) einige Dinge verbessert. + +Ich bin weder ein ACME-Experte noch ein begnadeter Programmierer, daher wird +vermutlich vieles schöner oder einfacher zu lösen sein, auch weil das mein +erstes "Projekt" ist, für das ich ACME verwendet habe; bzw. überhaupt einen +Assembler, denn bisher habe ich Assembler immer nur im Monitor gecodet. :-) +Ich bitte also um Nachsicht. :-) + +* Die Rahmenfarbe wird beim zweiten Befehl (nach dem initialen SEI) auf + hellgrau gestellt, also so früh wie möglich. Wenn die CPU den Reset-Vektor + lesen kann und startet, dann aber nach ein paar Befehlen crasht bleibt der + Rahmen (und damit der Hintergrund) hellgrau. In der Originalversion ist er + zu diesem Zeitpunkt schwarz - das ist der Standardwert des VIC beim + Einschalten. + +* Die Rahmenfarbe wechselt beim initialen RAM-Test auf türkis. (In der + originalen Version bliebt der Bildschirm hier ebenfalls schwarz). + +* Der Zeichensatz des Originals ist schlecht lesbar, vor allem die Ziffern. + Ich habe den originalen C-64-Zeichensatz eingebaut. + +* Der Sound-Test spielt nun alle vier Wellenformen inkl. "Rauschen", und zwar + in der "richtigen" Reihenfolge (Dreicke, Sägezahn, Rechteck, Rauschen). + +* Trifft der RAM-Test auf Fehler und wirft BAD neben den IC-Bezeichnungen + aus, wird in eine Endlosschleife gesprungen, wie auch bereits bei der + Original-Version. Jetzt wird in dieser Endlosschleife die Rahmenfarbe + durchgerollt, wie es das Easy Flash 3 auch macht, wenn es keinen Reset + auslösen konnte. + +* Der Bildschirmaufbau wurde etwas überarbeitet, es werden nun die + Bezeichnungen der Datenleitungen neben den IC-Namen angeführt, ebenso wie + die (abweichenden) IC-Nummern der 250466 und 250469; dies erleichtert + Reparaturen. :-) Bei Tests mit Ergebnis "OK" erscheint dieses nun in grün. + +* Am TAPE-Port können zwei Low-Current-LEDs, jeweils an "MOTOR" und "SENSE" + angeschlossen werden, über jeweils einen Widerstand von mindestens 1,8 kOhm + zum Schutz des I/O-Ports der CPU. Die LEDs wechseln ihren Status bei jedem + Update der Uhren-Anzeige rechts unten, also ca. alle 20 Sekunden. + Außerdem blinken sie mit dem Bildschirm mit, wenn beim initialen RAM-Test + ein Fehler festgestellt wird. Das ist vor allem auf Rechnern mit kaputter + Bildschirmausgabe interessant. + + In einer früheren Anleitung ahbe ich 1 kOhm-Vorwiderstände vorgeschlagen, + was sich jetzt als vermutlich zu klein herausstellt. Die-Shots der CPU + zeigen, dass die CPU-Port-Ausgänge sehr ähnlich den "6522-Port-A"-Ausgängen + aufgebaut sind und daher wohl ähnliche Ströme liefern können, das wären + ca. 1,6 mA. + + Das Check64 V2.0 von GMP aus dem Forum64 wird diese LEDs voraussichtlich + bereits mitbringen. + +Macht mit dem Source-Code, was ihr wollt (ist großteils eh nicht meiner :-), +aber schmückt euch nicht damit und "gebt credits". ;-) + +Neues in 003: + +* Die Rahmenfarbe wird während des initialen RAM Tests nun bei jedem neuen + Testpattern dekrementiert und stellt daher eine einfache Verlaufskontrolle + dar. + +* Bug fix: Das "RAM-Fehlerblinken" war kaputt, es hat nur noch einmal + geblinkt, unabhängig vom betroffenen Bit. + +Neues in 004: + +* Bug fix: Die LEDs hatten nicht mehr geblinkt während der Tests auf Grund + eines Tippfehlers. + +* Die LEDs ändern ihren Status nun auch während des initialen RAM-Tests, + gemeinsam mit der Rahmenfarbe. Außerdem werden sie auch nach dem Zero Page + und dem Stack Test umgeschaltet. + +Neues in 005: + +* Bug fix: Das Color RAM wurde immer nur mit dem Test-Pattern "00" getestet. + Behoben. + +Neues in 006: + +* Offenbar führt die Verwendung von 28C64 EEPROMs manchmal zu Problemen mit + Dead Test 005 - keine Ahnung, wieso; ich konnte das nicht nachstellen. + Falls es damit Probleme geben sollte, bitte stattdessen ein 27(C)64 EPROM + verwenden. + +* Bug fix: Die Echtzeituhren in den CIAs wurden nicht richtig initialisiert, + was zu einer falschen Uhrzeitanzeige beim Start führte (11-00-00 statt + 00-00-00). Der Bug wurde mit 001 "eingeführt". :-) + +* Bug fix: Bisher war keine Interrupt Service Routine (ISR) definiert, der + IRQ-Vektor zeigte auf $F000. Das konnte zu Problemen führen, wenn die IRQ- + Leitung durch einen Fehler auf GND klemmt. Es existiert nun eine ISR + bestehend aus einem einfachen "RTI". + +* Die Methode, um ganz am Anfang die Rahmenfarbe zu ändern, wurde von einem + DEC auf ein STX geändert - für den (seltenen) Fall, dass die CPU Probleme + mit "Read Modify Write"-Operationen hätte. + +------------ + +kinzi, 2022-09-18 + diff --git a/systems/CHECK64_v1.6_20220918_f64/3_586220ted_kinzi_4.bin b/systems/CHECK64_v1.6_20220918_f64/3_586220ted_kinzi_4.bin new file mode 100755 index 0000000..cef3a10 Binary files /dev/null and b/systems/CHECK64_v1.6_20220918_f64/3_586220ted_kinzi_4.bin differ diff --git a/systems/CHECK64_v1.6_20220918_f64/3_586220ted_kinzi_4.txt b/systems/CHECK64_v1.6_20220918_f64/3_586220ted_kinzi_4.txt new file mode 100644 index 0000000..34422f7 --- /dev/null +++ b/systems/CHECK64_v1.6_20220918_f64/3_586220ted_kinzi_4.txt @@ -0,0 +1,11 @@ +BASE: diag_586220ted +MOD: 3 byte patch to achieve compatibility with Retrofan's font + +BASE: diag_586220ted_kinzi +MOD: 6 byte patch to swap "U1 BAD" and "U2 BAD" in INTERRUPT TEST (obviously 35++ years old bug) + +BASE: diag_586220ted_kinzi_2 +MOD: 1 byte patch to correct not printing "INTERRUPT BAD" on interrupt test @94C0: replace RTS(60) by NOP(EA) + +BASE: diag_586220ted_kinzi_3 +MOD: Changed identification string to "C-64 DIAG 586220 KINZI 004" \ No newline at end of file diff --git a/systems/CHECK64_v1.6_20220918_f64/4_789210_c128_diag.bin b/systems/CHECK64_v1.6_20220918_f64/4_789210_c128_diag.bin new file mode 100755 index 0000000..c4c1453 Binary files /dev/null and b/systems/CHECK64_v1.6_20220918_f64/4_789210_c128_diag.bin differ diff --git a/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.bin b/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.bin new file mode 100755 index 0000000..4b25fb9 Binary files /dev/null and b/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.bin differ diff --git a/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.jpg b/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.jpg new file mode 100644 index 0000000..a2db766 Binary files /dev/null and b/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.jpg differ diff --git a/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.txt b/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.txt new file mode 100644 index 0000000..40f681d --- /dev/null +++ b/systems/CHECK64_v1.6_20220918_f64/CHECK64_v1.6_20220919.txt @@ -0,0 +1,6 @@ +Schalterstellungen + +1. 101011 ULTIMAX-RAM-CHECKER by GI-Joe / Mac Bacon +2. 101001 C-64 Dead Test Rev 781220 kinzi 006 +3. 010110 C-64 Diag Rev 586220 kinzi 004 +4. 000100 C128 Diagnostic Rev. 789010 diff --git a/systems/Commodore_Zif_Diagnostic_Cart_Case_4085017.zip b/systems/Commodore_Zif_Diagnostic_Cart_Case_4085017.zip new file mode 100644 index 0000000..60ca0be Binary files /dev/null and b/systems/Commodore_Zif_Diagnostic_Cart_Case_4085017.zip differ diff --git a/systems/Diag_DeadTest/C64_Dead_Test_Diagnostic_Manual_(1988-Jan).pdf b/systems/Diag_DeadTest/C64_Dead_Test_Diagnostic_Manual_(1988-Jan).pdf new file mode 100644 index 0000000..fad16b5 Binary files /dev/null and b/systems/Diag_DeadTest/C64_Dead_Test_Diagnostic_Manual_(1988-Jan).pdf differ diff --git a/systems/Diag_DeadTest/dead test.BIN b/systems/Diag_DeadTest/dead test.BIN new file mode 100644 index 0000000..665df75 Binary files /dev/null and b/systems/Diag_DeadTest/dead test.BIN differ diff --git a/systems/Diag_DeadTest/dead test.CRT b/systems/Diag_DeadTest/dead test.CRT new file mode 100644 index 0000000..15bce0a Binary files /dev/null and b/systems/Diag_DeadTest/dead test.CRT differ diff --git a/systems/Diag_DeadTest/dead test.png b/systems/Diag_DeadTest/dead test.png new file mode 100644 index 0000000..92e94d0 Binary files /dev/null and b/systems/Diag_DeadTest/dead test.png differ diff --git a/systems/LICENSE.txt b/systems/LICENSE.txt new file mode 100644 index 0000000..06ec344 --- /dev/null +++ b/systems/LICENSE.txt @@ -0,0 +1 @@ +This thing was created by Thingiverse user {netsurge %!s(bool=true)}, and is licensed under cc-nc. \ No newline at end of file diff --git a/systems/README.txt b/systems/README.txt new file mode 100644 index 0000000..1e952bb --- /dev/null +++ b/systems/README.txt @@ -0,0 +1 @@ +{Commodore Zif Diagnostic Cart Case %!s(bool=true)} by {netsurge %!s(bool=true)} on Thingiverse: https://www.thingiverse.com/thing:4085017 \ No newline at end of file diff --git a/systems/c64/CLAUDE.md b/systems/c64/CLAUDE.md new file mode 100644 index 0000000..2babffa --- /dev/null +++ b/systems/c64/CLAUDE.md @@ -0,0 +1,2 @@ +- Make sure you copy the packages to dist as-is before you byte compile and remove files. NEVER remove files in the source repos without explicit permission. +- Don't ever restore files from git without explicit permission. \ No newline at end of file diff --git a/systems/c64/__init__.py b/systems/c64/__init__.py index 82ec452..a6031d3 100755 --- a/systems/c64/__init__.py +++ b/systems/c64/__init__.py @@ -1,15 +1,33 @@ -#!/usr/bin/env python3 -"""Commodore 64 Emulator using the mos6502 CPU package.""" - -import logging -import sys -from pathlib import Path -from typing import Optional - -from mos6502 import CPU, CPUVariant, errors, add_cpu_arguments -from mos6502.core import INFINITE_CYCLES -from mos6502.memory import Byte, Word +"""Commodore 64 Emulator using the mos6502 CPU package. + +This module provides a complete C64 emulator including: +- 6510 CPU emulation (via mos6502 package) +- VIC-II video chip emulation +- SID sound chip (stub) +- CIA1/CIA2 I/O chips +- Keyboard matrix +- 1541 disk drive emulation +- Cartridge support (multiple types) +""" + +# Re-export everything from c64.py +from c64.c64 import ( + # Main class + C64, + # Logging + log, + # Debug flags + DEBUG_CIA, + DEBUG_VIC, + DEBUG_JIFFY, + DEBUG_KEYBOARD, + DEBUG_SCREEN, + DEBUG_CURSOR, + DEBUG_KERNAL, + DEBUG_BASIC, +) +# Re-export commonly used items from submodules for convenience from c64.cartridges import ( Cartridge, CartridgeTestResults, @@ -77,4693 +95,104 @@ CIA2_END, BASIC_PROGRAM_START, ) -from c64.drive import ( - Drive1541, - IECBus, - D64Image, - ThreadedDrive1541, - ThreadedIECBus, - MultiprocessDrive1541, - MultiprocessIECBus, - SharedIECState, -) - -logging.basicConfig(level=logging.CRITICAL) -log = logging.getLogger("c64") - -# Debug flags - set to True to enable verbose logging -DEBUG_CIA = False # CIA register reads/writes -DEBUG_VIC = False # VIC register operations -DEBUG_JIFFY = False # Jiffy clock updates -DEBUG_KEYBOARD = False # Keyboard events -DEBUG_SCREEN = False # Screen memory writes -DEBUG_CURSOR = False # Cursor position variables -DEBUG_KERNAL = False # Enable CPU logging when entering KERNAL ROM -DEBUG_BASIC = False # Enable CPU logging when entering BASIC ROM - -class C64: - """Commodore 64 Emulator. - - Memory Map: - $0000-$0001: I/O Ports (6510 specific - stubbed) - $0002-$9FFF: RAM (40KB usable) - $A000-$BFFF: BASIC ROM (8KB) - $C000-$CFFF: RAM (4KB) - $D000-$DFFF: I/O and Color RAM (4KB - stubbed) - $E000-$FFFF: KERNAL ROM (8KB) - """ - - # BASIC memory pointers (zero page) - # These must be updated when loading a BASIC program for RUN to work - TXTTAB = 0x2B # Start of BASIC program text (2 bytes, little-endian) - VARTAB = 0x2D # Start of BASIC variables / end of program (2 bytes) - ARYTAB = 0x2F # Start of BASIC arrays (2 bytes) - STREND = 0x31 # End of BASIC arrays / bottom of strings (2 bytes) - - # KERNAL keyboard buffer (for injecting typed commands) - KEYBOARD_BUFFER = 0x0277 # 10-byte keyboard buffer - KEYBOARD_BUFFER_SIZE = 0x00C6 # Number of characters in buffer - - # Reset vector location - RESET_VECTOR_ADDR = 0xFFFC - - # Cartridge memory regions - ROML_START = 0x8000 # Low ROM (8KB) - active when EXROM=0 - ROML_END = 0x9FFF - ROML_SIZE = 0x2000 # 8KB - - ROMH_START = 0xA000 # High ROM (8KB) - overlaps BASIC ROM area - ROMH_END = 0xBFFF - ROMH_SIZE = 0x2000 # 8KB - - # Cartridge auto-start signature location - CART_SIGNATURE_ADDR = 0x8004 # "CBM80" signature for auto-start - - # CRT hardware type names (from VICE specification) - # Type 0 is the only one we currently support - # Source: http://rr.c64.org/wiki/CRT_ID - CRT_HARDWARE_TYPES = { - 0: "Normal cartridge", - 1: "Action Replay", - 2: "KCS Power Cartridge", - 3: "Final Cartridge III", - 4: "Simons Basic", - 5: "Ocean type 1", - 6: "Expert Cartridge", - 7: "Fun Play, Power Play", - 8: "Super Games", - 9: "Atomic Power", - 10: "Epyx Fastload", - 11: "Westermann Learning", - 12: "Rex Utility", - 13: "Final Cartridge I", - 14: "Magic Formel", - 15: "C64 Game System, System 3", - 16: "WarpSpeed", - 17: "Dinamic", - 18: "Zaxxon, Super Zaxxon (SEGA)", - 19: "Magic Desk, Domark, HES Australia", - 20: "Super Snapshot V5", - 21: "Comal-80", - 22: "Structured Basic", - 23: "Ross", - 24: "Dela EP64", - 25: "Dela EP7x8", - 26: "Dela EP256", - 27: "Rex EP256", - 28: "Mikro Assembler", - 29: "Final Cartridge Plus", - 30: "Action Replay 4", - 31: "StarDOS", - 32: "EasyFlash", - 33: "EasyFlash X-Bank", - 34: "Capture", - 35: "Action Replay 3", - 36: "Retro Replay, Nordic Replay", - 37: "MMC64", - 38: "MMC Replay", - 39: "IDE64", - 40: "Super Snapshot V4", - 41: "IEEE488", - 42: "Game Killer", - 43: "Prophet 64", - 44: "Exos", - 45: "Freeze Frame", - 46: "Freeze Machine", - 47: "Snapshot64", - 48: "Super Explode V5", - 49: "Magic Voice", - 50: "Action Replay 2", - 51: "MACH 5", - 52: "Diashow Maker", - 53: "Pagefox", - 54: "Kingsoft Business Basic", - 55: "Silver Rock 128", - 56: "Formel 64", - 57: "RGCD", - 58: "RR-Net MK3", - 59: "Easy Calc Result", - 60: "GMod2", - 61: "MAX BASIC", - 62: "GMod3", - 63: "ZIPP-CODE 48", - 64: "Blackbox V8", - 65: "Blackbox V3", - 66: "Blackbox V4", - 67: "REX RAM Floppy", - 68: "BIS Plus", - 69: "SD Box", - 70: "MultiMAX", - 71: "Blackbox V9", - 72: "LT Kernal", - 73: "CMD RAMlink", - 74: "Drean (H.E.R.O. bootleg)", - 75: "IEEE Flash 64", - 76: "Turtle Graphics II", - 77: "Freeze Frame MK2", - 78: "Partner 64", - 79: "Hyper-BASIC MK2", - 80: "Universal Cartridge 1", - 81: "Universal Cartridge 1.5", - 82: "Universal Cartridge 2", - 83: "BMP Data Turbo 2000", - 84: "Profi-DOS", - 85: "Magic Desk 16", - } - - @classmethod - def args(cls, parser) -> None: - """Add C64-specific command-line arguments to an argument parser. - - Args: - parser: An argparse.ArgumentParser instance - """ - # Core emulator options - core_group = parser.add_argument_group("Core Options") - core_group.add_argument( - "--rom-dir", - type=Path, - default=Path("./roms"), - help="Directory containing ROM files (default: ./roms)", - ) - core_group.add_argument( - "--display", - type=str, - choices=["terminal", "pygame", "headless", "repl"], - default="pygame", - help="Display mode: pygame (default, graphical window), terminal (ASCII art), headless (no display), or repl (interactive terminal with keyboard input). Automatically falls back to terminal if pygame unavailable.", - ) - core_group.add_argument( - "--scale", - type=int, - default=2, - help="Pygame window scaling factor (default: 2 = 640x400)", - ) - core_group.add_argument( - "--video-chip", - type=str.upper, - choices=["6569", "6567R8", "6567R56A", "PAL", "NTSC"], - default="6569", - help="VIC-II chip variant: 6569 (PAL, default), 6567R8 (NTSC 1984+), " - "6567R56A (old NTSC 1982-1984). PAL/NTSC are aliases for 6569/6567R8.", - ) - core_group.add_argument( - "--no-irq", - action="store_true", - help="Disable IRQ injection (for debugging; system will hang waiting for IRQs)", - ) - core_group.add_argument( - "--throttle", - action="store_true", - default=True, - help="Throttle emulation to real-time speed (default: enabled)", - ) - core_group.add_argument( - "--no-throttle", - action="store_false", - dest="throttle", - help="Disable throttling - run at maximum speed (for benchmarks)", - ) - core_group.add_argument( - "--mouse", - action="store_true", - help="Enable 1351 proportional mouse emulation (pygame mode only)", - ) - core_group.add_argument( - "--mouse-port", - type=int, - choices=[1, 2], - default=1, - help="Joystick port for mouse (1 or 2, default: 1)", - ) - core_group.add_argument( - "--mouse-sensitivity", - type=float, - default=1.0, - help="Mouse sensitivity multiplier (default: 1.0)", - ) - core_group.add_argument( - "--paddle", - action="store_true", - help="Enable paddle emulation using mouse position (pygame mode only)", - ) - core_group.add_argument( - "--paddle-port", - type=int, - choices=[1, 2], - default=1, - help="Joystick port for paddles (1 or 2, default: 1)", - ) - core_group.add_argument( - "--lightpen", - action="store_true", - help="Enable lightpen emulation using mouse position (pygame mode only, port 1)", - ) - core_group.add_argument( - "--joystick", - action="store_true", - help="Enable keyboard joystick emulation (numpad or WASD+Space)", - ) - # CPU variant selection - uses core library function - add_cpu_arguments(parser, group_name="CPU Options") - - core_group.add_argument( - "--joystick-port", - type=int, - choices=[1, 2], - default=2, - help="Joystick port for keyboard emulation (1 or 2, default: 2)", - ) - - # Program loading options - program_group = parser.add_argument_group("Program Loading") - program_group.add_argument( - "--program", - type=Path, - help="Program file to load and run (.prg, .bin, etc.)", - ) - program_group.add_argument( - "--load-address", - type=lambda x: int(x, 0), - help="Override load address (hex or decimal, e.g., 0x0801 or 2049)", - ) - program_group.add_argument( - "--no-roms", - action="store_true", - help="Run without C64 ROMs (for testing standalone programs)", - ) - program_group.add_argument( - "--run", - action="store_true", - help="Auto-run program after loading (injects RUN command after boot)", - ) - - # Cartridge options - cart_group = parser.add_argument_group("Cartridge Options") - cart_group.add_argument( - "--cartridge", - type=Path, - help="Cartridge file to load (.crt format or raw binary)", - ) - cart_group.add_argument( - "--cartridge-type", - type=str.lower, - choices=["auto", "8k", "16k", "ultimax"], - default="auto", - help="Cartridge type: auto (detect from file), 8k, 16k, or ultimax (default: auto)", - ) - - # Disk drive options - drive_group = parser.add_argument_group("Disk Drive Options") - drive_group.add_argument( - "--disk", - type=Path, - help="D64 disk image to insert into drive 8", - ) - drive_group.add_argument( - "--drive-rom", - type=Path, - help="1541 DOS ROM file (default: /1541.rom)", - ) - drive_group.add_argument( - "--no-drive", - action="store_true", - help="Disable 1541 drive emulation (faster boot, no disk access)", - ) - drive_group.add_argument( - "--drive-runner", - choices=["threaded", "synchronous", "multiprocess"], - default="threaded", - dest="drive_runner", - help="Drive emulation runner: threaded (default), synchronous, or multiprocess", - ) - - # Execution control options - exec_group = parser.add_argument_group("Execution Control") - exec_group.add_argument( - "--max-cycles", - type=int, - default=INFINITE_CYCLES, - help="Maximum CPU cycles to execute (default: infinite)", - ) - exec_group.add_argument( - "--stop-on-basic", - action="store_true", - help="Stop execution when BASIC prompt is ready (useful for benchmarking boot time)", - ) - exec_group.add_argument( - "--stop-on-illegal-instruction", - action="store_true", - help="Stop and dump crash report when an illegal instruction is executed", - ) - - # Output options - output_group = parser.add_argument_group("Output Options") - output_group.add_argument( - "--dump-mem", - nargs=2, - metavar=("START", "END"), - type=lambda x: int(x, 0), - help="Dump memory region after execution (hex or decimal addresses)", - ) - output_group.add_argument( - "--show-screen", - action="store_true", - help="Display screen RAM after execution (40x25 character display)", - ) - output_group.add_argument( - "--verbose", - "-v", - action="store_true", - help="Enable verbose logging", - ) - - # Disassembly options - disasm_group = parser.add_argument_group("Disassembly") - disasm_group.add_argument( - "--disassemble", - type=lambda x: int(x, 0), - metavar="ADDRESS", - help="Disassemble at address and exit (hex or decimal)", - ) - disasm_group.add_argument( - "--num-instructions", - type=int, - default=20, - help="Number of instructions to disassemble (default: 20)", - ) - - # Debug flag arguments - debug_group = parser.add_argument_group("Debug Flags") - debug_group.add_argument( - "--debug-cia", - action="store_true", - help="Enable CIA register read/write logging", - ) - debug_group.add_argument( - "--debug-vic", - action="store_true", - help="Enable VIC register operation logging", - ) - debug_group.add_argument( - "--debug-jiffy", - action="store_true", - help="Enable jiffy clock update logging", - ) - debug_group.add_argument( - "--debug-keyboard", - action="store_true", - help="Enable keyboard event logging", - ) - debug_group.add_argument( - "--debug-screen", - action="store_true", - help="Enable screen memory write logging", - ) - debug_group.add_argument( - "--debug-cursor", - action="store_true", - help="Enable cursor position variable logging", - ) - debug_group.add_argument( - "--debug-kernal", - action="store_true", - help="Enable CPU logging when entering KERNAL ROM", - ) - debug_group.add_argument( - "--debug-basic", - action="store_true", - help="Enable CPU logging when entering BASIC ROM", - ) - debug_group.add_argument( - "--verbose-cycles", - action="store_true", - help="Enable per-cycle CPU logging (f/r/w/o markers) - very slow, for debugging only", - ) - - @classmethod - def from_args(cls, args) -> "C64": - """Create a C64 instance from parsed command-line arguments. - - Args: - args: Parsed argparse namespace with C64 arguments - - Returns: - Configured C64 instance - """ - return cls( - rom_dir=args.rom_dir, - display_mode=args.display, - scale=args.scale, - enable_irq=not getattr(args, 'no_irq', False), - video_chip=args.video_chip, - cpu_variant=getattr(args, 'cpu', '6502'), - verbose_cycles=getattr(args, 'verbose_cycles', False), - ) - - - def __init__(self, rom_dir: Path = Path("./roms"), display_mode: str = "pygame", scale: int = 2, enable_irq: bool = True, video_chip: str = "6569", cpu_variant: str = "6502", verbose_cycles: bool = False) -> None: - """Initialize the C64 emulator. - - Arguments: - rom_dir: Directory containing ROM files (basic, kernal, char) - display_mode: Display mode (pygame [default], terminal, headless) - If pygame fails to initialize, will automatically fall back to terminal - scale: Pygame window scaling factor - enable_irq: Enable IRQ injection (default: True) - video_chip: VIC-II chip variant ("6569" for PAL, "6567R8" for NTSC, - "6567R56A" for old NTSC). PAL/NTSC are aliases. - cpu_variant: CPU variant to emulate ("6502", "6502A", "6502C", "65C02"). - Default is "6502" (NMOS 6502). Note: The C64 uses a 6510 - which is essentially a 6502 with I/O ports. - verbose_cycles: Enable per-cycle CPU logging (default: False) - """ - self.rom_dir = Path(rom_dir) - self.display_mode = display_mode - self.scale = scale - self.enable_irq = enable_irq - - # Map video chip selection to VideoTiming - video_chip_upper = video_chip.upper() - if video_chip_upper in ("6569", "PAL"): - self.video_timing = VideoTiming.VIC_6569 - elif video_chip_upper in ("6567R8", "NTSC"): - self.video_timing = VideoTiming.VIC_6567R8 - elif video_chip_upper == "6567R56A": - self.video_timing = VideoTiming.VIC_6567R56A - else: - raise ValueError(f"Unknown video chip: {video_chip}") - - self.video_chip = self.video_timing.chip_name - - # If pygame mode requested, try to initialize it and fall back to terminal if it fails - if self.display_mode == "pygame": - try: - import pygame - # Test if pygame can actually initialize (might fail on headless systems) - pygame.init() - pygame.quit() - except (ImportError, Exception) as e: - log.warning(f"Pygame initialization failed: {e}") - log.warning("Falling back to terminal display mode") - self.display_mode = "terminal" - - # Initialize CPU (6510 is essentially a 6502 with I/O ports) - # Parse the cpu_variant string to get the enum - self._cpu_variant = CPUVariant.from_string(cpu_variant) - self.cpu = CPU(cpu_variant=self._cpu_variant, verbose_cycles=verbose_cycles) - - log.info(f"Initialized CPU: {self.cpu.variant_name}") - - # Storage for ROMs - self.basic_rom: Optional[bytes] = None - self.kernal_rom: Optional[bytes] = None - self.char_rom: Optional[bytes] = None - self.vic: Optional[C64VIC] = None - self.cia1: Optional[CIA1] = None - self.cia2: Optional[CIA2] = None - - # Cartridge support - the Cartridge object handles all banking logic - # and provides EXROM/GAME signals. Stored on C64Memory, accessed via self.memory.cartridge - # Cartridge type string for display purposes - self.cartridge_type: str = "none" # "none", "8k", "16k", "error" - - # 1541 Disk Drive support - self.iec_bus: Optional[IECBus] = None - self.drive8: Optional[Drive1541] = None - self.drive_enabled: bool = False # Set by attach_drive() - - # Pygame display attributes - self.pygame_screen = None - self.pygame_surface = None - self.pygame_available = False - - # Screen dirty tracking for optimized rendering - self.dirty_tracker = ScreenDirtyTracker() - - # Debug logging control - self.basic_logging_enabled = False - self.last_pc_region = None - - # BASIC ready detection (set by pc_callback when PC enters BASIC ROM range) - self._basic_ready = False - self._stop_on_basic = False - - # KERNAL keyboard input detection (PC at $E5CF-$E5D6 = waiting for input) - self._kernal_waiting_for_input = False - self._stop_on_kernal_input = False - - # Execution timing for speedup calculation - self._execution_start_time: Optional[float] = None - self._execution_end_time: Optional[float] = None - - # Rolling average speed tracking (default 10 samples = 10 second window) - from collections import deque - self._speed_sample_count: int = 10 - self._speed_samples: deque[float] = deque(maxlen=self._speed_sample_count) - self._last_sample_time: float = 0.0 - self._last_sample_cycles: int = 0 - - # Load ROMs during initialization - # This sets up the memory handler and all peripherals (VIC, CIAs) - self.load_roms() - - def load_rom(self, filename: str, expected_size: int, description: str) -> bytes: - """Load a ROM file from the rom directory. - - Arguments: - filename: Name of the ROM file - expected_size: Expected size in bytes - description: Human-readable description for logging - - Returns: - ROM data as bytes - - Raises: - FileNotFoundError: If ROM file doesn't exist - ValueError: If ROM file size is incorrect - """ - rom_path = self.rom_dir / filename - - if not rom_path.exists(): - raise FileNotFoundError( - f"{description} ROM not found: {rom_path}\n" - f"Expected file: {filename} in directory: {self.rom_dir}" - ) - - rom_data = rom_path.read_bytes() - - if len(rom_data) != expected_size: - raise ValueError( - f"{description} ROM has incorrect size: {len(rom_data)} bytes " - f"(expected {expected_size} bytes)" - ) - - log.info(f"Loaded {description} ROM: {rom_path} ({len(rom_data)} bytes)") - return rom_data - - def load_roms(self) -> None: - """Load all C64 ROM files into memory.""" - # Try common ROM filenames - basic_names = ["basic", "basic.rom", "basic.901226-01.bin"] - kernal_names = ["kernal", "kernal.rom", "kernal.901227-03.bin"] - char_names = ["char", "char.rom", "characters.901225-01.bin", "chargen"] - - # Load BASIC ROM - for name in basic_names: - try: - self.basic_rom = self.load_rom(name, BASIC_ROM_SIZE, "BASIC") - break - except FileNotFoundError: - continue - else: - raise FileNotFoundError( - f"BASIC ROM not found. Tried: {', '.join(basic_names)} in {self.rom_dir}" - ) - - # Load KERNAL ROM - for name in kernal_names: - try: - self.kernal_rom = self.load_rom(name, KERNAL_ROM_SIZE, "KERNAL") - break - except FileNotFoundError: - continue - else: - raise FileNotFoundError( - f"KERNAL ROM not found. Tried: {', '.join(kernal_names)} in {self.rom_dir}" - ) - - # Load CHAR ROM (optional for now) - for name in char_names: - try: - self.char_rom = self.load_rom(name, CHAR_ROM_SIZE, "CHAR") - break - except FileNotFoundError: - continue - - if self.char_rom is None: - log.warning(f"CHAR ROM not found (optional). Tried: {', '.join(char_names)}") - - # DON'T write ROMs to CPU RAM! The C64Memory handler reads from ROM arrays directly. - # Writing them to RAM would: - # 1. Waste memory (duplicate data) - # 2. Cause banking issues (RAM vs ROM access) - # 3. Corrupt I/O space in case of CHAR ROM ($D000-$DFFF) - # - # The memory handler (installed at line 1086) provides ROM access via banking logic: - # - BASIC ROM at $A000-$BFFF when bit 0 of $0001 is set - # - KERNAL ROM at $E000-$FFFF when bit 1 of $0001 is set - # - CHAR ROM at $D000-$DFFF when bit 2 of $0001 is clear (I/O disabled) - # - # self._write_rom_to_memory(BASIC_ROM_START, self.basic_rom) - # self._write_rom_to_memory(KERNAL_ROM_START, self.kernal_rom) - # self._write_rom_to_memory(CHAR_ROM_START, self.char_rom) - - - # Now set up the CIA1, CIA2, SID, and VIC - self.cia1 = CIA1(cpu=self.cpu) - self.cia2 = CIA2(cpu=self.cpu) - - # Link the CIAs for FLAG pin cross-triggering (IEC bus simulation) - self.cia1.set_other_cia(self.cia2) - self.cia2.set_other_cia(self.cia1) - - self.sid = SID() - self.vic = C64VIC(char_rom=self.char_rom, cpu=self.cpu, cia2=self.cia2, video_timing=self.video_timing) - - # Initialize memory - self.memory = C64Memory( - self.cpu.ram, - basic_rom=self.basic_rom, - kernal_rom=self.kernal_rom, - char_rom=self.char_rom, - cia1=self.cia1, - cia2=self.cia2, - vic=self.vic, - sid=self.sid, - dirty_tracker=self.dirty_tracker, - ) - # Hook up the memory handler so CPU RAM accesses go through C64Memory - self.cpu.ram.memory_handler = self.memory - - # Give VIC access to C64Memory for VBlank snapshots - self.vic.set_memory(self.memory) - - # Set up periodic update callback for VIC, CIA1, CIA2, and disk drive - # VIC checks cycle count and triggers raster IRQs - # CIA1 counts down timers and triggers timer IRQs - # CIA2 counts down timers and triggers NMIs - # IEC bus and drive: The bus updates happen immediately when CIA2 port A - # is written, and the drive CPU is synchronized at that time. The periodic - # callback just ensures regular updates for VIC/CIA timers. - def update_peripherals(): - self.vic.update() - self.cia1.update() - self.cia2.update() - # Note: Drive CPU sync is now handled per-instruction via - # post_instruction_callback for cycle-accurate IEC timing - # In headless mode, clear frame_complete since there's no render thread - if self.display_mode == "headless" and self.vic.frame_complete.is_set(): - self.vic.frame_complete.clear() - - self.cpu.periodic_callback = update_peripherals - self.cpu.periodic_callback_interval = self.vic.cycles_per_line # Update every raster line - - log.info("All ROMs loaded into memory") - - # Note: PC is already set from reset vector by cpu.reset() in main() - # The reset() method handles the complete reset sequence including - # fetching the vector from $FFFC/$FFFD and setting PC accordingly - log.info(f"PC initialized to ${self.cpu.PC:04X} (from reset vector at ${self.RESET_VECTOR_ADDR:04X})") - - def attach_drive(self, drive_rom_path: Optional[Path] = None, disk_path: Optional[Path] = None, - runner: str = "threaded") -> bool: - """Attach a 1541 disk drive to the IEC bus. - - Supports multiple ROM formats: - - Single 16KB ROM (1541-II style): 1541.rom, 1541-II.251968-03.bin, etc. - - Two 8KB ROMs (original 1541): 1541-c000.bin + 1541-e000.bin - - Args: - drive_rom_path: Path to 1541 DOS ROM (default: auto-detect in rom_dir) - disk_path: Optional D64 disk image to insert - runner: Drive execution mode: - - "threaded" (default): Uses ThreadedIECBus for atomic state - - "synchronous": Cycle-accurate emulation - - "multiprocess": Drive runs in separate process (bypasses GIL) - - Returns: - True if drive attached successfully, False otherwise - """ - rom_path = drive_rom_path - rom_path_e000 = None - - if rom_path is None: - # Try to find 1541 ROM(s) in rom_dir - # Priority 1: Single 16KB ROM files (most common) - rom_16k_names = [ - "1541.rom", - "1541-II.rom", - "1541-II.251968-03.bin", # Most compatible 1541-II ROM - "1541C.251968-01.bin", - "1541C.251968-02.bin", - "1541-II.355640-01.bin", - "dos1541", - "dos1541.rom", - ] - for name in rom_16k_names: - candidate = self.rom_dir / name - if candidate.exists(): - rom_path = candidate - break - - # Priority 2: Two 8KB ROM files (original 1541 style) - if rom_path is None: - # Look for C000 ROM - c000_names = [ - "1541-c000.325302-01.bin", - "1541-c000.bin", - "1540-c000.325302-01.bin", - ] - # Look for E000 ROM (multiple revisions available) - e000_names = [ - "1541-e000.901229-05.bin", # Short-board, most common - "1541-e000.901229-03.bin", # Long-board with autoboot - "1541-e000.901229-02.bin", - "1541-e000.901229-01.bin", - "1541-e000.bin", - ] - - for c000_name in c000_names: - c000_candidate = self.rom_dir / c000_name - if c000_candidate.exists(): - # Found C000 ROM, now look for E000 ROM - for e000_name in e000_names: - e000_candidate = self.rom_dir / e000_name - if e000_candidate.exists(): - rom_path = c000_candidate - rom_path_e000 = e000_candidate - break - if rom_path_e000 is not None: - break - - if rom_path is None or not rom_path.exists(): - log.warning(f"1541 ROM not found in {self.rom_dir}. Drive disabled.") - log.warning(" Expected: 1541.rom (16KB) or 1541-c000.bin + 1541-e000.bin (8KB each)") - return False - - # Track the runner mode - self.drive_runner = runner - - if runner == "multiprocess": - # Multiprocess mode: Drive runs in separate process (bypasses GIL) - import os - import time - - # Create shared memory for IEC state (use PID + time for uniqueness) - unique_id = f"{os.getpid()}_{int(time.time() * 1000) % 1000000}" - self._iec_shared_state = SharedIECState( - name=f"iec_bus_{unique_id}", - create=True - ) - - # Create multiprocess IEC bus - self.iec_bus = MultiprocessIECBus(self._iec_shared_state) - self.iec_bus.connect_c64(self.cia2) - self.cia2.set_iec_bus(self.iec_bus) - - # Create and start drive subprocess - self.drive8 = MultiprocessDrive1541(device_number=8) - self.drive8.start_process( - rom_path=rom_path, - rom_path_e000=rom_path_e000, - disk_path=disk_path, - shared_state=self._iec_shared_state, - ) - - # Wire up tick synchronization Events - tick_request, tick_done = self.drive8.get_tick_events() - self.iec_bus.set_tick_events(tick_request, tick_done) - - self.drive_enabled = True - - # Set up synchronous tick-based execution - # Similar to threaded mode but with batching to reduce IPC overhead - self._mp_accumulated_cycles = 0 - self._mp_batch_size = 100 # Cycles per IPC call (balance timing vs overhead) - - def sync_multiprocess(cpu, cycles): - """Accumulate cycles and sync with drive subprocess.""" - if self.drive8 and self._iec_shared_state: - # Update IEC bus state - self.iec_bus.update() - - # Accumulate cycles - self._mp_accumulated_cycles += cycles - - # When batch is full, sync with drive - if self._mp_accumulated_cycles >= self._mp_batch_size: - batch = self._mp_accumulated_cycles - self._mp_accumulated_cycles = 0 - - # Update C64 cycle counter - self._iec_shared_state.set_c64_cycles(cpu.cycles_executed) - - # Wait for drive to catch up to our cycle count - tick_request, tick_done = self.drive8.get_tick_events() - target_cycles = cpu.cycles_executed - - # Signal drive and wait for it to process - import time - max_wait = 0.1 # seconds - start = time.time() - while time.time() - start < max_wait: - tick_request.set() - drive_cycles = self._iec_shared_state.get_drive_cycles() - if drive_cycles >= target_cycles - 50: - break - time.sleep(0.00001) # 10us - - # Read bus state after drive processed - self.iec_bus.atn, self.iec_bus.clk, self.iec_bus.data = \ - self._iec_shared_state.get_bus_state(is_drive=False) - - self.cpu.post_tick_callback = sync_multiprocess - log.info(f"1541 drive 8 attached in MULTIPROCESS mode (ROM: {rom_path.name})") - return True - - elif runner == "threaded": - # Threaded mode: Uses ThreadedIECBus for atomic state updates - self.iec_bus = ThreadedIECBus() - self.iec_bus.connect_c64(self.cia2) - self.cia2.set_iec_bus(self.iec_bus) - - # Create threaded drive 8 - self.drive8 = ThreadedDrive1541(device_number=8) - else: - # Synchronous mode: Cycle-accurate but slower - self.iec_bus = IECBus() - self.iec_bus.connect_c64(self.cia2) - self.cia2.set_iec_bus(self.iec_bus) - - # Create standard drive 8 - self.drive8 = Drive1541(device_number=8) - - # Create a separate CPU for the drive (for threaded/synchronous modes) - # The 1541 uses a full 6502 (not 6510) - drive_cpu = CPU(cpu_variant=CPUVariant.NMOS_6502, verbose_cycles=False) - self.drive8.cpu = drive_cpu - - # Set up drive CPU memory handler - drive_cpu.ram.memory_handler = self.drive8.memory - - # Load 1541 ROM (supports both 16KB single file and 8KB+8KB split) - try: - self.drive8.load_rom(rom_path, rom_path_e000) - except Exception as e: - log.error(f"Failed to load 1541 ROM: {e}") - self.drive8 = None - self.iec_bus = None - return False - - # Connect drive to IEC bus - if runner == "threaded": - self.drive8.connect_to_threaded_bus(self.iec_bus) - else: - self.iec_bus.connect_drive(self.drive8) - - # Insert disk if provided - if disk_path is not None: - try: - self.drive8.insert_disk(disk_path) - except Exception as e: - log.error(f"Failed to insert disk: {e}") - - # Reset drive to initialize - self.drive8.reset() - - self.drive_enabled = True - - if runner == "threaded": - # Threaded mode: Uses ThreadedIECBus for thread-safe state - # but still runs drive in lockstep via post_tick_callback - # The threading benefit is that bus state updates are atomic - - def sync_drive_on_tick_threaded(cpu, cycles): - """Sync drive CPU and IEC bus after each C64 cycle consumption.""" - if self.drive8 and self.drive8.cpu: - # Update IEC bus state so drive sees current C64 outputs - self.iec_bus.update() - - # Run drive for the same number of cycles (1:1 sync) - # Call the base class tick() directly since ThreadedDrive1541.tick() is a no-op - Drive1541.tick(self.drive8, cycles) - - # Update IEC bus again so C64 sees drive's response - self.iec_bus.update() - - self.cpu.post_tick_callback = sync_drive_on_tick_threaded - # Note: Not starting drive thread - running synchronously with ThreadedIECBus - log.info(f"1541 drive 8 attached with ThreadedIECBus (ROM: {rom_path.name})") - else: - # Synchronous mode: Set up cycle-accurate IEC synchronization - # using post_tick_callback. The tick() function is called every - # time the CPU spends cycles, which is the natural place to - # synchronize connected hardware. - # - # The IEC serial bus is bit-banged and requires tight timing between - # the C64 and 1541 CPUs. By hooking into tick(), we ensure the drive - # runs in lockstep with every cycle the C64 spends. - - def sync_drive_on_tick(cpu, cycles): - """Sync drive CPU and IEC bus after each C64 cycle consumption.""" - if self.drive8 and self.drive8.cpu: - # Update IEC bus state so drive sees current C64 outputs - self.iec_bus.update() - - # Run drive for the same number of cycles - # The drive's tick() method handles its own cycle budget - self.drive8.tick(cycles) - - # Update IEC bus again so C64 sees drive's response - self.iec_bus.update() - - self.cpu.post_tick_callback = sync_drive_on_tick - log.info(f"1541 drive 8 attached in SYNCHRONOUS mode (ROM: {rom_path.name})") - - return True - - def insert_disk(self, disk_path: Path) -> bool: - """Insert a D64 disk image into drive 8. - - Args: - disk_path: Path to D64 disk image - - Returns: - True if disk inserted successfully - """ - if not self.drive_enabled or self.drive8 is None: - log.error("No drive attached. Use --disk or attach_drive() first.") - return False - - try: - self.drive8.insert_disk(disk_path) - return True - except Exception as e: - log.error(f"Failed to insert disk: {e}") - return False - - def eject_disk(self) -> None: - """Eject the disk from drive 8.""" - if self.drive8 is not None: - self.drive8.eject_disk() - - def cleanup(self) -> None: - """Clean up resources (drive subprocess, shared memory, etc.).""" - # Stop multiprocess drive if running - if self.drive8 is not None: - if isinstance(self.drive8, MultiprocessDrive1541): - self.drive8.stop_process() - elif isinstance(self.drive8, ThreadedDrive1541): - self.drive8.stop_thread() - - # Clean up shared memory - if hasattr(self, '_iec_shared_state') and self._iec_shared_state is not None: - try: - self._iec_shared_state.close() - self._iec_shared_state.unlink() - except Exception: - pass - self._iec_shared_state = None - - def load_cartridge(self, path: Path, cart_type: str = "auto") -> None: - """Load a cartridge ROM file. - - Supports: - - Raw binary files (.bin, .rom): 8KB or 16KB - - CRT files (.crt): Standard C64 cartridge format with header - - Arguments: - path: Path to cartridge file - cart_type: "auto" (detect from file), "8k", or "16k" - - Raises: - FileNotFoundError: If cartridge file doesn't exist - ValueError: If cartridge format is invalid or unsupported - """ - path = Path(path) - if not path.exists(): - raise FileNotFoundError(f"Cartridge file not found: {path}") - - data = path.read_bytes() - suffix = path.suffix.lower() - - # Check for CRT format (has "C64 CARTRIDGE" signature) - if suffix == ".crt" or data[:16] == b"C64 CARTRIDGE ": - self._load_crt_cartridge(data, path) - else: - # Raw binary format - self._load_raw_cartridge(data, path, cart_type) - - # Log cartridge status - if self.memory is not None and self.memory.cartridge is not None: - cart = self.memory.cartridge - log.info( - f"Cartridge loaded: {path.name} " - f"(type: {self.cartridge_type}, EXROM={0 if not cart.exrom else 1}, GAME={0 if not cart.game else 1})" - ) - - def _load_raw_cartridge(self, data: bytes, path: Path, cart_type: str) -> None: - """Load a raw binary cartridge file. - - Arguments: - data: Raw cartridge data - path: Path to cartridge file (for error messages) - cart_type: "auto", "8k", "16k", or "ultimax" - - Raises: - ValueError: If size doesn't match expected cartridge size - """ - size = len(data) - - # Determine cartridge type from size and content if auto - if cart_type == "auto": - if size == self.ROML_SIZE: - # 8KB file - check if it's a standard 8K cart or Ultimax - # Standard 8K carts have CBM80 signature at offset 4 ($8004) - # Ultimax carts have reset vector at end pointing to $E000-$FFFF - has_cbm80 = data[4:9] == b"CBM80" - - if has_cbm80: - cart_type = "8k" - log.debug(f"Auto-detected 8K cartridge (CBM80 signature found)") - else: - # Check reset vector at end of file (offsets $1FFC/$1FFD) - # For Ultimax, this should point to $E000-$FFFF range - reset_lo = data[0x1FFC] - reset_hi = data[0x1FFD] - reset_vector = reset_lo | (reset_hi << 8) - - if 0xE000 <= reset_vector <= 0xFFFF: - cart_type = "ultimax" - log.info( - f"Auto-detected Ultimax cartridge: reset vector ${reset_vector:04X} " - f"points to cartridge ROM space" - ) - else: - # Default to 8K if we can't determine - cart_type = "8k" - log.debug( - f"Assuming 8K cartridge (no CBM80, reset vector ${reset_vector:04X})" - ) - elif size == self.ROML_SIZE + self.ROMH_SIZE: - cart_type = "16k" - else: - raise ValueError( - f"Cannot auto-detect cartridge type for {path.name}: " - f"size {size} bytes (expected 8192 or 16384)" - ) - - # Validate size matches specified type and create Cartridge object - if cart_type == "8k": - if size != self.ROML_SIZE: - raise ValueError( - f"8K cartridge {path.name} has wrong size: " - f"{size} bytes (expected {self.ROML_SIZE})" - ) - cartridge = StaticROMCartridge( - roml_data=data, - romh_data=None, - name=path.stem, - ) - self.cartridge_type = "8k" - elif cart_type == "16k": - if size != self.ROML_SIZE + self.ROMH_SIZE: - raise ValueError( - f"16K cartridge {path.name} has wrong size: " - f"{size} bytes (expected {self.ROML_SIZE + self.ROMH_SIZE})" - ) - cartridge = StaticROMCartridge( - roml_data=data[:self.ROML_SIZE], - romh_data=data[self.ROML_SIZE:], - name=path.stem, - ) - self.cartridge_type = "16k" - elif cart_type == "ultimax": - if size != self.ROMH_SIZE: - raise ValueError( - f"Ultimax cartridge {path.name} has wrong size: " - f"{size} bytes (expected {self.ROMH_SIZE})" - ) - cartridge = StaticROMCartridge( - roml_data=None, - romh_data=None, - ultimax_romh_data=data, - name=path.stem, - ) - self.cartridge_type = "ultimax" - else: - raise ValueError(f"Unknown cartridge type: {cart_type}") - - # Attach cartridge to memory handler - if self.memory is not None: - self.memory.cartridge = cartridge - - log.info(f"Loaded raw {cart_type.upper()} cartridge: {path.name} ({size} bytes)") - - def _create_error_cartridge(self, error_lines: list[str]) -> bytes: - """Create an 8KB cartridge ROM that displays an error message. - - This is used when an unsupported cartridge type is loaded, to give - the user a friendly on-screen message instead of crashing. - - Arguments: - error_lines: List of text lines to display (max ~38 chars each) - - Returns: - 8KB cartridge ROM data - """ - # Use the shared function from cartridges module (single source of truth) - return create_error_cartridge_rom(error_lines, border_color=0x02) - - def _load_error_cartridge_with_results(self, results: CartridgeTestResults) -> None: - """Generate and load an error cartridge displaying test results. - - Arguments: - results: CartridgeTestResults with current pass/fail state - """ - error_lines = results.to_display_lines() - error_roml_data = self._create_error_cartridge(error_lines) - - # Create ErrorCartridge object - cartridge = ErrorCartridge( - roml_data=error_roml_data, - original_type=results.hardware_type, - original_name=results.cart_name, - ) - self.cartridge_type = "error" - - # Attach cartridge to memory handler - if self.memory is not None: - self.memory.cartridge = cartridge - - log.info(f"Loaded error cartridge with test results for type {results.hardware_type}") - - def _load_crt_cartridge(self, data: bytes, path: Path) -> None: - """Load a CRT format cartridge file. - - CRT format: - - 64-byte header with signature, hardware type, EXROM/GAME lines - - CHIP packets containing ROM data with load addresses - - Arguments: - data: CRT file data - path: Path to cartridge file (for error messages) - - Raises: - ValueError: If CRT format is invalid or unsupported - """ - # Create test results - starts with all FAILs - results = CartridgeTestResults() - - # Validate CRT header size - if len(data) < 64: - # Generate error cart with current results (all FAIL) - self._load_error_cartridge_with_results(results) - return - - results.header_size_valid = True - - # Check signature - signature = data[:16] - if signature != b"C64 CARTRIDGE ": - # Generate error cart with current results - self._load_error_cartridge_with_results(results) - return - - results.signature_valid = True - - # Parse header (big-endian values) - header_length = int.from_bytes(data[0x10:0x14], "big") - version_hi = data[0x14] - version_lo = data[0x15] - hardware_type = int.from_bytes(data[0x16:0x18], "big") - exrom_line = data[0x18] - game_line = data[0x19] - cart_name = data[0x20:0x40].rstrip(b"\x00").decode("latin-1", errors="replace") - - # Update results with parsed values - results.version_valid = True # We parsed it successfully - results.hardware_type = hardware_type - results.hardware_name = self.CRT_HARDWARE_TYPES.get(hardware_type, f"Unknown type {hardware_type}") - results.exrom_line = exrom_line - results.game_line = game_line - results.cart_name = cart_name - - log.info( - f"CRT header: name='{cart_name}', version={version_hi}.{version_lo}, " - f"hardware_type={hardware_type} ({results.hardware_name}), EXROM={exrom_line}, GAME={game_line}" - ) - - # Check if hardware type is supported - mapper_supported = hardware_type in CARTRIDGE_TYPES - if mapper_supported: - results.mapper_supported = True - else: - log.warning( - f"Unsupported cartridge type {hardware_type} ({results.hardware_name}). " - f"Will parse CHIP packets for diagnostics." - ) - - # Parse CHIP packets (even for unsupported types, for diagnostics) - offset = header_length - - # For Type 0 (standard cartridge) - roml_data = None - romh_data = None - ultimax_romh_data = None - - # For banked cartridges (Type 1+) - banks: dict[int, bytes] = {} # bank_number -> rom_data - - # Ultimax mode detection from CRT header - # EXROM=1, GAME=0 indicates Ultimax mode - is_ultimax = (exrom_line == 1 and game_line == 0) - - while offset < len(data): - if offset + 16 > len(data): - break # Not enough data for another CHIP header - - chip_sig = data[offset:offset + 4] - if chip_sig != b"CHIP": - # Invalid CHIP signature - generate error cart with results so far - self._load_error_cartridge_with_results(results) - return - - packet_length = int.from_bytes(data[offset + 4:offset + 8], "big") - chip_type = int.from_bytes(data[offset + 8:offset + 10], "big") - bank_number = int.from_bytes(data[offset + 10:offset + 12], "big") - load_address = int.from_bytes(data[offset + 12:offset + 14], "big") - rom_size = int.from_bytes(data[offset + 14:offset + 16], "big") - - log.debug( - f"CHIP packet: type={chip_type}, bank={bank_number}, " - f"load=${load_address:04X}, size={rom_size}" - ) - - # Track that we found a CHIP packet - results.chip_count += 1 - results.chip_packets_found = True - - # Only handle ROM chips (type 0) for now - if chip_type != 0: - log.warning(f"Skipping non-ROM CHIP type {chip_type}") - offset += packet_length - continue - - # Extract ROM data - rom_data = data[offset + 16:offset + 16 + rom_size] - - if hardware_type == 0: - # Type 0: Standard cartridge - single bank only - if bank_number != 0: - log.warning(f"Skipping bank {bank_number} for Type 0 cartridge") - offset += packet_length - continue - - if load_address == self.ROML_START: - # Check if this is a 16KB ROM that needs to be split - if rom_size > self.ROML_SIZE: - # Split 16KB ROM: first 8KB to ROML, second 8KB to ROMH - roml_data = rom_data[:self.ROML_SIZE] - romh_data = rom_data[self.ROML_SIZE:] - results.roml_valid = True - results.romh_valid = True - log.info(f"Loaded ROML: ${load_address:04X}-${load_address + self.ROML_SIZE - 1:04X} ({self.ROML_SIZE} bytes)") - log.info(f"Loaded ROMH: ${self.ROMH_START:04X}-${self.ROMH_START + len(romh_data) - 1:04X} ({len(romh_data)} bytes)") - else: - roml_data = rom_data - results.roml_valid = True - log.info(f"Loaded ROML: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - elif load_address == self.ROMH_START: - romh_data = rom_data - results.romh_valid = True - log.info(f"Loaded ROMH: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - elif load_address == KERNAL_ROM_START: - # Ultimax mode: ROM at $E000-$FFFF replaces KERNAL - ultimax_romh_data = rom_data - results.ultimax_romh_valid = True - log.info(f"Loaded Ultimax ROMH: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - else: - log.warning(f"Unknown CHIP load address: ${load_address:04X}") - elif hardware_type == 3: - # Type 3: Final Cartridge III - 4 x 16KB banks - # Each bank is a single 16KB CHIP packet at $8000 - if load_address == self.ROML_START: - banks[bank_number] = rom_data - results.roml_valid = True - results.bank_switching_valid = len(banks) > 1 - log.info(f"Loaded FC3 bank {bank_number}: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - else: - log.warning(f"Unknown CHIP load address for Type 3: ${load_address:04X}") - elif hardware_type == 4 or hardware_type == 13: - # Type 4: Simons' BASIC - 16KB cartridge with ROML + ROMH - # Type 13: Final Cartridge I - 16KB cartridge with ROML + ROMH - # Two CHIP packets: one for ROML at $8000, one for ROMH at $A000 - if load_address == self.ROML_START: - roml_data = rom_data - results.roml_valid = True - log.info(f"Loaded ROML: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - elif load_address == self.ROMH_START: - romh_data = rom_data - results.romh_valid = True - log.info(f"Loaded ROMH: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - else: - log.warning(f"Unknown CHIP load address for Type {hardware_type}: ${load_address:04X}") - else: - # Banked cartridges (Type 1, 5+): Collect all banks - # Each bank is typically 8KB at $8000 - if load_address == self.ROML_START: - banks[bank_number] = rom_data - results.roml_valid = True # At least one bank loaded to ROML region - results.bank_switching_valid = len(banks) > 1 # Multiple banks = bank switching works - log.info(f"Loaded bank {bank_number}: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") - else: - log.warning(f"Unexpected load address ${load_address:04X} for bank {bank_number}") - - offset += packet_length - - # If mapper not supported, generate error cart with diagnostics - if not mapper_supported: - self._load_error_cartridge_with_results(results) - return - - # Create appropriate cartridge object based on hardware type - if hardware_type == 0: - # Validate we got valid ROM data for Type 0 - if ultimax_romh_data is None and roml_data is None: - # No usable ROM data - generate error cart - self._load_error_cartridge_with_results(results) - return - - cartridge = create_cartridge( - hardware_type=0, - roml_data=roml_data, - romh_data=romh_data, - ultimax_romh_data=ultimax_romh_data, - name=cart_name, - ) - - # Determine cartridge type from what we loaded - if ultimax_romh_data is not None: - self.cartridge_type = "ultimax" - elif romh_data is not None: - self.cartridge_type = "16k" - else: - self.cartridge_type = "8k" - elif hardware_type == 4: - # Type 4: Simons' BASIC - needs both ROML and ROMH - if roml_data is None or romh_data is None: - # Missing ROM data - generate error cart - self._load_error_cartridge_with_results(results) - return - - cartridge = create_cartridge( - hardware_type=4, - roml_data=roml_data, - romh_data=romh_data, - name=cart_name, - ) - self.cartridge_type = "simons_basic" - elif hardware_type == 13: - # Type 13: Final Cartridge I - needs both ROML and ROMH - if roml_data is None: - # Missing ROM data - generate error cart - self._load_error_cartridge_with_results(results) - return - - cartridge = create_cartridge( - hardware_type=13, - roml_data=roml_data, - romh_data=romh_data, - name=cart_name, - ) - self.cartridge_type = "final_cartridge_i" - else: - # Banked cartridges - convert bank dict to sorted list - if not banks: - # No bank data - generate error cart - self._load_error_cartridge_with_results(results) - return - - # Create sorted list of banks (fill missing banks with empty data) - max_bank = max(banks.keys()) - bank_list = [] - for i in range(max_bank + 1): - if i in banks: - bank_list.append(banks[i]) - else: - # Fill missing banks with empty 8KB - log.warning(f"Bank {i} missing, filling with empty data") - bank_list.append(bytes(ROML_SIZE)) - - cartridge = create_cartridge( - hardware_type=hardware_type, - banks=bank_list, - name=cart_name, - ) - self.cartridge_type = results.hardware_name.lower().replace(" ", "_") - - # Mark as fully loaded - results.fully_loaded = True - - # Attach cartridge to memory handler - if self.memory is not None: - self.memory.cartridge = cartridge - - log.info(f"Loaded CRT cartridge: '{cart_name}' (type {hardware_type}: {results.hardware_name})") - - def get_video_standard(self) -> str: - """Get the video standard (PAL or NTSC) based on the current video chip. - - Returns: - "PAL" for chip 6569, "NTSC" for chips 6567R8 or 6567R56A - """ - if self.video_chip == "6569": - return "PAL" - else: # 6567R8 or 6567R56A - return "NTSC" - - def init_pygame_display(self) -> bool: - """Initialize pygame display. - - Returns: - True if pygame was successfully initialized, False otherwise - """ - try: - import pygame - self.pygame_available = True - except ImportError: - log.error("Pygame not installed. Install with: pip install pygame-ce") - return False - - try: - pygame.init() - - # Get display dimensions from VIC (hardware characteristics) - total_width = self.vic.total_width - total_height = self.vic.total_height - - # Create window with scaled dimensions - # Disable pygame vsync - we do our own frame timing via VIC - # This prevents pygame.display.flip() from blocking - width = total_width * self.scale - height = total_height * self.scale - try: - # pygame 2.0+ supports vsync parameter - self.pygame_screen = pygame.display.set_mode((width, height), vsync=0) - except TypeError: - # Older pygame doesn't support vsync parameter - self.pygame_screen = pygame.display.set_mode((width, height)) - pygame.display.set_caption(f"C64 Emulator - {self.get_video_standard()} ({self.video_chip})") - - # Enable key repeat (delay=300ms before repeat, interval=30ms between repeats) - # This matches typical terminal key repeat behavior - pygame.key.set_repeat(300, 30) - - # Initialize clipboard support for Ctrl+V paste - try: - pygame.scrap.init() - except Exception as e: - log.warning(f"Clipboard support unavailable: {e}") - - # Create the rendering surface (384x270 with border) - self.pygame_surface = pygame.Surface((total_width, total_height)) - - log.info(f"Pygame display initialized: {width}x{height} (scale={self.scale})") - return True - - except Exception as e: - log.error(f"Failed to initialize pygame: {e}") - self.pygame_available = False - return False - - def set_speed_sample_count(self, count: int) -> None: - """Set the number of samples for rolling average speed calculation. - - Args: - count: Number of samples to keep (e.g., 10 = 10 second rolling window). - Set to 0 to disable rolling average. - """ - from collections import deque - self._speed_sample_count = count - if count > 0: - self._speed_samples = deque(maxlen=count) - else: - self._speed_samples = deque(maxlen=1) # Keep at least 1 for the API - self._speed_samples.clear() - - def _record_speed_sample(self) -> bool: - """Record a speed sample for rolling average (rate-limited to once per second). - - Returns: - True if a sample was recorded, False if rate-limited. - """ - import time - now = time.time() - - # Only record once per second - if self._last_sample_time > 0 and now - self._last_sample_time < 1.0: - return False - - # Calculate cycles since last sample - current_cycles = self.cpu.cycles_executed - if self._last_sample_time > 0: - delta_time = now - self._last_sample_time - delta_cycles = current_cycles - self._last_sample_cycles - if delta_time > 0: - sample_cps = delta_cycles / delta_time - self._speed_samples.append(sample_cps) - - self._last_sample_time = now - self._last_sample_cycles = current_cycles - return True - - def _update_pygame_title(self, pygame) -> None: - """Update pygame window title with speed stats (rate-limited to once per second).""" - if not self._record_speed_sample(): - return - - # Use rolling average if we have samples - if self._speed_samples: - cycles_per_second = sum(self._speed_samples) / len(self._speed_samples) - real_cpu_freq = self.video_timing.cpu_freq - speedup = cycles_per_second / real_cpu_freq - chip = self.video_timing.chip_name - region = "PAL" if chip == "6569" else "NTSC" - actual_mhz = cycles_per_second / 1e6 - real_mhz = real_cpu_freq / 1e6 - title = (f"C64 Emulator - {region} ({chip}) - " - f"{actual_mhz:.3f}MHz ({speedup:.1%} of {real_mhz:.3f}MHz)") - pygame.display.set_caption(title) - - def _write_rom_to_memory(self, start_addr: int, rom_data: bytes) -> None: - """Write ROM data to CPU memory. - - Arguments: - start_addr: Starting address in CPU memory - rom_data: ROM data to write - """ - for offset, byte_value in enumerate(rom_data): - self.cpu.ram[start_addr + offset] = byte_value - - log.debug(f"Wrote ROM to ${start_addr:04X}-${start_addr + len(rom_data) - 1:04X}") - - def load_program( - self, program_path: Path, load_address: Optional[int] = None - ) -> tuple[int, int]: - """Load a program into memory. - - Arguments: - program_path: Path to the program file - load_address: Address to load program at (default: $0801 for BASIC programs) - If None and file has a 2-byte header, use header address - - Returns: - Tuple of (load_address, end_address) - the address range used - """ - program_path = Path(program_path) - - if not program_path.exists(): - raise FileNotFoundError(f"Program not found: {program_path}") - - program_data = program_path.read_bytes() - - # Check if program has a load address header (common for .prg files) - if load_address is None and len(program_data) >= 2: - # First two bytes might be load address (little-endian) - header_addr = program_data[0] | (program_data[1] << 8) - - # If it looks like a reasonable address, use it - if 0x0000 <= header_addr <= 0xFFFF: - load_address = header_addr - program_data = program_data[2:] # Skip header - log.info(f"Using load address from file header: ${load_address:04X}") - - # Default to BASIC program start - if load_address is None: - load_address = BASIC_PROGRAM_START - - # Write program to memory - for offset, byte_value in enumerate(program_data): - self.cpu.ram[load_address + offset] = byte_value - - end_address = load_address + len(program_data) - - log.info( - f"Loaded program: {program_path.name} " - f"at ${load_address:04X}-${end_address - 1:04X} " - f"({len(program_data)} bytes)" - ) - - return load_address, end_address - - def update_basic_pointers(self, program_end: int) -> None: - """Update BASIC memory pointers after loading a program. - - When loading a BASIC program at $0801, BASIC's internal pointers - must be updated so that RUN knows where the program ends. - - Arguments: - program_end: Address immediately after the last byte of the program - """ - # VARTAB, ARYTAB, and STREND should all point to the end of the program - # (They get properly set up when BASIC parses the program, but for - # directly loaded programs we need to set them manually) - lo = program_end & 0xFF - hi = (program_end >> 8) & 0xFF - - # Set VARTAB (start of variables = end of program) - self.cpu.ram[self.VARTAB] = lo - self.cpu.ram[self.VARTAB + 1] = hi - - # Set ARYTAB (start of arrays = end of variables) - self.cpu.ram[self.ARYTAB] = lo - self.cpu.ram[self.ARYTAB + 1] = hi - - # Set STREND (end of arrays = bottom of strings) - self.cpu.ram[self.STREND] = lo - self.cpu.ram[self.STREND + 1] = hi - - log.info(f"Updated BASIC pointers: VARTAB/ARYTAB/STREND = ${program_end:04X}") - - def inject_keyboard_buffer(self, text: str) -> None: - """Inject a string into the KERNAL keyboard buffer. - - This writes directly to the keyboard buffer at $0277-$0280 and sets - the buffer count at $00C6. The KERNAL will process these characters - as if they were typed. Maximum 10 characters. - - Arguments: - text: String to inject (max 10 chars, typically ending with \\r for RETURN) - """ - # Convert to PETSCII (for simple ASCII chars, it's mostly the same) - # RETURN key is 0x0D in PETSCII - petscii_text = text.replace('\r', '\x0d').replace('\n', '\x0d') - - # Limit to 10 characters (keyboard buffer size) - if len(petscii_text) > 10: - log.warning(f"Keyboard buffer overflow: truncating '{text}' to 10 chars") - petscii_text = petscii_text[:10] - - # Write characters to buffer at $0277 - for i, char in enumerate(petscii_text): - self.cpu.ram[self.KEYBOARD_BUFFER + i] = ord(char) - - # Set buffer count at $00C6 - self.cpu.ram[self.KEYBOARD_BUFFER_SIZE] = len(petscii_text) - - log.info(f"Injected '{text.strip()}' into keyboard buffer ({len(petscii_text)} chars)") - - def inject_keyboard_string(self, text: str, cycles_per_chunk: int = 100_000) -> None: - """Inject a string into the keyboard buffer, handling strings longer than 10 chars. - - For strings longer than 10 characters, this method injects them in chunks - of 10 characters each, running the CPU between chunks to allow KERNAL - to process the buffer. - - Arguments: - text: String to inject (any length, typically ending with \\r for RETURN) - cycles_per_chunk: CPU cycles to run between chunks (default 100,000) - """ - from mos6502.errors import CPUCycleExhaustionError - - # Convert to PETSCII - petscii_text = text.replace('\r', '\x0d').replace('\n', '\x0d') - - # Process in chunks of 10 characters - chunk_size = 10 - position = 0 - - while position < len(petscii_text): - # Wait for keyboard buffer to be empty - max_wait_cycles = 1_000_000 - waited = 0 - while int(self.cpu.ram[self.KEYBOARD_BUFFER_SIZE]) > 0 and waited < max_wait_cycles: - try: - self.cpu.execute(cycles=10_000) - except CPUCycleExhaustionError: - pass - waited += 10_000 - - if waited >= max_wait_cycles: - log.warning("Timeout waiting for keyboard buffer to empty") - break - - # Inject next chunk - chunk = petscii_text[position:position + chunk_size] - for i, char in enumerate(chunk): - self.cpu.ram[self.KEYBOARD_BUFFER + i] = ord(char) - self.cpu.ram[self.KEYBOARD_BUFFER_SIZE] = len(chunk) - - log.debug(f"Injected chunk {position//chunk_size + 1}: {len(chunk)} chars") - position += chunk_size - - # Run CPU to process this chunk - if position < len(petscii_text): - try: - self.cpu.execute(cycles=cycles_per_chunk) - except CPUCycleExhaustionError: - pass - - log.info(f"Injected '{text.strip()}' into keyboard buffer ({len(petscii_text)} chars total)") - - def reset(self) -> None: - """Reset the C64 (CPU reset). - - The CPU reset() method now handles the complete 6502 reset sequence: - - Sets S = 0xFD - - Sets P = 0x34 (I flag set, interrupts disabled) - - Fetches reset vector from $FFFC/$FFFD - - Sets PC to vector value - - Consumes 7 cycles - """ - log.info("Resetting C64...") - - # CPU reset handles the complete hardware reset sequence - self.cpu.reset() - - log.info(f"Reset complete: PC=${self.cpu.PC:04X}, S=${self.cpu.S & 0xFF:02X}") - - def get_pc_region(self) -> str: - """Determine which memory region PC is currently in. - - Takes memory banking into account (via $0001 port). - - Returns: - String describing the region: "RAM", "BASIC", "KERNAL", "I/O", "CHAR", or "???" - """ - pc = self.cpu.PC - port = self.memory.port - - if pc < BASIC_ROM_START: - return "RAM" - elif BASIC_ROM_START <= pc <= BASIC_ROM_END: - # BASIC ROM only visible if bit 0 of port is set - return "BASIC" if (port & 0x01) else "RAM" - elif CHAR_ROM_START <= pc <= CHAR_ROM_END: - # I/O vs CHAR vs RAM depends on bits in port - if port & 0x04: - return "I/O" - elif (port & 0x03) == 0x01: - return "CHAR" - else: - return "RAM" - elif KERNAL_ROM_START <= pc <= KERNAL_ROM_END: - # KERNAL ROM only visible if bit 1 of port is set - return "KERNAL" if (port & 0x02) else "RAM" - else: - return "???" - - def get_drive_pc_region(self) -> str: - """Determine which memory region the drive's PC is currently in. - - 1541 memory map: - - $0000-$07FF: RAM (2KB) - - $1800-$1BFF: VIA1 - - $1C00-$1FFF: VIA2 - - $C000-$FFFF: ROM (DOS) - - Returns: - String describing the region: "RAM", "VIA1", "VIA2", "DOS", or "???" - """ - if not self.drive8 or not self.drive8.cpu: - return "???" - - pc = self.drive8.cpu.PC - - if pc < 0x0800: - return "RAM" - elif 0x1800 <= pc < 0x1C00: - return "VIA1" - elif 0x1C00 <= pc < 0x2000: - return "VIA2" - elif pc >= 0xC000: - return "DOS" - else: - return "???" - - def _get_instruction_color(self, opcode: int) -> str: - """Get ANSI color code for instruction based on type. - - Args: - opcode: The instruction opcode byte - - Returns: - ANSI escape sequence for the instruction color: - - Light blue (96): Common instructions identical across variants - - Yellow (93): Instructions that differ between variants (ADC, SBC, JMP, BRK) - - Red (91): Illegal/undocumented instructions - """ - from mos6502 import instructions - - # ANSI color codes - LIGHT_BLUE = "\033[96m" # Light cyan/blue - YELLOW = "\033[93m" # Yellow - RED = "\033[91m" # Red - RESET = "\033[0m" - - if opcode not in instructions.OPCODE_LOOKUP: - return RED # Unknown opcode - - opcode_obj = instructions.OPCODE_LOOKUP[opcode] - package = opcode_obj.package - - # Check if it's an illegal instruction - if ".illegal." in package: - return RED - - # Check if it's a variant-specific instruction - # These have different implementations for 6502 vs 65C02 - variant_instructions = { - "_adc", # Decimal mode differs - "_sbc", # Decimal mode differs - "_jmp", # Indirect JMP page boundary bug - "_brk", # Flag handling differs - } - for variant_inst in variant_instructions: - if variant_inst in package: - return YELLOW - - return LIGHT_BLUE - - def _format_cpu_status(self, prefix: str = "C64") -> str: - """Format C64 CPU status line. - - Args: - prefix: Label prefix for the status line - - Returns: - Formatted status string with colorized instruction display: - - Light blue: Common instructions identical across variants - - Yellow: Instructions that differ between variants (ADC, SBC, JMP, BRK) - - Red: Illegal/undocumented instructions - """ - from mos6502.flags import format_flags - RESET = "\033[0m" - - flags = format_flags(self.cpu._flags.value) - region = self.get_pc_region() - pc = self.cpu.PC - - # Get opcode for colorization - try: - opcode = self.cpu.ram[pc] - color = self._get_instruction_color(opcode) - except (KeyError, IndexError): - color = "\033[91m" # Red for unknown - opcode = None - - # Disassemble current instruction - try: - inst_str = self.disassemble_instruction(pc) - inst_display = inst_str.strip() - except (KeyError, ValueError, IndexError): - try: - b0 = self.cpu.ram[pc] - b1 = self.cpu.ram[pc + 1] - b2 = self.cpu.ram[pc + 2] - inst_display = f"{b0:02X} {b1:02X} {b2:02X} ???" - except IndexError: - inst_display = "???" - - # Apply color to instruction display - colored_inst = f"{color}{inst_display:20s}{RESET}" - - return (f"{prefix}: Cycles: {self.cpu.cycles_executed:,} | " - f"PC=${pc:04X}[{region}] {colored_inst} | " - f"A=${self.cpu.A:02X} X=${self.cpu.X:02X} " - f"Y=${self.cpu.Y:02X} S=${self.cpu.S & 0xFF:02X} P={flags}") - - def _format_drive_status(self) -> str: - """Format 1541 drive CPU status line. - - Returns: - Formatted status string with colorized instruction display: - - Light blue: Common instructions identical across variants - - Yellow: Instructions that differ between variants (ADC, SBC, JMP, BRK) - - Red: Illegal/undocumented instructions - Empty string if no drive attached. - """ - if not self.drive8 or not self.drive8.cpu: - return "" - - from mos6502.flags import format_flags - from mos6502 import instructions - RESET = "\033[0m" - - drive_cpu = self.drive8.cpu - flags = format_flags(drive_cpu._flags.value) - region = self.get_drive_pc_region() - pc = drive_cpu.PC - - # Disassemble current instruction from drive memory - try: - b0 = self.drive8.memory.read(pc) - b1 = self.drive8.memory.read(pc + 1) - b2 = self.drive8.memory.read(pc + 2) - - # Get color for this opcode - color = self._get_instruction_color(b0) - - # Use the instruction set to get mnemonic and operand size - if b0 in instructions.InstructionSet.map: - inst_info = instructions.InstructionSet.map[b0] - try: - num_bytes = int(inst_info.get("bytes", 1)) - except (ValueError, TypeError): - num_bytes = 1 - assembler = inst_info.get("assembler", "???") - mnemonic = assembler.split()[0] if assembler != "???" else "???" - mode = inst_info.get("addressing", "") - elif b0 in instructions.OPCODE_LOOKUP: - opcode_obj = instructions.OPCODE_LOOKUP[b0] - func_name = opcode_obj.function - mnemonic = func_name.split("_")[0].upper() - if "implied" in func_name or "accumulator" in func_name: - num_bytes = 1 - elif "relative" in func_name or "immediate" in func_name or "zeropage" in func_name: - num_bytes = 2 - else: - num_bytes = 3 - mode = "" - else: - num_bytes = 1 - mnemonic = "???" - mode = "" - - # Build hex dump - hex_bytes = [f"{self.drive8.memory.read(pc + i):02X}" - for i in range(min(num_bytes, 3))] - hex_str = " ".join(hex_bytes).ljust(8) - - # Build operand display - if num_bytes == 1: - operand_str = "" - elif num_bytes == 2: - operand_str = f" ${b1:02X}" - else: - operand = (b2 << 8) | b1 - operand_str = f" ${operand:04X}" - - if mode: - inst_display = f"{hex_str} {mnemonic}{operand_str} ; {mode}" - else: - inst_display = f"{hex_str} {mnemonic}{operand_str}" - - except Exception: - inst_display = "???" - color = "\033[91m" # Red for errors - - # Apply color to instruction display - colored_inst = f"{color}{inst_display:20s}{RESET}" - - return (f"1541: Cycles: {drive_cpu.cycles_executed:,} | " - f"PC=${pc:04X}[{region}] {colored_inst} | " - f"A=${drive_cpu.A:02X} X=${drive_cpu.X:02X} " - f"Y=${drive_cpu.Y:02X} S=${drive_cpu.S & 0xFF:02X} P={flags}") - - @property - def basic_ready(self) -> bool: - """Return True if BASIC input loop has been reached.""" - return self._basic_ready - - @property - def kernal_waiting_for_input(self) -> bool: - """Return True if KERNAL is waiting for keyboard input. - - The KERNAL keyboard input loop is at $E5CF-$E5D6. - When PC is in this range, the system is waiting for user input. - """ - return self._kernal_waiting_for_input - - def _pc_callback(self, new_pc: int) -> None: - """PC change callback - detects when BASIC is ready or KERNAL is waiting for input. - - This is called by the CPU every time PC changes. - """ - # Check if PC is in BASIC ROM range - if BASIC_ROM_START <= new_pc <= BASIC_ROM_END: - self._basic_ready = True - if self._stop_on_basic: - raise StopIteration("BASIC is ready") - - # Check if PC is in KERNAL keyboard input loop ($E5CF-$E5D6) - # This is the GETIN routine that waits for keyboard input - if 0xE5CF <= new_pc <= 0xE5D6: - self._kernal_waiting_for_input = True - if self._stop_on_kernal_input: - raise StopIteration("KERNAL waiting for input") - else: - # Reset when PC leaves the input loop - self._kernal_waiting_for_input = False - - def _setup_pc_callback( - self, stop_on_basic: bool = False, stop_on_kernal_input: bool = False - ) -> None: - """Set up the PC callback for BASIC/KERNAL detection. - - Arguments: - stop_on_basic: If True, raise StopIteration when BASIC is ready - stop_on_kernal_input: If True, raise StopIteration when KERNAL is waiting for input - """ - self._stop_on_basic = stop_on_basic - self._stop_on_kernal_input = stop_on_kernal_input - self.cpu.pc_callback = self._pc_callback - - def _clear_pc_callback(self) -> None: - """Remove the PC callback and reset detection flags.""" - self.cpu.pc_callback = None - self._stop_on_basic = False - self._stop_on_kernal_input = False - - def _check_pc_region(self) -> None: - """Monitor PC and enable detailed logging when entering BASIC or KERNAL ROM.""" - region = self.get_pc_region() - - # Track important PC locations - pc = self.cpu.PC - - # Log when we enter BASIC ROM - if BASIC_ROM_START <= pc <= BASIC_ROM_END and not hasattr(self, '_logged_basic_entry'): - self._logged_basic_entry = True - self._basic_ready = True - log.info(f"*** ENTERED BASIC ROM at ${pc:04X} ***") - - # Log when stuck at KERNAL idle loop ($E5CF-$E5D2) - if 0xE5CF <= pc <= 0xE5D2: - if not hasattr(self, '_e5cf_count'): - self._e5cf_count = 0 - self._e5cf_count += 1 - if self._e5cf_count % 10000 == 0: - log.info(f"*** Still in KERNAL idle loop at ${pc:04X} (count={self._e5cf_count}) ***") - - # Enable logging when entering KERNAL for the first time (for debugging) - if DEBUG_KERNAL and region == "KERNAL" and self.last_pc_region != "KERNAL": - logging.getLogger("mos6502").setLevel(logging.DEBUG) - logging.getLogger("mos6502.cpu").setLevel(logging.DEBUG) - logging.getLogger("mos6502.cpu.flags").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory.RAM").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory.Byte").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory.Word").setLevel(logging.DEBUG) - log.info(f"*** ENTERING KERNAL ROM at ${self.cpu.PC:04X} - Enabling detailed CPU logging ***") - - # Enable logging when entering BASIC for the first time - if DEBUG_BASIC and region == "BASIC" and not self.basic_logging_enabled: - self.basic_logging_enabled = True - logging.getLogger("mos6502").setLevel(logging.DEBUG) - logging.getLogger("mos6502.cpu").setLevel(logging.DEBUG) - logging.getLogger("mos6502.cpu.flags").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory.RAM").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory.Byte").setLevel(logging.DEBUG) - logging.getLogger("mos6502.memory.Word").setLevel(logging.DEBUG) - log.info(f"*** ENTERING BASIC ROM at ${self.cpu.PC:04X} - Enabling detailed CPU logging ***") - - self.last_pc_region = region - - - - def run( - self, - max_cycles: int = INFINITE_CYCLES, - stop_on_basic: bool = False, - stop_on_kernal_input: bool = False, - throttle: bool = True, - stop_on_illegal_instruction: bool = False, - ) -> None: - """Run the C64 emulator. - - Arguments: - max_cycles: Maximum number of CPU cycles to execute - (default: INFINITE_CYCLES for continuous execution) - stop_on_basic: If True, stop execution when BASIC prompt is ready - stop_on_kernal_input: If True, stop execution when KERNAL is waiting for keyboard input - throttle: If True, throttle emulation to real-time speed (default: True) - Use --no-throttle for benchmarks to run at maximum speed - stop_on_illegal_instruction: If True, dump crash report on illegal instruction - """ - # Store for use in error handler - self._stop_on_illegal_instruction = stop_on_illegal_instruction - log.info(f"Starting execution at PC=${self.cpu.PC:04X}") - log.info("Press Ctrl+C to stop") - - # Set up PC callback for BASIC/KERNAL detection if requested - if stop_on_basic or stop_on_kernal_input: - self._setup_pc_callback( - stop_on_basic=stop_on_basic, stop_on_kernal_input=stop_on_kernal_input - ) - - try: - # Execute with cycle counter display - # Use threading for concurrent execution - multiprocessing has pickling issues - # with closures and the GIL doesn't affect us since we're I/O bound on display - # The frame_complete uses multiprocessing.Event for cross-process safety - import threading - import time - import sys as _sys - from mos6502.timing import FrameGovernor - - # Record execution start time for speedup calculation - self._execution_start_time = time.perf_counter() - - # Check if we're in a TTY (terminal) for interactive display - is_tty = _sys.stdout.isatty() - - # All modes: CPU in background thread, display + input in main thread - # This ensures responsive input handling regardless of display mode - pygame_mode = self.display_mode == "pygame" and self.pygame_available - - cpu_done = threading.Event() - cpu_error = None - stop_cpu = threading.Event() - - # Create frame governor for real-time throttling - governor = FrameGovernor( - fps=self.video_timing.refresh_hz, - enabled=throttle - ) - cycles_per_frame = self.video_timing.cycles_per_frame - - def cpu_thread() -> None: - nonlocal cpu_error - try: - # Execute frame-by-frame with optional throttling - # This allows the governor to maintain real-time speed - cycles_remaining = max_cycles - while cycles_remaining > 0 and not stop_cpu.is_set(): - # Execute one frame's worth of cycles - cycles_this_frame = min(cycles_per_frame, cycles_remaining) - try: - self.cpu.execute(cycles=cycles_this_frame) - except errors.CPUCycleExhaustionError: - pass # Normal - frame completed - cycles_remaining -= cycles_this_frame - - # Throttle to real-time (governor.throttle() returns - # immediately if throttling is disabled) - governor.throttle() - except Exception as e: - cpu_error = e - finally: - cpu_done.set() - - # Start CPU thread - cpu_thread_obj = threading.Thread(target=cpu_thread, daemon=True) - cpu_thread_obj.start() - - # Set up terminal input (works for all modes) - terminal_input_available = False - old_settings = None - try: - import select - import termios - import tty - if _sys.stdin.isatty(): - old_settings = termios.tcgetattr(_sys.stdin) - tty.setcbreak(_sys.stdin.fileno()) - terminal_input_available = True - except ImportError: - pass # Terminal input not available (Windows, etc.) - - # Main thread handles display + input - try: - last_terminal_render = time.perf_counter() - TERMINAL_RENDER_INTERVAL = 0.1 # 100ms between terminal renders - - while not cpu_done.is_set(): - # Check stop conditions - if stop_on_basic and self.basic_ready: - break - if stop_on_kernal_input and self.kernal_waiting_for_input: - break - - # Handle terminal keyboard input (all modes) - if terminal_input_available: - import select - if select.select([_sys.stdin], [], [], 0)[0]: - char = _sys.stdin.read(1) - if self._handle_terminal_input(char): - break # Ctrl+C pressed - - # Process any pending key releases (non-blocking) - self._process_pending_key_releases() - - # Pump pygame events every iteration (outside of draw loop) - if pygame_mode: - self._pump_pygame_events() - self._process_pygame_key_buffer() - - # Mode-specific rendering - # Use half frame time as timeout - ensures we check twice per frame - # even if CPU is slow, while not wasting CPU on excessive polling - frame_timeout = 0.5 / self.video_timing.refresh_hz # ~10ms PAL, ~8ms NTSC - - if pygame_mode: - # Render when VIC has a new frame ready (both pygame and terminal) - if self.vic.frame_complete.is_set(): - self._render_pygame() - else: - # Tiny sleep to prevent busy-spinning when no frame ready - time.sleep(0.001) # 1ms - else: - # Terminal/headless: render when VIC has a new frame ready - if self.vic.frame_complete.is_set(): - self.vic.frame_complete.clear() - self._check_pc_region() - self._render_terminal() - else: - time.sleep(0.001) # 1ms - - finally: - # Signal CPU thread to stop and wait for it - stop_cpu.set() - cpu_done.set() - cpu_thread_obj.join(timeout=0.5) - - # Stop drive thread if running in threaded mode - if self.drive_enabled and getattr(self, 'drive_threaded', False): - if self.drive8 is not None and hasattr(self.drive8, 'stop_thread'): - self.drive8.stop_thread() - - # Restore terminal settings if we changed them - if old_settings is not None: - import termios - termios.tcsetattr(_sys.stdin, termios.TCSADRAIN, old_settings) - - # Cleanup pygame if used - if pygame_mode: - try: - import pygame - pygame.quit() - except Exception: - pass - - # Log governor stats if throttling was enabled - if throttle: - stats = governor.stats() - log.info(f"Governor stats: {stats['frame_count']} frames, " - f"avg sleep {stats['avg_sleep_per_frame']*1000:.1f}ms/frame, " - f"dropped {stats['frames_dropped']}") - - # Re-raise CPU thread exception if any - if cpu_error: - raise cpu_error - - except StopIteration as e: - # PC callback requested stop (e.g., BASIC is ready or KERNAL waiting for input) - log.info(f"Execution stopped at PC=${self.cpu.PC:04X} ({e})") - except errors.CPUCycleExhaustionError as e: - log.info(f"CPU execution completed: {e}") - except errors.CPUBreakError as e: - log.info(f"Program terminated (BRK at PC=${self.cpu.PC:04X})") - except (KeyboardInterrupt, errors.QuitRequestError) as e: - if isinstance(e, errors.QuitRequestError): - log.info(f"\nExecution stopped: {e}") - else: - log.info("\nExecution interrupted by user") - log.info(f"PC=${self.cpu.PC:04X}, Cycles={self.cpu.cycles_executed}") - except (errors.IllegalCPUInstructionError, RuntimeError) as e: - # Check if we should dump crash report and stop (vs raising) - if getattr(self, '_stop_on_illegal_instruction', False) and isinstance(e, errors.IllegalCPUInstructionError): - self.dump_crash_report(exception=e) - # Don't re-raise - just stop execution cleanly - else: - log.exception(f"Execution error at PC=${self.cpu.PC:04X}") - # Show context around error - try: - pc_val = int(self.cpu.PC) - self.show_disassembly(max(0, pc_val - 10), num_instructions=20) - self.dump_memory(max(0, pc_val - 16), min(0xFFFF, pc_val + 16)) - except (IndexError, KeyError, ValueError): - log.exception("Could not display context") - raise - finally: - # Record execution end time for speedup calculation - import time - self._execution_end_time = time.perf_counter() - # Clean up PC callback - self._clear_pc_callback() - # Show screen buffer on termination - self.show_screen() - # Clean up drive subprocess if running - self.cleanup() - - def dump_memory(self, start: int, end: int, bytes_per_line: int = 16) -> None: - """Dump memory contents for debugging. - - Arguments: - start: Starting address - end: Ending address - bytes_per_line: Number of bytes to display per line - """ - print(f"\nMemory dump ${start:04X}-${end:04X}:") - print(" ", end="") - for i in range(bytes_per_line): - print(f" {i:02X}", end="") - print() - - for addr in range(start, end + 1, bytes_per_line): - print(f"{addr:04X}: ", end="") - for offset in range(bytes_per_line): - if addr + offset <= end: - byte_val = self.cpu.ram[addr + offset] - print(f" {byte_val:02X}", end="") - else: - print(" ", end="") - print() - - def dump_crash_report(self, exception: Exception = None) -> None: - """Dump comprehensive crash report for illegal instruction or other CPU errors. - - Arguments: - exception: The exception that triggered the crash (optional) - """ - print("\n" + "=" * 70) - print("CRASH REPORT - Illegal Instruction") - print("=" * 70) - - # Show exception info - if exception: - print(f"\nException: {type(exception).__name__}: {exception}") - - # CPU state - pc = int(self.cpu.PC) - opcode = self.cpu.ram[pc] - print(f"\nCPU State at crash:") - print(f" PC: ${pc:04X} (opcode: ${opcode:02X})") - print(f" A: ${self.cpu.A:02X}") - print(f" X: ${self.cpu.X:02X}") - print(f" Y: ${self.cpu.Y:02X}") - print(f" S: ${self.cpu.S & 0xFF:02X} (stack pointer)") - print(f" P: ${self.cpu._flags.value:02X} (N={int(self.cpu.N)} V={int(self.cpu.V)} B={int(self.cpu.B)} D={int(self.cpu.D)} I={int(self.cpu.I)} Z={int(self.cpu.Z)} C={int(self.cpu.C)})") - print(f" Cycles: {self.cpu.cycles_executed:,}") - - # Memory region - region = "RAM" - if 0xA000 <= pc <= 0xBFFF and (self.memory.read(1) & 0x03): - region = "BASIC ROM" - elif 0xD000 <= pc <= 0xDFFF: - region = "I/O" if (self.memory.read(1) & 0x04) else "CHAR ROM" - elif 0xE000 <= pc <= 0xFFFF and (self.memory.read(1) & 0x02): - region = "KERNAL ROM" - print(f" Region: {region}") - - # Stack contents (show 16 bytes from current SP) - sp = self.cpu.S & 0xFF - print(f"\nStack (${sp:02X} -> $FF):") - stack_addr = 0x0100 + sp - print(" ", end="") - for i in range(16): - print(f" {i:02X}", end="") - print() - for row_start in range(stack_addr, 0x0200, 16): - if row_start >= 0x0200: - break - print(f" {row_start:04X}:", end="") - for offset in range(16): - addr = row_start + offset - if addr < 0x0200: - print(f" {self.cpu.ram[addr]:02X}", end="") - else: - print(" ", end="") - print() - - # Disassembly around crash - print(f"\nDisassembly around PC ${pc:04X}:") - try: - start_addr = max(0, pc - 16) - self.show_disassembly(start_addr, num_instructions=20) - except Exception as e: - print(f" Could not disassemble: {e}") - - # Memory around PC - print(f"\nMemory around PC ${pc:04X}:") - mem_start = max(0, pc - 32) - mem_end = min(0xFFFF, pc + 32) - self.dump_memory(mem_start, mem_end) - - # Zero page (important for 6502) - print("\nZero page ($00-$FF):") - self.dump_memory(0x00, 0xFF) - - # Key C64 memory locations - print("\nKey C64 Memory Locations:") - print(f" $00 (DDR): ${self.cpu.ram[0x00]:02X}") - print(f" $01 (Bank): ${self.cpu.ram[0x01]:02X}") - print(f" $90 (KERNAL ST): ${self.cpu.ram[0x90]:02X}") - print(f" $9D (Direct): ${self.cpu.ram[0x9D]:02X}") - print(f" $2B-$2C (TXTTAB):${self.cpu.ram[0x2B]:02X}{self.cpu.ram[0x2C]:02X}") - print(f" $2D-$2E (VARTAB):${self.cpu.ram[0x2D]:02X}{self.cpu.ram[0x2E]:02X}") - - # Vectors - print("\nInterrupt Vectors:") - nmi = self.cpu.ram[0xFFFA] | (self.cpu.ram[0xFFFB] << 8) - reset = self.cpu.ram[0xFFFC] | (self.cpu.ram[0xFFFD] << 8) - irq = self.cpu.ram[0xFFFE] | (self.cpu.ram[0xFFFF] << 8) - print(f" NMI: ${nmi:04X}") - print(f" RESET: ${reset:04X}") - print(f" IRQ: ${irq:04X}") - - print("\n" + "=" * 70) - print("END CRASH REPORT") - print("=" * 70 + "\n") - - def get_speed_stats(self) -> Optional[dict]: - """Calculate CPU execution speed statistics. - - Returns: - Dictionary with speed stats, or None if timing data not available: - - elapsed_seconds: Wall-clock time elapsed - - cycles_executed: Total CPU cycles executed - - cycles_per_second: Actual execution rate (lifetime average) - - rolling_cycles_per_second: Rolling average over last 10 seconds (if available) - - real_cpu_freq: Real C64 CPU frequency for this chip - - speedup: Ratio of actual speed to real hardware speed (lifetime) - - rolling_speedup: Ratio based on rolling average (if available) - - chip_name: VIC-II chip name (6569, 6567R8, etc.) - """ - if self._execution_start_time is None: - return None - - import time - end_time = self._execution_end_time or time.perf_counter() - elapsed = end_time - self._execution_start_time - - if elapsed <= 0: - return None - - cycles_executed = self.cpu.cycles_executed - cycles_per_second = cycles_executed / elapsed - real_cpu_freq = self.video_timing.cpu_freq - speedup = cycles_per_second / real_cpu_freq - - result = { - "elapsed_seconds": elapsed, - "cycles_executed": cycles_executed, - "cycles_per_second": cycles_per_second, - "real_cpu_freq": real_cpu_freq, - "speedup": speedup, - "chip_name": self.video_timing.chip_name, - } - - # Add rolling average if we have samples - if self._speed_samples: - rolling_cps = sum(self._speed_samples) / len(self._speed_samples) - result["rolling_cycles_per_second"] = rolling_cps - result["rolling_speedup"] = rolling_cps / real_cpu_freq - - return result - - def dump_registers(self) -> None: - """Dump CPU register state.""" - print(f"\nCPU Registers:") - print(f" PC: ${self.cpu.PC:04X}") - print(f" A: ${self.cpu.A:02X} ({self.cpu.A})") - print(f" X: ${self.cpu.X:02X} ({self.cpu.X})") - print(f" Y: ${self.cpu.Y:02X} ({self.cpu.Y})") - print(f" S: ${self.cpu.S:04X}") - print(f" Flags: C={self.cpu.C} Z={self.cpu.Z} I={self.cpu.I} " - f"D={self.cpu.D} B={self.cpu.B} V={self.cpu.V} N={self.cpu.N}") - print(f" Cycles executed: {self.cpu.cycles_executed}") - - # Show speed stats if available - stats = self.get_speed_stats() - if stats: - chip = stats['chip_name'] - region = "PAL" if chip == "6569" else "NTSC" - actual_mhz = stats['cycles_per_second'] / 1e6 - print(f" Speed: {stats['cycles_per_second']:,.0f} ({actual_mhz:.3f}MHz) cycles/sec " - f"({stats['speedup']:.1%} of {region} ({chip}) C64 @ " - f"{stats['real_cpu_freq']/1e6:.3f}MHz)") - - def disassemble_at(self, address: int, num_instructions: int = 10) -> list[str]: - """Disassemble instructions starting at address. - - Arguments: - address: Starting address - num_instructions: Number of instructions to disassemble - - Returns: - List of disassembly strings - """ - from mos6502 import instructions - - lines = [] - current_addr = address - - for _ in range(num_instructions): - if current_addr > 0xFFFF: - break - - opcode = self.cpu.ram[current_addr] - - # First try InstructionSet.map (has full metadata) - if opcode in instructions.InstructionSet.map: - inst_info = instructions.InstructionSet.map[opcode] - # Convert bytes/cycles to int (they might be strings in the map) - try: - num_bytes = int(inst_info.get("bytes", 1)) - except (ValueError, TypeError): - num_bytes = 1 - - # Extract mnemonic from assembler string (e.g., "LDX #{oper}" -> "LDX") - assembler = inst_info.get("assembler", "???") - mnemonic = assembler.split()[0] if assembler != "???" else "???" - mode = inst_info.get("addressing", "") - - # If not in map, try OPCODE_LOOKUP (just has opcode objects) - elif opcode in instructions.OPCODE_LOOKUP: - opcode_obj = instructions.OPCODE_LOOKUP[opcode] - # Extract mnemonic from function name (e.g., "sei_implied_0x78" -> "SEI") - func_name = opcode_obj.function - mnemonic = func_name.split("_")[0].upper() - - # Guess number of bytes from function name - if "implied" in func_name or "accumulator" in func_name: - num_bytes = 1 - elif "relative" in func_name or "immediate" in func_name or "zeropage" in func_name: - num_bytes = 2 - else: - num_bytes = 3 - - mode = "implied" - - else: - # Unknown/illegal opcode - hex_str = f"{opcode:02X}" - line = f"${current_addr:04X}: {hex_str} ??? ; ILLEGAL/UNKNOWN ${opcode:02X}" - lines.append(line) - current_addr += 1 - continue - - # Build hex dump - hex_bytes = [f"{self.cpu.ram[current_addr + i]:02X}" - for i in range(min(num_bytes, 3))] - hex_str = " ".join(hex_bytes).ljust(8) - - # Build operand display - if num_bytes == 1: - operand_str = "" - elif num_bytes == 2: - operand = self.cpu.ram[current_addr + 1] - operand_str = f" ${operand:02X}" - elif num_bytes == 3: - lo = self.cpu.ram[current_addr + 1] - hi = self.cpu.ram[current_addr + 2] - operand = (hi << 8) | lo - operand_str = f" ${operand:04X}" - else: - operand_str = "" - - # Mark illegal opcodes - if mnemonic == "???": - line = f"${current_addr:04X}: {hex_str} {mnemonic} ; ILLEGAL ${opcode:02X}" - else: - line = f"${current_addr:04X}: {hex_str} {mnemonic}{operand_str} ; {mode}" - lines.append(line) - current_addr += num_bytes - - return lines - - def show_disassembly(self, address: int, num_instructions: int = 10) -> None: - """Display disassembly at address.""" - print(f"\nDisassembly at ${address:04X}:") - print("-" * 60) - for line in self.disassemble_at(address, num_instructions): - print(line) - - def petscii_to_ascii(self, petscii_code: int) -> str: - """Convert PETSCII code to displayable ASCII character. - - Arguments: - petscii_code: PETSCII character code (0-255) - - Returns: - ASCII character or representation - """ - # Basic PETSCII to ASCII conversion (simplified) - # Uppercase letters (PETSCII 65-90 = ASCII 65-90) - if 65 <= petscii_code <= 90: - return chr(petscii_code) - # Lowercase letters (PETSCII 97-122 = ASCII 97-122) - if 97 <= petscii_code <= 122: - return chr(petscii_code) - # Digits (PETSCII 48-57 = ASCII 48-57) - if 48 <= petscii_code <= 57: - return chr(petscii_code) - # Space - if petscii_code == 32: - return " " - # Common punctuation - punctuation = { - 33: "!", 34: '"', 35: "#", 36: "$", 37: "%", 38: "&", 39: "'", - 40: "(", 41: ")", 42: "*", 43: "+", 44: ",", 45: "-", 46: ".", 47: "/", - 58: ":", 59: ";", 60: "<", 61: "=", 62: ">", 63: "?", 64: "@", - 91: "[", 93: "]", 95: "_" - } - if petscii_code in punctuation: - return punctuation[petscii_code] - # Screen codes 1-26 map to letters A-Z (reverse video in C64) - if 1 <= petscii_code <= 26: - return chr(64 + petscii_code) # Convert to uppercase letter - # Default: show as '.' for unprintable - return "." - - def show_screen(self) -> None: - """Display the C64 screen (40x25 characters from screen RAM at $0400).""" - screen_start = 0x0400 - screen_end = 0x07E7 - cols = 40 - rows = 25 - - print("\n" + "=" * 42) - print(" C64 SCREEN") - print("=" * 42) - - for row in range(rows): - line = "" - for col in range(cols): - addr = screen_start + (row * cols) + col - if addr <= screen_end: - petscii = int(self.cpu.ram[addr]) - line += self.petscii_to_ascii(petscii) - else: - line += " " - # Only print non-empty lines - if line.strip(): - print(line.rstrip()) - else: - print() - - print("=" * 42) - - def _render_terminal(self) -> None: - """Render C64 screen to terminal with dirty region optimization.""" - import sys as _sys - - screen_start = 0x0400 - cols = 40 - rows = 25 - - # Header row offset (4 lines: title, speed, separator, top border) - header_offset = 4 - - # Check if we need a full redraw - needs_full = self.dirty_tracker.needs_full_redraw() - - if needs_full: - # Full screen redraw - _sys.stdout.write("\033[2J\033[H") # Clear screen and move to top - - # Render C64 screen (40x25 characters) - _sys.stdout.write("=" * 42 + "\n") - _sys.stdout.write(" C64 SCREEN\n") - _sys.stdout.write("=" * 42 + "\n") - - for row in range(rows): - line = "" - for col in range(cols): - addr = screen_start + (row * cols) + col - petscii = int(self.cpu.ram[addr]) - line += self.petscii_to_ascii(petscii) - _sys.stdout.write(line + "\n") - - _sys.stdout.write("=" * 42 + "\n") - - elif self.dirty_tracker.has_changes(): - # Incremental update - only redraw dirty cells - dirty_cells = self.dirty_tracker.get_dirty_cells() - for row, col in dirty_cells: - # Move cursor to the cell position - # Terminal rows are 1-indexed, add header offset - term_row = row + header_offset + 1 - term_col = col + 1 - _sys.stdout.write(f"\033[{term_row};{term_col}H") - - # Get and render the character - addr = screen_start + (row * cols) + col - petscii = int(self.cpu.ram[addr]) - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(char) - - # Always update status line (at row 29: 3 header + 25 screen + 1 border) - # Add extra row if drive is attached - status_row = header_offset + rows + 2 - - # Move to status line and update it - _sys.stdout.write(f"\033[{status_row};1H") - status = self._format_cpu_status("C64") - # Clear line and write status - _sys.stdout.write("\033[K" + status) - - # Add drive status line if drive is attached - drive_status = self._format_drive_status() - if drive_status: - _sys.stdout.write(f"\n\033[K" + drive_status) - - _sys.stdout.flush() - - # Clear dirty flags after rendering - self.dirty_tracker.clear() - - def _render_terminal_debug(self) -> None: - """Render C64 screen and CPU state to terminal (for pygame mode debug). - - This version uses ANSI escape codes to update in place without scrolling. - It shows the screen, CPU registers, and current instruction. - """ - import sys as _sys - - screen_start = 0x0400 - cols = 40 - rows = 25 - - # Get colors - bg_color = self.vic.regs[0x21] & 0x0F - bg_ansi = c64_to_ansi_bg(bg_color) - border_color = self.vic.regs[0x20] & 0x0F - border_ansi = c64_to_ansi_bg(border_color) - - # Clear screen and move to top - _sys.stdout.write("\033[2J\033[H") - - # Border and title - _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\n") - _sys.stdout.write(border_ansi + " " + ANSI_RESET + - " C64 (pygame mode) " + - border_ansi + " " * 24 + ANSI_RESET + "\n") - _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\n") - - # Screen content with colors - for row in range(rows): - # Left border - _sys.stdout.write(border_ansi + " " + ANSI_RESET) - - last_fg = -1 - for col in range(cols): - screen_addr = screen_start + (row * cols) + col - color_addr = row * cols + col - petscii = int(self.cpu.ram[screen_addr]) - fg_color = self.memory.ram_color[color_addr] & 0x0F - - # Check for reverse video (screen codes 128-255) - if petscii >= 128: - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(c64_to_ansi_bg(fg_color) + - c64_to_ansi_fg(bg_color) + char) - last_fg = -1 - else: - if fg_color != last_fg: - _sys.stdout.write(bg_ansi + c64_to_ansi_fg(fg_color)) - last_fg = fg_color - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(char) - - # Right border - _sys.stdout.write(ANSI_RESET + border_ansi + " " + ANSI_RESET + "\n") - - # Bottom border - _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\n") - - # C64 CPU status line - status = self._format_cpu_status("C64") - _sys.stdout.write(status + "\n") - - # Drive status line (if drive is attached) - drive_status = self._format_drive_status() - if drive_status: - _sys.stdout.write(drive_status + "\n") - - _sys.stdout.flush() - - def _render_terminal_repl(self) -> None: - """Render C64 screen to terminal for REPL mode with color support. - - This version uses \r\n for line endings to work correctly in cbreak mode. - Colors are rendered using ANSI 256-color escape codes. - """ - import sys as _sys - - # Record speed sample for rolling average (once per second) - self._record_speed_sample() - - screen_start = 0x0400 - cols = 40 - rows = 25 - - # Get background color from VIC register $D021 - bg_color = self.vic.regs[0x21] & 0x0F - bg_ansi = c64_to_ansi_bg(bg_color) - - # Get border color from VIC register $D020 - border_color = self.vic.regs[0x20] & 0x0F - border_ansi = c64_to_ansi_bg(border_color) - - # Header row offset (4 lines: title, speed, separator, top border) - header_offset = 4 - - # Check if we need a full redraw - needs_full = self.dirty_tracker.needs_full_redraw() - - if needs_full: - # Full screen redraw - _sys.stdout.write("\033[2J\033[H") # Clear screen and move to top - - # Terminal header (not part of C64 display) - title_line = f"C64 REPL (Ctrl+C to exit) - {self.get_video_standard()} ({self.video_chip})" - stats = self.get_speed_stats() - if stats: - # Prefer rolling average if available, otherwise use lifetime average - cps = stats.get('rolling_cycles_per_second', stats['cycles_per_second']) - speedup = stats.get('rolling_speedup', stats['speedup']) - actual_mhz = cps / 1e6 - real_mhz = stats['real_cpu_freq'] / 1e6 - speed_line = f"{actual_mhz:.3f}MHz ({speedup:.1%} of {real_mhz:.3f}MHz)" - else: - speed_line = "(calculating speed...)" - _sys.stdout.write(f"{title_line}\r\n") - _sys.stdout.write(f"{speed_line}\r\n") - _sys.stdout.write("=" * 44 + "\r\n") - - # C64 screen with border (top border) - _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\r\n") - - for row in range(rows): - # Left border - _sys.stdout.write(border_ansi + " " + ANSI_RESET) - - # Screen content with colors - last_fg = -1 - for col in range(cols): - screen_addr = screen_start + (row * cols) + col - color_addr = row * cols + col - petscii = int(self.cpu.ram[screen_addr]) - fg_color = self.memory.ram_color[color_addr] & 0x0F - - # Check for reverse video (screen codes 128-255) - if petscii >= 128: - # Reverse video: swap fg and bg - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(c64_to_ansi_bg(fg_color) + - c64_to_ansi_fg(bg_color) + char) - last_fg = -1 # Force color reset - else: - # Normal: fg on bg - if fg_color != last_fg: - _sys.stdout.write(bg_ansi + c64_to_ansi_fg(fg_color)) - last_fg = fg_color - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(char) - - # Right border and reset - _sys.stdout.write(ANSI_RESET + border_ansi + " " + ANSI_RESET + "\r\n") - - # Bottom border - _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\r\n") - - elif self.dirty_tracker.has_changes(): - # Incremental update - only redraw dirty cells - dirty_cells = self.dirty_tracker.get_dirty_cells() - for row, col in dirty_cells: - # Move cursor to the cell position - # Terminal rows are 1-indexed, add header offset, +2 for left border - term_row = row + header_offset + 1 - term_col = col + 3 # +2 for border, +1 for 1-indexing - _sys.stdout.write(f"\033[{term_row};{term_col}H") - - # Get screen and color data - screen_addr = screen_start + (row * cols) + col - color_addr = row * cols + col - petscii = int(self.cpu.ram[screen_addr]) - fg_color = self.memory.ram_color[color_addr] & 0x0F - - # Render with color - if petscii >= 128: - # Reverse video - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(c64_to_ansi_bg(fg_color) + - c64_to_ansi_fg(bg_color) + char + ANSI_RESET) - else: - char = self.petscii_to_ascii(petscii) - _sys.stdout.write(bg_ansi + c64_to_ansi_fg(fg_color) + char + ANSI_RESET) - - # Always update status line (at row 30: 3 header + 25 screen + 1 border + 1) - status_row = header_offset + rows + 2 - - # Move to status line and update it - _sys.stdout.write(f"\033[{status_row};1H") - status = self._format_cpu_status("C64") - # Clear line and write status - _sys.stdout.write("\033[K" + status) - - # Add drive status line if drive is attached - drive_status = self._format_drive_status() - if drive_status: - _sys.stdout.write(f"\n\033[K" + drive_status) - - _sys.stdout.flush() - - # Clear dirty flags after rendering - self.dirty_tracker.clear() - - def _handle_pygame_keyboard(self, event, pygame) -> None: - """Handle pygame keyboard events and update CIA1 keyboard matrix. - - Args: - event: Pygame event - pygame: Pygame module - """ - # C64 keyboard matrix mapping: (row, col) - # Reference: https://www.c64-wiki.com/wiki/Keyboard - key_map = { - # Row 0 - pygame.K_BACKSPACE: (0, 0), # DEL/INST - pygame.K_RETURN: (0, 1), # RETURN - pygame.K_KP_ENTER: (0, 1), # RETURN (numeric keypad) - pygame.K_RIGHT: (0, 2), # CRSR → - pygame.K_F7: (0, 3), # F7 - pygame.K_F1: (0, 4), # F1 - pygame.K_F3: (0, 5), # F3 - pygame.K_F5: (0, 6), # F5 - pygame.K_DOWN: (0, 7), # CRSR ↓ - - # Row 1 - pygame.K_3: (1, 0), - pygame.K_w: (1, 1), - pygame.K_a: (1, 2), - pygame.K_4: (1, 3), - pygame.K_z: (1, 4), - pygame.K_s: (1, 5), - pygame.K_e: (1, 6), - pygame.K_LSHIFT: (1, 7), # Left SHIFT - - # Row 2 - pygame.K_5: (2, 0), - pygame.K_r: (2, 1), - pygame.K_d: (2, 2), - pygame.K_6: (2, 3), - pygame.K_c: (2, 4), - pygame.K_f: (2, 5), - pygame.K_t: (2, 6), - pygame.K_x: (2, 7), - - # Row 3 - pygame.K_7: (3, 0), - pygame.K_y: (3, 1), - pygame.K_g: (3, 2), - pygame.K_8: (3, 3), - pygame.K_b: (3, 4), - pygame.K_h: (3, 5), - pygame.K_u: (3, 6), - pygame.K_v: (3, 7), - - # Row 4 - pygame.K_9: (4, 0), - pygame.K_i: (4, 1), - pygame.K_j: (4, 2), - pygame.K_0: (4, 3), - pygame.K_m: (4, 4), - pygame.K_k: (4, 5), - pygame.K_o: (4, 6), - pygame.K_n: (4, 7), - - # Row 5 - pygame.K_PLUS: (5, 0), # + - pygame.K_p: (5, 1), - pygame.K_l: (5, 2), - pygame.K_MINUS: (5, 3), # - - pygame.K_PERIOD: (5, 4), # . - pygame.K_COLON: (5, 5), # : - pygame.K_AT: (5, 6), # @ - pygame.K_COMMA: (5, 7), # , - - # Row 6 - # pygame.K_POUND: (6, 0), # £ (pound symbol on C64, no direct US keyboard equivalent) - pygame.K_QUOTE: (7, 3), # Map to '2' key - SHIFT+2 produces " (double quote) for BASIC strings - pygame.K_ASTERISK: (6, 1), # * - pygame.K_KP_MULTIPLY: (6, 1), # * on numpad - pygame.K_SEMICOLON: (6, 2), # ; - pygame.K_HOME: (6, 3), # HOME/CLR - # pygame.K_CLR: (6, 4), # CLR (combined with HOME) - pygame.K_EQUALS: (6, 5), # = - pygame.K_UP: (6, 6), # ↑ (up arrow, mapped to up key) - pygame.K_SLASH: (6, 7), # / - # US keyboard Shift+number symbols mapped to C64 equivalents - # On US keyboard: Shift+8=*, Shift+9=(, Shift+0=) - # On C64: Shift+8=(, Shift+9=), * is separate key - pygame.K_LEFTPAREN: (3, 3), # ( -> maps to '8' key (C64: Shift+8 = '(') - pygame.K_RIGHTPAREN: (4, 0), # ) -> maps to '9' key (C64: Shift+9 = ')') - - # Row 7 - pygame.K_1: (7, 0), - pygame.K_LEFT: (7, 1), # ← (CRSR left, using arrow key) - pygame.K_LCTRL: (7, 2), # CTRL - pygame.K_2: (7, 3), - pygame.K_QUOTEDBL: (7, 3), # " (double quote) - same as '2' key (SHIFT+2 on C64) - pygame.K_SPACE: (7, 4), # SPACE - # pygame.K_COMMODORE: (7, 5), # C= (no pygame equivalent) - pygame.K_q: (7, 6), - pygame.K_ESCAPE: (7, 7), # RUN/STOP (mapped to ESC) - } - - # Keys that should be held directly (not buffered) - modifiers and control keys - # These affect the state of other keys and need real-time response - direct_keys = { - pygame.K_LSHIFT, pygame.K_RSHIFT, # SHIFT modifiers - pygame.K_LCTRL, pygame.K_RCTRL, # CTRL - pygame.K_ESCAPE, # RUN/STOP - } - - # Keyboard-to-joystick mappings when joystick emulation is enabled - # Numpad: 8=Up, 2=Down, 4=Left, 6=Right, 0=Fire, 5=Fire (alt) - # Cursor keys: Up/Down/Left/Right arrows, Right Ctrl=Fire - joystick_key_map = { - # Numpad - pygame.K_KP8: JOYSTICK_UP, - pygame.K_KP2: JOYSTICK_DOWN, - pygame.K_KP4: JOYSTICK_LEFT, - pygame.K_KP6: JOYSTICK_RIGHT, - pygame.K_KP0: JOYSTICK_FIRE, - pygame.K_KP5: JOYSTICK_FIRE, # Alt fire on numpad center - # Cursor keys (when joystick enabled, these become joystick instead of C64 cursor) - pygame.K_UP: JOYSTICK_UP, - pygame.K_DOWN: JOYSTICK_DOWN, - pygame.K_LEFT: JOYSTICK_LEFT, - pygame.K_RIGHT: JOYSTICK_RIGHT, - pygame.K_RCTRL: JOYSTICK_FIRE, # Right Ctrl = Fire - pygame.K_KP_ENTER: JOYSTICK_FIRE, # Numpad Enter = Fire - } - - if event.type == pygame.KEYDOWN: - # Log all key presses with key code and name - key_name = pygame.key.name(event.key) if hasattr(pygame.key, 'name') else str(event.key) - - # Try to get ASCII representation - try: - ascii_char = chr(event.key) if 32 <= event.key < 127 else f"<{event.key}>" - ascii_code = event.key - except (ValueError, OverflowError): - ascii_char = f"" - ascii_code = event.key - - # Handle Ctrl+V for paste from system clipboard - ctrl_held = bool(event.mod & (pygame.KMOD_LCTRL | pygame.KMOD_RCTRL)) - if ctrl_held and event.key == pygame.K_v: - try: - clipboard_text = pygame.scrap.get(pygame.SCRAP_TEXT) - if clipboard_text: - # Decode bytes to string if needed - if isinstance(clipboard_text, bytes): - clipboard_text = clipboard_text.decode('utf-8', errors='ignore') - # Remove null terminator if present - clipboard_text = clipboard_text.rstrip('\x00') - if clipboard_text: - self._paste_text(clipboard_text) - log.info(f"Pasted {len(clipboard_text)} characters from clipboard") - except Exception as e: - log.warning(f"Paste failed: {e}") - return - - # Handle joystick keys first (if joystick enabled) - if self._joystick_enabled and event.key in joystick_key_map: - direction = joystick_key_map[event.key] - self.set_joystick_direction(direction, True) - if DEBUG_KEYBOARD: - log.info(f"*** JOYSTICK KEYDOWN: key={key_name}, direction=0x{direction:02X} ***") - return # Don't process as keyboard key - - # US keyboard to C64 symbol remapping when SHIFT is held - # US keyboard: Shift+8=*, Shift+9=(, Shift+0=) - # C64 keyboard: Shift+8=(, Shift+9=), * is separate key - # Remap these to produce expected characters on C64 - shift_held = bool(event.mod & (pygame.KMOD_LSHIFT | pygame.KMOD_RSHIFT)) - remapped_key = event.key - remap_needs_shift = False - remap_suppress_shift = False - if shift_held: - if event.key == pygame.K_8: - # US Shift+8 = * → C64 * key (must suppress shift!) - remapped_key = pygame.K_ASTERISK - remap_needs_shift = False - remap_suppress_shift = True # User is holding shift, but we want unshifted * - elif event.key == pygame.K_9: - # US Shift+9 = ( → C64 Shift+8 - remapped_key = pygame.K_8 - remap_needs_shift = True - elif event.key == pygame.K_0: - # US Shift+0 = ) → C64 Shift+9 - remapped_key = pygame.K_9 - remap_needs_shift = True - - if remapped_key in key_map: - row, col = key_map[remapped_key] - - # Track physical key state - self._pygame_keys_currently_pressed.add(event.key) - - # Direct keys (modifiers) are pressed immediately for real-time feel - if event.key in direct_keys: - self.cia1.press_key(row, col) - else: - # Buffer the key for injection with proper timing - # Some keys need SHIFT to produce the expected character on C64: - # - Quote/double-quote: SHIFT+2 on C64 - # - Parentheses: SHIFT+8 for '(', SHIFT+9 for ')' on C64 - # - Remapped shifted number keys (set above) - needs_shift = (remap_needs_shift or - event.key == pygame.K_QUOTE or - event.key == pygame.K_QUOTEDBL or - event.key == pygame.K_LEFTPAREN or - event.key == pygame.K_RIGHTPAREN) - self._buffer_pygame_key(row, col, needs_shift, remap_suppress_shift) - - if DEBUG_KEYBOARD: - petscii_key = self.cia1._get_key_name(row, col) - buffered = "BUFFERED" if event.key not in direct_keys else "DIRECT" - log.info(f"*** KEYDOWN [{buffered}]: pygame='{key_name}' (code={event.key}), ASCII='{ascii_char}' (0x{ascii_code:02X}), matrix=({row},{col}), PETSCII={petscii_key}, buffer_len={len(self._pygame_key_buffer)} ***") - else: - if DEBUG_KEYBOARD: - log.info(f"*** UNMAPPED KEYDOWN: pygame='{key_name}' (code={event.key}), ASCII='{ascii_char}' ***") - - elif event.type == pygame.KEYUP: - # Handle joystick keys first (if joystick enabled) - if self._joystick_enabled and event.key in joystick_key_map: - direction = joystick_key_map[event.key] - self.set_joystick_direction(direction, False) - if DEBUG_KEYBOARD: - key_name = pygame.key.name(event.key) if hasattr(pygame.key, 'name') else str(event.key) - log.info(f"*** JOYSTICK KEYUP: key={key_name}, direction=0x{direction:02X} ***") - return # Don't process as keyboard key - - if event.key in key_map: - row, col = key_map[event.key] - - # Track physical key state - self._pygame_keys_currently_pressed.discard(event.key) - - # Direct keys are released immediately - if event.key in direct_keys: - self.cia1.release_key(row, col) - - # Buffered keys don't need explicit release - buffer handles timing - - if DEBUG_KEYBOARD: - log.info(f"*** KEYUP: pygame key {event.key}, row={row}, col={col} ***") - - def _render_pygame(self) -> None: - """Render C64 screen to pygame window with dirty region optimization.""" - if not self.pygame_available or self.pygame_screen is None: - return - - try: - import pygame - - # Check if we've entered BASIC ROM (for conditional logging) - self._check_pc_region() - - # Handle pygame events (window close, keyboard, mouse, etc.) - for event in pygame.event.get(): - if event.type == pygame.QUIT: - raise errors.QuitRequestError("Window closed") - elif event.type == pygame.KEYDOWN: - if DEBUG_KEYBOARD: - log.info(f"*** PYGAME KEYDOWN EVENT: key={event.key} ***") - self._handle_pygame_keyboard(event, pygame) - elif event.type == pygame.KEYUP: - self._handle_pygame_keyboard(event, pygame) - elif event.type == pygame.MOUSEMOTION: - # Update mouse/paddle/lightpen position - if self._mouse_enabled: - # Mouse mode: relative motion (like 1351 proportional mouse) - self.update_mouse_motion(event.rel[0], event.rel[1]) - elif self._paddle_enabled: - # Paddle mode: absolute position scaled to window - # Mouse X → Paddle 1 (POTX), Mouse Y → Paddle 2 (POTY) - window_size = self.pygame_screen.get_size() - self.update_paddle_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) - elif self._lightpen_enabled: - # Lightpen mode: absolute position to VIC registers - window_size = self.pygame_screen.get_size() - self.update_lightpen_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) - elif event.type == pygame.MOUSEBUTTONDOWN: - # Mouse/paddle/lightpen button pressed - if self._mouse_enabled: - self.set_mouse_button(event.button, True) - elif self._paddle_enabled: - # Left click → Paddle 1 fire, Right click → Paddle 2 fire - self.set_paddle_button(event.button, True) - elif self._lightpen_enabled: - self.set_lightpen_button(event.button, True) - elif event.type == pygame.MOUSEBUTTONUP: - # Mouse/paddle/lightpen button released - if self._mouse_enabled: - self.set_mouse_button(event.button, False) - elif self._paddle_enabled: - self.set_paddle_button(event.button, False) - elif self._lightpen_enabled: - self.set_lightpen_button(event.button, False) - - # Check if VIC has a new frame ready (VBlank) - # VIC takes the snapshot at the exact moment of VBlank for consistency - new_frame = self.vic.frame_complete.is_set() - if new_frame: - self.vic.frame_complete.clear() - self._frame_count = getattr(self, '_frame_count', 0) + 1 - if self._frame_count <= 5 or self._frame_count % 50 == 0: - log.info(f"*** PYGAME: Caught frame {self._frame_count}, cycles={self.cpu.cycles_executed} ***") - - # Create memory wrappers for VIC - # Use VIC's snapshot (taken at VBlank) if available, otherwise live RAM - # The snapshot is only 16KB (one VIC bank), so we need to adjust addresses - class RAMWrapper: - def __init__(wrapper_self, snapshot, snapshot_bank, live_ram): - wrapper_self.snapshot = snapshot - wrapper_self.snapshot_bank = snapshot_bank - wrapper_self.live_ram = live_ram - - def __getitem__(wrapper_self, index): - if wrapper_self.snapshot is not None: - # Convert absolute address to bank-relative offset - # The snapshot covers snapshot_bank to snapshot_bank + 16KB - relative_index = index - wrapper_self.snapshot_bank - if 0 <= relative_index < len(wrapper_self.snapshot): - return wrapper_self.snapshot[relative_index] - # Address outside snapshot bank - fall through to live RAM - return int(wrapper_self.live_ram[index]) - - class ColorRAMWrapper: - def __init__(wrapper_self, snapshot, live_color): - wrapper_self.snapshot = snapshot - wrapper_self.live_color = live_color - - def __getitem__(wrapper_self, index): - if wrapper_self.snapshot is not None: - return wrapper_self.snapshot[index] & 0x0F - return wrapper_self.live_color[index] & 0x0F - - # Initialize glyph cache if needed - if not hasattr(self, '_glyph_cache'): - self._glyph_cache = {} - - vic = self.vic - ram_snapshot = vic.ram_snapshot - ram_snapshot_bank = vic.ram_snapshot_bank - color_snapshot = vic.color_snapshot - - def read_ram(addr): - if ram_snapshot is not None: - rel = addr - ram_snapshot_bank - if 0 <= rel < len(ram_snapshot): - return ram_snapshot[rel] - return int(self.cpu.ram[addr]) - - def read_color(idx): - if color_snapshot is not None: - return color_snapshot[idx] & 0x0F - return self.memory.ram_color[idx] & 0x0F - - surface = self.pygame_surface - - # --- Display-side rendering --- - border_color = vic.regs[0x20] & 0x0F - surface.fill(COLORS[border_color]) - - vic_bank = vic.get_vic_bank() - mem_control = vic.regs[0x18] - screen_base = vic_bank + ((mem_control & 0xF0) >> 4) * 0x0400 - char_bank_offset = ((mem_control & 0x0E) >> 1) * 0x0800 - - ecm = bool(vic.regs[0x11] & 0x40) - bmm = bool(vic.regs[0x11] & 0x20) - den = bool(vic.regs[0x11] & 0x10) - mcm = bool(vic.regs[0x16] & 0x10) - bg_color = vic.regs[0x21] & 0x0F - - hscroll = vic.regs[0x16] & 0x07 - vscroll = vic.regs[0x11] & 0x07 - x_origin = vic.border_left - hscroll - y_origin = vic.border_top - vscroll - - if den and not bmm and not ecm and not mcm: - # Standard text mode - cached glyph rendering - char_rom = vic.char_rom - for row in range(25): - for col in range(40): - cell_addr = screen_base + row * 40 + col - char_code = read_ram(cell_addr) - color = read_color(row * 40 + col) - - reverse = bool(char_code & 0x80) - char_code &= 0x7F - - glyph_addr = (char_code * 8) + char_bank_offset - glyph_addr &= 0x0FFF - glyph = char_rom[glyph_addr : glyph_addr + 8] - - if reverse: - fg, bg = bg_color, color - else: - fg, bg = color, bg_color - - cache_key = (tuple(glyph), fg, bg) - if cache_key not in self._glyph_cache: - glyph_surf = pygame.Surface((8, 8)) - fg_rgb = COLORS[fg] - bg_rgb = COLORS[bg] - for y in range(8): - line = glyph[y] - for x in range(8): - bit = (line >> (7 - x)) & 0x01 - glyph_surf.set_at((x, y), fg_rgb if bit else bg_rgb) - self._glyph_cache[cache_key] = glyph_surf - - base_x = x_origin + col * 8 - base_y = y_origin + row * 8 - surface.blit(self._glyph_cache[cache_key], (base_x, base_y)) - elif den: - # Other modes - fall back to VIC for now - ram_wrapper = RAMWrapper(ram_snapshot, ram_snapshot_bank, self.cpu.ram) - color_wrapper = ColorRAMWrapper(color_snapshot, self.memory.ram_color) - vic.render_frame(surface, ram_wrapper, color_wrapper) - - # Scale and blit to screen - scaled_surface = pygame.transform.scale( - surface, - (vic.total_width * self.scale, vic.total_height * self.scale) - ) - self.pygame_screen.blit(scaled_surface, (0, 0)) - pygame.display.flip() - - # Update window title with speed stats (rate-limited to once per second) - self._update_pygame_title(pygame) - - # Also render terminal repl output (screen with colors) - # Force full redraw since pygame already handled the dirty tracking - # DO NOT REMOVE - useful for debugging pygame mode via terminal - self.dirty_tracker.force_redraw() - self._render_terminal_repl() - - except Exception as e: - log.error(f"Error rendering pygame display: {e}") - - def _render_pygame_only(self) -> None: - """Render C64 screen to pygame window. - - All pygame rendering happens here in the display loop, not in the VIC core. - Uses glyph caching for fast text mode rendering. - """ - if not self.pygame_available or self.pygame_screen is None: - return - - try: - import pygame - import time as _time - - render_start = _time.perf_counter() - - # Check if VIC has a new frame ready (VBlank) - new_frame = self.vic.frame_complete.is_set() - if new_frame: - self.vic.frame_complete.clear() - self._frame_count = getattr(self, '_frame_count', 0) + 1 - - # Initialize glyph cache if needed - if not hasattr(self, '_glyph_cache'): - self._glyph_cache = {} - - # Get RAM snapshot or live RAM - vic = self.vic - ram_snapshot = vic.ram_snapshot - ram_snapshot_bank = vic.ram_snapshot_bank - color_snapshot = vic.color_snapshot - - # Helper to read RAM - def read_ram(addr): - if ram_snapshot is not None: - rel = addr - ram_snapshot_bank - if 0 <= rel < len(ram_snapshot): - return ram_snapshot[rel] - return int(self.cpu.ram[addr]) - - def read_color(idx): - if color_snapshot is not None: - return color_snapshot[idx] & 0x0F - return self.memory.ram_color[idx] & 0x0F - - surface = self.pygame_surface - - # --- Render frame (display-side, no VIC pygame code) --- - - # Border colour - border_color = vic.regs[0x20] & 0x0F - surface.fill(COLORS[border_color]) - - # Get VIC bank from CIA2 - vic_bank = vic.get_vic_bank() - - # Decode $D018 - mem_control = vic.regs[0x18] - screen_base = vic_bank + ((mem_control & 0xF0) >> 4) * 0x0400 - char_bank_offset = ((mem_control & 0x0E) >> 1) * 0x0800 - - # Mode flags - ecm = bool(vic.regs[0x11] & 0x40) - bmm = bool(vic.regs[0x11] & 0x20) - den = bool(vic.regs[0x11] & 0x10) - mcm = bool(vic.regs[0x16] & 0x10) - - bg_color = vic.regs[0x21] & 0x0F - - hscroll = vic.regs[0x16] & 0x07 - vscroll = vic.regs[0x11] & 0x07 - x_origin = vic.border_left - hscroll - y_origin = vic.border_top - vscroll - - if den and not bmm and not ecm and not mcm: - # Standard text mode - use cached glyph rendering - char_rom = vic.char_rom - - for row in range(25): - for col in range(40): - cell_addr = screen_base + row * 40 + col - char_code = read_ram(cell_addr) - - color = read_color(row * 40 + col) - - reverse = bool(char_code & 0x80) - char_code &= 0x7F - - glyph_addr = (char_code * 8) + char_bank_offset - glyph_addr &= 0x0FFF - glyph = char_rom[glyph_addr : glyph_addr + 8] - - if reverse: - fg, bg = bg_color, color - else: - fg, bg = color, bg_color - - # Cache lookup - cache_key = (tuple(glyph), fg, bg) - if cache_key not in self._glyph_cache: - # Render glyph to cache - glyph_surf = pygame.Surface((8, 8)) - fg_rgb = COLORS[fg] - bg_rgb = COLORS[bg] - for y in range(8): - line = glyph[y] - for x in range(8): - bit = (line >> (7 - x)) & 0x01 - glyph_surf.set_at((x, y), fg_rgb if bit else bg_rgb) - self._glyph_cache[cache_key] = glyph_surf - - # Blit cached glyph - base_x = x_origin + col * 8 - base_y = y_origin + row * 8 - surface.blit(self._glyph_cache[cache_key], (base_x, base_y)) - - elif den: - # Other modes - fall back to VIC renderer for now - # TODO: Move bitmap/multicolor/ECM rendering here too - class RAMWrapper: - def __getitem__(wrapper_self, index): - return read_ram(index) - - class ColorWrapper: - def __getitem__(wrapper_self, index): - return read_color(index) - - vic.render_frame(surface, RAMWrapper(), ColorWrapper()) - - # Scale and blit to screen - scaled_surface = pygame.transform.scale( - surface, - (vic.total_width * self.scale, vic.total_height * self.scale) - ) - self.pygame_screen.blit(scaled_surface, (0, 0)) - pygame.display.flip() - - # Log render time periodically - render_time = _time.perf_counter() - render_start - if self._frame_count <= 5 or self._frame_count % 60 == 0: - cache_size = len(self._glyph_cache) - log.critical( - f"*** RENDER: {render_time*1000:.1f}ms " - f"(frame {self._frame_count}, cache={cache_size}) ***" - ) - - except Exception as e: - log.error(f"Error rendering pygame display: {e}") - - def _pump_pygame_events(self) -> None: - """Process pygame events without rendering. - - Called when the frame_complete wait times out to keep the UI - responsive (handle window close, keyboard input, mouse input, etc.). - """ - if not self.pygame_available or self.pygame_screen is None: - return - - try: - import pygame - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - raise errors.QuitRequestError("Window closed") - elif event.type == pygame.KEYDOWN: - if DEBUG_KEYBOARD: - log.info(f"*** PYGAME KEYDOWN EVENT: key={event.key} ***") - self._handle_pygame_keyboard(event, pygame) - elif event.type == pygame.KEYUP: - self._handle_pygame_keyboard(event, pygame) - elif event.type == pygame.MOUSEMOTION: - # Update mouse/paddle/lightpen position - if self._mouse_enabled: - # Mouse mode: relative motion (like 1351 proportional mouse) - self.update_mouse_motion(event.rel[0], event.rel[1]) - elif self._paddle_enabled: - # Paddle mode: absolute position scaled to window - # Mouse X → Paddle 1 (POTX), Mouse Y → Paddle 2 (POTY) - window_size = self.pygame_screen.get_size() - self.update_paddle_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) - elif self._lightpen_enabled: - # Lightpen mode: absolute position to VIC registers - window_size = self.pygame_screen.get_size() - self.update_lightpen_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) - elif event.type == pygame.MOUSEBUTTONDOWN: - # Mouse/paddle/lightpen button pressed - if self._mouse_enabled: - self.set_mouse_button(event.button, True) - elif self._paddle_enabled: - # Left click → Paddle 1 fire, Right click → Paddle 2 fire - self.set_paddle_button(event.button, True) - elif self._lightpen_enabled: - self.set_lightpen_button(event.button, True) - elif event.type == pygame.MOUSEBUTTONUP: - # Mouse/paddle/lightpen button released - if self._mouse_enabled: - self.set_mouse_button(event.button, False) - elif self._paddle_enabled: - self.set_paddle_button(event.button, False) - elif self._lightpen_enabled: - self.set_lightpen_button(event.button, False) - - except errors.QuitRequestError: - raise # Re-raise quit request to propagate up - except Exception as e: - log.error(f"Error pumping pygame events: {e}") - - # ASCII to C64 keyboard matrix mapping - # Maps ASCII characters to (row, col) positions in the C64 keyboard matrix - # Some characters require SHIFT to be pressed - ASCII_TO_MATRIX = { - # Letters (unshifted = uppercase on C64 in default mode) - 'A': (1, 2), 'B': (3, 4), 'C': (2, 4), 'D': (2, 2), 'E': (1, 6), - 'F': (2, 5), 'G': (3, 2), 'H': (3, 5), 'I': (4, 1), 'J': (4, 2), - 'K': (4, 5), 'L': (5, 2), 'M': (4, 4), 'N': (4, 7), 'O': (4, 6), - 'P': (5, 1), 'Q': (7, 6), 'R': (2, 1), 'S': (1, 5), 'T': (2, 6), - 'U': (3, 6), 'V': (3, 7), 'W': (1, 1), 'X': (2, 7), 'Y': (3, 1), - 'Z': (1, 4), - # Lowercase (same matrix position, C64 handles shift mode internally) - 'a': (1, 2), 'b': (3, 4), 'c': (2, 4), 'd': (2, 2), 'e': (1, 6), - 'f': (2, 5), 'g': (3, 2), 'h': (3, 5), 'i': (4, 1), 'j': (4, 2), - 'k': (4, 5), 'l': (5, 2), 'm': (4, 4), 'n': (4, 7), 'o': (4, 6), - 'p': (5, 1), 'q': (7, 6), 'r': (2, 1), 's': (1, 5), 't': (2, 6), - 'u': (3, 6), 'v': (3, 7), 'w': (1, 1), 'x': (2, 7), 'y': (3, 1), - 'z': (1, 4), - # Digits - '0': (4, 3), '1': (7, 0), '2': (7, 3), '3': (1, 0), '4': (1, 3), - '5': (2, 0), '6': (2, 3), '7': (3, 0), '8': (3, 3), '9': (4, 0), - # Special characters - ' ': (7, 4), # SPACE - '\n': (0, 1), # RETURN - '\r': (0, 1), # RETURN - '+': (5, 0), - '-': (5, 3), - '*': (6, 1), - '/': (6, 7), - '=': (6, 5), - '.': (5, 4), - ',': (5, 7), - ':': (5, 5), - ';': (6, 2), - '@': (5, 6), - '#': None, # Requires SHIFT+3 (handled specially) - '$': None, # Requires SHIFT+4 (handled specially) - '%': None, # Requires SHIFT+5 (handled specially) - '(': None, # Requires SHIFT+8 (handled specially) - ')': None, # Requires SHIFT+9 (handled specially) - '"': None, # Requires SHIFT+2 (handled specially) - '!': None, # Requires SHIFT+1 (handled specially) - '?': None, # Requires SHIFT+/ (handled specially) - '<': None, # Requires SHIFT+, (handled specially) - '>': None, # Requires SHIFT+. (handled specially) - } - - # Characters that require SHIFT: (shift_row, shift_col, key_row, key_col) - ASCII_SHIFTED = { - '!': (1, 7, 7, 0), # SHIFT + 1 - '"': (1, 7, 7, 3), # SHIFT + 2 - '#': (1, 7, 1, 0), # SHIFT + 3 - '$': (1, 7, 1, 3), # SHIFT + 4 - '%': (1, 7, 2, 0), # SHIFT + 5 - '&': (1, 7, 2, 3), # SHIFT + 6 - "'": (1, 7, 7, 3), # SHIFT + 2 - produces " (double quote) for BASIC strings - '(': (1, 7, 3, 3), # SHIFT + 8 - ')': (1, 7, 4, 0), # SHIFT + 9 - '?': (1, 7, 6, 7), # SHIFT + / - '<': (1, 7, 5, 7), # SHIFT + , - '>': (1, 7, 5, 4), # SHIFT + . - } - - def ascii_to_key_press(self, char: str) -> tuple: - """Convert ASCII character to C64 key press. - - Arguments: - char: Single ASCII character - - Returns: - Tuple of (needs_shift, row, col) or None if not mappable - """ - if char in self.ASCII_SHIFTED: - shift_row, shift_col, key_row, key_col = self.ASCII_SHIFTED[char] - return (True, key_row, key_col) - elif char in self.ASCII_TO_MATRIX: - pos = self.ASCII_TO_MATRIX[char] - if pos is not None: - return (False, pos[0], pos[1]) - return None - - def type_character(self, char: str, hold_cycles: int = 5000) -> None: - """Simulate typing a character on the C64 keyboard. - - Arguments: - char: Single ASCII character to type - hold_cycles: Number of CPU cycles to hold the key down - """ - key_info = self.ascii_to_key_press(char) - if key_info is None: - log.warning(f"Cannot type character: {repr(char)}") - return - - needs_shift, row, col = key_info - - # Press SHIFT if needed - if needs_shift: - self.cia1.press_key(1, 7) # Left SHIFT - - # Press the key - self.cia1.press_key(row, col) - - # Run CPU for some cycles to let the KERNAL process the keypress - try: - self.cpu.execute(cycles=hold_cycles) - except errors.CPUCycleExhaustionError: - pass - - # Release the key - self.cia1.release_key(row, col) - - # Release SHIFT if it was pressed - if needs_shift: - self.cia1.release_key(1, 7) - - # Run a bit more to ensure key release is processed - try: - self.cpu.execute(cycles=hold_cycles // 2) - except errors.CPUCycleExhaustionError: - pass - - def type_string(self, text: str, hold_cycles: int = 5000) -> None: - """Type a string of characters on the C64 keyboard. - - Arguments: - text: String to type - hold_cycles: Number of CPU cycles to hold each key down - """ - for char in text: - self.type_character(char, hold_cycles) - - # ------------------------------------------------------------------------- - # Mouse Input (1351 proportional mouse emulation) - # ------------------------------------------------------------------------- - # The Commodore 1351 mouse uses: - # - SID POT registers ($D419/$D41A) for position (relative motion, wraps 0-255) - # - Joystick port for buttons (active low): - # - Left button = Fire (bit 4) - # - Right button = Up (bit 0) in some implementations - # Mouse is typically plugged into Port 1 (joystick_1) - - _mouse_enabled: bool = False - _mouse_port: int = 1 # Which joystick port (1 or 2) - _mouse_sensitivity: float = 1.0 # Scale factor for mouse motion - - def enable_mouse(self, enabled: bool = True, port: int = 1, sensitivity: float = 1.0) -> None: - """Enable or disable mouse input. - - Args: - enabled: Whether mouse input is enabled - port: Which joystick port (1 or 2) the mouse is connected to - sensitivity: Scale factor for mouse motion (1.0 = 1:1 mapping) - """ - self._mouse_enabled = enabled - self._mouse_port = port - self._mouse_sensitivity = sensitivity - self.sid.mouse_enabled = enabled - log.info(f"Mouse input {'enabled' if enabled else 'disabled'} on port {port}, sensitivity={sensitivity}") - - def update_mouse_motion(self, delta_x: int, delta_y: int) -> None: - """Update mouse position from motion delta. - - The 1351 mouse sends relative motion that wraps around 0-255. - This method should be called with pygame MOUSEMOTION event rel values. - - Args: - delta_x: Horizontal motion in pixels (positive = right) - delta_y: Vertical motion in pixels (positive = down) - """ - if not self._mouse_enabled: - return - - # Apply sensitivity scaling - scaled_x = int(delta_x * self._mouse_sensitivity) - scaled_y = int(delta_y * self._mouse_sensitivity) - - # Update SID POT registers - self.sid.update_mouse(scaled_x, scaled_y) - - def set_mouse_button(self, button: int, pressed: bool) -> None: - """Set mouse button state. - - Args: - button: Button number (1 = left, 3 = right for pygame) - pressed: True if button is pressed, False if released - """ - if not self._mouse_enabled: - return - - # Get the joystick register for the configured port - if self._mouse_port == 1: - joystick = self.cia1.joystick_1 - else: - joystick = self.cia1.joystick_2 - - # Mouse buttons use active-low logic (0 = pressed, 1 = released) - # Left button (1) = Fire (bit 4) - # Right button (3) = Up (bit 0) - common mapping for 1351 - if button == 1: # Left button - if pressed: - joystick &= ~MOUSE_LEFT_BUTTON # Clear bit (pressed) - else: - joystick |= MOUSE_LEFT_BUTTON # Set bit (released) - elif button == 3: # Right button - if pressed: - joystick &= ~MOUSE_RIGHT_BUTTON # Clear bit (pressed) - else: - joystick |= MOUSE_RIGHT_BUTTON # Set bit (released) - - # Update the joystick state - if self._mouse_port == 1: - self.cia1.joystick_1 = joystick - else: - self.cia1.joystick_2 = joystick - - # ========================================================================= - # Paddle Input Support - # ========================================================================= - # Paddles use the same SID POT registers as the 1351 mouse, but with - # absolute positioning instead of relative motion. - # - Two paddles per port (directly reading the same POT registers) - # - Each paddle has a fire button (directly triggers joystick port bits) - # - Paddle 1: POTX ($D419), fire on bit 2 of joystick port - # - Paddle 2: POTY ($D41A), fire on bit 3 of joystick port - # Fire buttons active-low on the joystick port. - - _paddle_enabled: bool = False - _paddle_port: int = 1 # Which joystick port (1 or 2) - - def enable_paddle(self, enabled: bool = True, port: int = 1) -> None: - """Enable or disable paddle input. - - Args: - enabled: Whether paddle input is enabled - port: Which joystick port (1 or 2) the paddles are connected to - """ - self._paddle_enabled = enabled - self._paddle_port = port - log.info(f"Paddle input {'enabled' if enabled else 'disabled'} on port {port}") - - def update_paddle_position(self, mouse_x: int, mouse_y: int, window_width: int, window_height: int) -> None: - """Update paddle position from absolute mouse position. - - Args: - mouse_x: Mouse X position in window (pixels) - mouse_y: Mouse Y position in window (pixels) - window_width: Window width (pixels) - window_height: Window height (pixels) - """ - if not self._paddle_enabled: - return - - # Scale mouse position to paddle range (0-255) - # Clamp to valid range in case mouse is outside window - paddle_x = max(0, min(255, int((mouse_x / max(1, window_width)) * 255))) - paddle_y = max(0, min(255, int((mouse_y / max(1, window_height)) * 255))) - - # Update SID POT registers - self.sid.set_paddle(paddle_x, paddle_y) - - def set_paddle_button(self, button: int, pressed: bool) -> None: - """Set paddle button state. - - C64 paddle fire buttons use different CIA bits than joystick fire: - - Paddle 1 (X-axis paddle) fire: Bit 2 of CIA port - - Paddle 2 (Y-axis paddle) fire: Bit 3 of CIA port - - Port 1 paddles read from $DC01 (CIA1 Port B) - Port 2 paddles read from $DC00 (CIA1 Port A) - - Reference: https://www.c64-wiki.com/wiki/Paddle - - Args: - button: Mouse button (1 = left → paddle 1 fire, 3 = right → paddle 2 fire) - pressed: True if button is pressed, False if released - """ - if not self._paddle_enabled: - return - - # Get the joystick register for the configured port - if self._paddle_port == 1: - joystick = self.cia1.joystick_1 - else: - joystick = self.cia1.joystick_2 - - # Paddle buttons use active-low logic (0 = pressed, 1 = released) - # Real C64 paddle fire buttons: - # - Paddle 1 (X) fire = Bit 2 (directly wired to control port pin 3) - # - Paddle 2 (Y) fire = Bit 3 (directly wired to control port pin 4) - if button == 1: # Left mouse button = Paddle 1 fire - if pressed: - joystick &= ~PADDLE_1_FIRE # Clear bit 2 (pressed) - else: - joystick |= PADDLE_1_FIRE # Set bit 2 (released) - elif button == 3: # Right mouse button = Paddle 2 fire - if pressed: - joystick &= ~PADDLE_2_FIRE # Clear bit 3 (pressed) - else: - joystick |= PADDLE_2_FIRE # Set bit 3 (released) - - # Update the joystick state - if self._paddle_port == 1: - self.cia1.joystick_1 = joystick - else: - self.cia1.joystick_2 = joystick - - # ========================================================================= - # Lightpen Input Support - # ========================================================================= - # Lightpen uses VIC-II registers for position (not SID POT like mouse/paddle): - # - $D013 (LPX): X coordinate divided by 2 (multiply by 2 to get actual X) - # - $D014 (LPY): Y coordinate (same as sprite Y coordinates) - # - Button triggers joystick fire on port 1 only (bit 4 of $DC01) - # - Can also trigger VIC IRQ bit 3 when position is latched - # - # Lightpen only works on control port 1 (directly wired to VIC). - # Reference: https://www.c64-wiki.com/wiki/Light_pen - - _lightpen_enabled: bool = False - - def enable_lightpen(self, enabled: bool = True) -> None: - """Enable or disable lightpen input. - - Note: Lightpen only works on control port 1 (hardware limitation). - - Args: - enabled: Whether lightpen input is enabled - """ - self._lightpen_enabled = enabled - log.info(f"Lightpen input {'enabled' if enabled else 'disabled'}") - - def update_lightpen_position(self, mouse_x: int, mouse_y: int, - window_width: int, window_height: int) -> None: - """Update lightpen position from mouse position. - - Maps mouse window coordinates to VIC-II lightpen registers. - The VIC stores X/2 in $D013 and Y in $D014, using sprite coordinate space. - - Args: - mouse_x: Mouse X position in window (pixels) - mouse_y: Mouse Y position in window (pixels) - window_width: Window width (pixels) - window_height: Window height (pixels) - """ - if not self._lightpen_enabled: - return - - # VIC-II visible area in sprite coordinates: - # PAL: X = 24-343 (320 pixels), Y = 50-249 (200 pixels) - # We map the window to the visible screen area - - # X coordinate: map window X to sprite X range (24-343), then divide by 2 - # The visible screen is 320 pixels wide, starting at sprite X=24 - sprite_x = 24 + int((mouse_x / max(1, window_width)) * 320) - sprite_x = max(0, min(511, sprite_x)) # Clamp to 9-bit range - lpx = sprite_x // 2 # VIC stores X/2 - - # Y coordinate: map window Y to sprite Y range (50-249) - # The visible screen is 200 pixels tall, starting at sprite Y=50 - sprite_y = 50 + int((mouse_y / max(1, window_height)) * 200) - sprite_y = max(0, min(255, sprite_y)) # Clamp to 8-bit range - - # Update VIC lightpen registers - self.vic.regs[0x13] = lpx & 0xFF - self.vic.regs[0x14] = sprite_y & 0xFF - - def set_lightpen_button(self, button: int, pressed: bool) -> None: - """Set lightpen button state. - - Lightpen button is directly wired to joystick fire on port 1. - Only left mouse button is used (lightpens have one button). - - Reference: https://www.c64-wiki.com/wiki/Light_pen - - Args: - button: Mouse button (1 = left/lightpen button) - pressed: True if button is pressed, False if released - """ - if not self._lightpen_enabled: - return - - # Lightpen only uses port 1 (hardware constraint) - joystick = self.cia1.joystick_1 - - # Lightpen button uses active-low logic (0 = pressed, 1 = released) - # Only left button (1) triggers the lightpen - if button == 1: - if pressed: - joystick &= ~LIGHTPEN_BUTTON # Clear bit (pressed) - else: - joystick |= LIGHTPEN_BUTTON # Set bit (released) - - self.cia1.joystick_1 = joystick - - # ========================================================================= - # Keyboard Joystick Emulation - # ========================================================================= - # Maps keyboard keys to joystick directions and fire button. - # Supports both numpad (8/2/4/6/0) and WASD+Space layouts. - # Default port is 2 since most C64 games expect joystick in port 2. - # - # C64 joystick uses active-low logic (0 = pressed, 1 = released): - # - Bit 0: Up - # - Bit 1: Down - # - Bit 2: Left - # - Bit 3: Right - # - Bit 4: Fire - # Reference: https://www.c64-wiki.com/wiki/Joystick - - _joystick_enabled: bool = False - _joystick_port: int = 2 # Default to port 2 (most common for C64 games) - - def enable_joystick(self, enabled: bool = True, port: int = 2) -> None: - """Enable or disable keyboard joystick emulation. - - When enabled, keyboard keys are mapped to joystick directions: - - Numpad: 8=Up, 2=Down, 4=Left, 6=Right, 0=Fire - - WASD: W=Up, S=Down, A=Left, D=Right, Space=Fire - - Args: - enabled: Whether joystick emulation is enabled - port: Which joystick port to emulate (1 or 2, default: 2) - """ - self._joystick_enabled = enabled - self._joystick_port = port - log.info(f"Keyboard joystick {'enabled' if enabled else 'disabled'} on port {port}") - - def set_joystick_direction(self, direction: int, pressed: bool) -> None: - """Set a joystick direction or fire button state. - - Args: - direction: Direction bit (JOYSTICK_UP, JOYSTICK_DOWN, etc.) - pressed: True if pressed, False if released - """ - if not self._joystick_enabled: - return - - if self._joystick_port == 1: - joystick = self.cia1.joystick_1 - else: - joystick = self.cia1.joystick_2 - - # Active-low logic: 0 = pressed, 1 = released - if pressed: - joystick &= ~direction # Clear bit (pressed) - else: - joystick |= direction # Set bit (released) - - if self._joystick_port == 1: - self.cia1.joystick_1 = joystick - else: - self.cia1.joystick_2 = joystick - - # Pending key releases for non-blocking terminal input - # Each entry is (release_cycles, row, col) - uses CPU cycles, not wall-clock time - _pending_key_releases: list = [] - - # Pygame keyboard buffer for type-ahead - # Stores (row, col, needs_shift) tuples for keys waiting to be injected - _pygame_key_buffer: list = [] - _pygame_keys_currently_pressed: set = set() # Track physical key state - _pygame_current_injection: tuple | None = None # (row, col, needs_shift, start_cycles, released) - - # Timing in CPU cycles (not wall-clock) so keys inject correctly at any emulator speed - # KERNAL scans keyboard once per frame (~17000-20000 cycles). We need to span one scan. - # At ~1MHz: 20000 cycles = ~20ms (one full frame), 2000 cycles = ~2ms - _key_hold_cycles: int = 20000 # Hold key for one full frame, guarantees KERNAL sees it - _key_gap_cycles: int = 2000 # Gap between keys - - def _queue_key_release(self, row: int, col: int) -> bool: - """Queue a key for release after a delay (cycle-based, not wall-clock). - - Uses CPU cycles for timing so key handling works correctly at any - emulator speed (throttled or unthrottled). - - Implements debouncing: if this key already has a pending release, - the keypress is skipped entirely to prevent key repeat issues. - - Args: - row: Keyboard matrix row - col: Keyboard matrix column - - Returns: - True if key was queued, False if skipped due to debouncing - """ - # Check if this key already has a pending release (debounce) - for _, pending_row, pending_col in self._pending_key_releases: - if pending_row == row and pending_col == col: - # Key already pending - skip this press (debounce) - return False - - release_cycles = self.cpu.cycles_executed + self._key_hold_cycles - self._pending_key_releases.append((release_cycles, row, col)) - return True - - def _process_pending_key_releases(self) -> None: - """Process any pending key releases (call from main loop). - - Uses CPU cycles for timing, which scales correctly with emulator speed. - """ - if not self._pending_key_releases: - return - - current_cycles = self.cpu.cycles_executed - still_pending = [] - for release_cycles, row, col in self._pending_key_releases: - if current_cycles >= release_cycles: - self.cia1.release_key(row, col) - else: - still_pending.append((release_cycles, row, col)) - self._pending_key_releases = still_pending - - def _buffer_pygame_key(self, row: int, col: int, needs_shift: bool = False, - suppress_shift: bool = False) -> None: - """Buffer a keypress from pygame for injection into CIA. - - Keys are buffered and injected one at a time with proper timing - to ensure the KERNAL sees every keypress. - - Args: - row: C64 keyboard matrix row - col: C64 keyboard matrix column - needs_shift: If True, press SHIFT along with this key - suppress_shift: If True, release SHIFT while pressing this key - (for when user is holding shift but we want unshifted char) - """ - self._pygame_key_buffer.append((row, col, needs_shift, suppress_shift)) - - def _paste_text(self, text: str) -> None: - """Paste text by buffering keystrokes for each character. - - Converts ASCII/Unicode text to C64 key matrix positions and buffers - them for injection via the pygame key buffer system. - - Args: - text: Text to paste (ASCII characters) - """ - # ASCII character to C64 keyboard matrix mapping: (row, col, needs_shift) - # Based on the C64 keyboard matrix - ascii_to_matrix = { - # Letters (unshifted = uppercase on C64) - 'A': (1, 2, False), 'B': (3, 4, False), 'C': (2, 4, False), - 'D': (2, 2, False), 'E': (1, 6, False), 'F': (2, 5, False), - 'G': (3, 2, False), 'H': (3, 5, False), 'I': (4, 1, False), - 'J': (4, 2, False), 'K': (4, 5, False), 'L': (5, 2, False), - 'M': (4, 4, False), 'N': (4, 7, False), 'O': (4, 6, False), - 'P': (5, 1, False), 'Q': (7, 6, False), 'R': (2, 1, False), - 'S': (1, 5, False), 'T': (2, 6, False), 'U': (3, 6, False), - 'V': (3, 7, False), 'W': (1, 1, False), 'X': (2, 7, False), - 'Y': (3, 1, False), 'Z': (1, 4, False), - # Lowercase -> same as uppercase (C64 types uppercase by default) - 'a': (1, 2, False), 'b': (3, 4, False), 'c': (2, 4, False), - 'd': (2, 2, False), 'e': (1, 6, False), 'f': (2, 5, False), - 'g': (3, 2, False), 'h': (3, 5, False), 'i': (4, 1, False), - 'j': (4, 2, False), 'k': (4, 5, False), 'l': (5, 2, False), - 'm': (4, 4, False), 'n': (4, 7, False), 'o': (4, 6, False), - 'p': (5, 1, False), 'q': (7, 6, False), 'r': (2, 1, False), - 's': (1, 5, False), 't': (2, 6, False), 'u': (3, 6, False), - 'v': (3, 7, False), 'w': (1, 1, False), 'x': (2, 7, False), - 'y': (3, 1, False), 'z': (1, 4, False), - # Numbers - '1': (7, 0, False), '2': (7, 3, False), '3': (1, 0, False), - '4': (1, 3, False), '5': (2, 0, False), '6': (2, 3, False), - '7': (3, 0, False), '8': (3, 3, False), '9': (4, 0, False), - '0': (4, 3, False), - # Symbols (unshifted) - ' ': (7, 4, False), # SPACE - '\r': (0, 1, False), # RETURN - '\n': (0, 1, False), # RETURN (newline) - ',': (5, 7, False), # COMMA - '.': (5, 4, False), # PERIOD - ':': (5, 5, False), # COLON - ';': (6, 2, False), # SEMICOLON - '/': (6, 7, False), # SLASH - '=': (6, 5, False), # EQUALS - '+': (5, 0, False), # PLUS - '-': (5, 3, False), # MINUS - '*': (6, 1, False), # ASTERISK - '@': (5, 6, False), # AT - # Shifted symbols - '!': (7, 0, True), # SHIFT+1 - '"': (7, 3, True), # SHIFT+2 (quote) - '#': (1, 0, True), # SHIFT+3 - '$': (1, 3, True), # SHIFT+4 - '%': (2, 0, True), # SHIFT+5 - '&': (2, 3, True), # SHIFT+6 - "'": (3, 0, True), # SHIFT+7 (apostrophe) - '(': (3, 3, True), # SHIFT+8 - ')': (4, 0, True), # SHIFT+9 - '<': (5, 7, True), # SHIFT+COMMA - '>': (5, 4, True), # SHIFT+PERIOD - '?': (6, 7, True), # SHIFT+SLASH - } - - for char in text: - if char in ascii_to_matrix: - row, col, needs_shift = ascii_to_matrix[char] - self._buffer_pygame_key(row, col, needs_shift, suppress_shift=False) - else: - # Skip unmapped characters - log.debug(f"Paste: skipping unmapped character '{char}' (0x{ord(char):02X})") - - def _process_pygame_key_buffer(self) -> None: - """Process the pygame key buffer, injecting keys into CIA. - - Called from main loop. Handles timing for key injection using CPU cycles - so keys inject faster when emulator runs faster than real-time. - - Press key, hold for ~20000 cycles (~20ms at 1MHz) - - Release key, wait ~5000 cycles gap - - Repeat for next key - """ - current_cycles = self.cpu.cycles_executed - - # If we have a current injection in progress, check timing - if self._pygame_current_injection is not None: - row, col, needs_shift, suppress_shift, start_cycles, released = self._pygame_current_injection - - if not released: - # Key is being held - check if hold cycles elapsed - if current_cycles - start_cycles >= self._key_hold_cycles: - # Release the key - self.cia1.release_key(row, col) - if needs_shift: - self.cia1.release_key(1, 7) # Release SHIFT - if suppress_shift: - # Re-press shift if user is still holding it physically - import pygame - if pygame.K_LSHIFT in self._pygame_keys_currently_pressed or \ - pygame.K_RSHIFT in self._pygame_keys_currently_pressed: - self.cia1.press_key(1, 7) - # Mark as released, record release cycle - self._pygame_current_injection = (row, col, needs_shift, suppress_shift, current_cycles, True) - else: - # Key is released - check if gap cycles elapsed - if current_cycles - start_cycles >= self._key_gap_cycles: - # Done with this key, clear injection - self._pygame_current_injection = None - - # If no current injection and buffer has keys, start next one - if self._pygame_current_injection is None and self._pygame_key_buffer: - row, col, needs_shift, suppress_shift = self._pygame_key_buffer.pop(0) - # Press the key - if needs_shift: - self.cia1.press_key(1, 7) # Press SHIFT - if suppress_shift: - self.cia1.release_key(1, 7) # Release SHIFT even if physically held - self.cia1.press_key(row, col) - # Record injection start (not released yet) - self._pygame_current_injection = (row, col, needs_shift, suppress_shift, current_cycles, False) - - def _handle_terminal_input(self, char: str) -> bool: - """Handle a single character of terminal input. - - Converts ASCII input to C64 key presses. Handles escape sequences - for arrow keys and other special keys. Uses non-blocking key release - queue to avoid stalling the main loop. - - Arguments: - char: Single character from terminal input - - Returns: - True if Ctrl+C was pressed (should exit), False otherwise - """ - import sys as _sys - - # Handle special keys - if char == '\x03': # Ctrl+C - return True - elif char == '\x1b': # Escape sequence - # Read the rest of the escape sequence - try: - import select - if select.select([_sys.stdin], [], [], 0.1)[0]: - seq = _sys.stdin.read(2) - if seq == '[A': # Up arrow -> CRSR UP (SHIFT + CRSR DOWN) - self.cia1.press_key(1, 7) # SHIFT - self.cia1.press_key(0, 7) # CRSR DOWN - self._queue_key_release(0, 7) - self._queue_key_release(1, 7) - elif seq == '[B': # Down arrow -> CRSR DOWN - self.cia1.press_key(0, 7) - self._queue_key_release(0, 7) - elif seq == '[C': # Right arrow -> CRSR RIGHT - self.cia1.press_key(0, 2) - self._queue_key_release(0, 2) - elif seq == '[D': # Left arrow -> CRSR LEFT (SHIFT + CRSR RIGHT) - self.cia1.press_key(1, 7) # SHIFT - self.cia1.press_key(0, 2) # CRSR RIGHT - self._queue_key_release(0, 2) - self._queue_key_release(1, 7) - elif seq == '[3': # Delete key (followed by ~) - if select.select([_sys.stdin], [], [], 0.1)[0]: - _sys.stdin.read(1) # Consume the ~ - self.cia1.press_key(0, 0) # DEL - self._queue_key_release(0, 0) - except ImportError: - pass # select not available - elif char == '\x7f': # Backspace - # Check if key is already pending (debounce) - already_pending = any(r == 0 and c == 0 for _, r, c in self._pending_key_releases) - if not already_pending: - self.cia1.press_key(0, 0) # DEL - self._queue_key_release(0, 0) - else: - # Regular character - press key and queue release - key_info = self.ascii_to_key_press(char) - if key_info: - needs_shift, row, col = key_info - # Check if key is already pending (debounce) - already_pending = any(r == row and c == col for _, r, c in self._pending_key_releases) - if not already_pending: - if needs_shift: - self.cia1.press_key(1, 7) # SHIFT - self.cia1.press_key(row, col) - self._queue_key_release(row, col) - if needs_shift: - self._queue_key_release(1, 7) - - return False - - def run_repl(self, max_cycles: int = INFINITE_CYCLES) -> None: - """Run the C64 in REPL mode with terminal input. - - This mode renders the C64 screen to the terminal and accepts - keyboard input, converting ASCII to C64 key presses. - - Note: REPL mode requires Unix-like terminal support (termios, tty). - It is not available on Windows. - - Arguments: - max_cycles: Maximum cycles to run (default: infinite) - """ - import sys as _sys - import threading - import time - - try: - import select - import termios - import tty - except ImportError: - log.error("REPL mode requires Unix-like terminal support (termios, tty).") - log.error("This mode is not available on Windows. Use --display terminal instead.") - return - - if not _sys.stdin.isatty(): - log.error("REPL mode requires an interactive terminal (stdin must be a TTY).") - return - - # Save terminal settings - old_settings = termios.tcgetattr(_sys.stdin) - - # Shared state between threads - stop_event = threading.Event() - cpu_error = None - - def cpu_thread(): - """Run CPU in background thread.""" - nonlocal cpu_error - try: - self.cpu.execute(cycles=max_cycles) - except errors.CPUCycleExhaustionError: - pass # Normal termination when max_cycles reached - except Exception as e: - cpu_error = e - log.error(f"CPU thread error: {e}") - finally: - stop_event.set() - - try: - # Put terminal in cbreak mode (character-at-a-time, no echo) - tty.setcbreak(_sys.stdin.fileno()) - - # Record execution start time for speedup calculation - self._execution_start_time = time.perf_counter() - - # Start CPU in background thread - cpu_thread_obj = threading.Thread(target=cpu_thread, daemon=True) - cpu_thread_obj.start() - - # Main loop: handle input and render - # Limit render rate to match video timing (PAL ~50Hz, NTSC ~60Hz) - last_render = 0 - render_interval = self.video_timing.render_interval - - while not stop_event.is_set(): - # Check for input (non-blocking with short timeout) - if select.select([_sys.stdin], [], [], 0.01)[0]: - char = _sys.stdin.read(1) - if self._handle_terminal_input(char): - break # Ctrl+C pressed - - # Process any pending key releases - self._process_pending_key_releases() - - # Render at limited rate to avoid terminal buffer overflow - now = time.time() - if now - last_render >= render_interval: - self.dirty_tracker.force_redraw() - self._render_terminal_repl() - last_render = now - - except (KeyboardInterrupt, errors.QuitRequestError): - pass - finally: - # Record execution end time for speedup calculation - self._execution_end_time = time.perf_counter() - - stop_event.set() - # Wait for CPU thread to finish - cpu_thread_obj.join(timeout=0.5) - - # Restore terminal settings - termios.tcsetattr(_sys.stdin, termios.TCSADRAIN, old_settings) - - # Clear screen and show final state - _sys.stdout.write("\033[2J\033[H") - _sys.stdout.flush() - self.show_screen() - - # Clean up drive subprocess if running - self.cleanup() - - # Re-raise CPU error if any - if cpu_error: - raise cpu_error - - def disassemble_instruction(self, address: int) -> str: - """Disassemble a single instruction at the given address. - - Arguments: - address: Address of instruction to disassemble - - Returns: - Formatted disassembly string for the instruction - - """ - from mos6502 import instructions - - opcode = self.cpu.ram[address] - - # First try InstructionSet.map (has full metadata) - if opcode in instructions.InstructionSet.map: - inst_info = instructions.InstructionSet.map[opcode] - # Convert bytes/cycles to int (they might be strings in the map) - try: - num_bytes = int(inst_info.get("bytes", 1)) - except (ValueError, TypeError): - num_bytes = 1 - - # Extract mnemonic from assembler string (e.g., "LDX #{oper}" -> "LDX") - assembler = inst_info.get("assembler", "???") - mnemonic = assembler.split()[0] if assembler != "???" else "???" - mode = inst_info.get("addressing", "") - - # If not in map, try OPCODE_LOOKUP (just has opcode objects) - elif opcode in instructions.OPCODE_LOOKUP: - opcode_obj = instructions.OPCODE_LOOKUP[opcode] - # Extract mnemonic from function name (e.g., "sei_implied_0x78" -> "SEI") - func_name = opcode_obj.function - mnemonic = func_name.split("_")[0].upper() - - # Guess number of bytes from function name - if "implied" in func_name or "accumulator" in func_name: - num_bytes = 1 - elif "relative" in func_name or "immediate" in func_name or "zeropage" in func_name: - num_bytes = 2 - else: - num_bytes = 3 - - mode = "implied" - - else: - # Unknown/illegal opcode - return formatted string - return f"{opcode:02X} ??? ; ILLEGAL ${opcode:02X}" - - # Build hex dump - hex_bytes = [f"{self.cpu.ram[address + i]:02X}" - for i in range(min(num_bytes, 3))] - hex_str = " ".join(hex_bytes).ljust(8) - - # Build operand display - if num_bytes == 1: - operand_str = "" - elif num_bytes == 2: - operand = self.cpu.ram[address + 1] - operand_str = f" ${operand:02X}" - elif num_bytes == 3: - lo = self.cpu.ram[address + 1] - hi = self.cpu.ram[address + 2] - operand = (hi << 8) | lo - operand_str = f" ${operand:04X}" - else: - operand_str = "" - - # Return formatted string without the address prefix - if mnemonic == "???": - return f"{hex_str} {mnemonic} ; ILLEGAL ${opcode:02X}" - else: - return f"{hex_str} {mnemonic}{operand_str} ; {mode}" - - -def main() -> int | None: - """Run the C64 emulator CLI.""" - import argparse - - parser = argparse.ArgumentParser(description="Commodore 64 Emulator") - C64.args(parser) - args = parser.parse_args() - - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Set debug flags from command-line arguments - global DEBUG_CIA, DEBUG_VIC, DEBUG_JIFFY, DEBUG_KEYBOARD - global DEBUG_SCREEN, DEBUG_CURSOR, DEBUG_KERNAL, DEBUG_BASIC - if args.debug_cia: - DEBUG_CIA = True - if args.debug_vic: - DEBUG_VIC = True - if args.debug_jiffy: - DEBUG_JIFFY = True - if args.debug_keyboard: - DEBUG_KEYBOARD = True - if args.debug_screen: - DEBUG_SCREEN = True - if args.debug_cursor: - DEBUG_CURSOR = True - if args.debug_kernal: - DEBUG_KERNAL = True - if args.debug_basic: - DEBUG_BASIC = True - - try: - # Initialize C64 - c64 = C64(rom_dir=args.rom_dir, display_mode=args.display, scale=args.scale, enable_irq=not args.no_irq, video_chip=args.video_chip) - log.info(f"VIC-II chip: {c64.video_chip} ({c64.video_timing.refresh_hz:.2f}Hz, {c64.video_timing.cpu_freq/1e6:.3f}MHz)") - - # Start with minimal logging - will auto-enable when BASIC ROM is entered - # This avoids flooding the console during KERNAL boot - logging.getLogger("mos6502").setLevel(logging.CRITICAL) - logging.getLogger("mos6502.cpu.flags").setLevel(logging.CRITICAL) - log.info("CPU logging will enable when BASIC ROM is entered") - - # Load cartridge BEFORE reset if specified - # Cartridge ROMs affect memory banking and may provide auto-start vectors - if args.cartridge: - c64.load_cartridge(args.cartridge, args.cartridge_type) - log.info(f"Cartridge type: {c64.cartridge_type}") - - # Attach disk drive only if --disk is specified (and not disabled) - # This avoids the overhead of drive emulation when not needed - disk_path = getattr(args, 'disk', None) - if disk_path and not getattr(args, 'no_drive', False): - drive_rom = getattr(args, 'drive_rom', None) - drive_runner = getattr(args, 'drive_runner', 'threaded') - if c64.attach_drive(drive_rom_path=drive_rom, disk_path=disk_path, runner=drive_runner): - log.info(f"Disk inserted: {disk_path.name} (runner: {drive_runner})") - else: - log.info("No 1541 ROM found - disk drive disabled") - - # ROMs are automatically loaded in C64.__init__() - # Reset CPU AFTER ROMs are loaded so reset vector can be read correctly - # This implements the complete 6502 reset sequence: - # - Clears RAM to 0xFF - # - Sets S = 0xFD - # - Sets P = 0x34 (I=1, interrupts disabled) - # - Reads reset vector from $FFFC/$FFFD - # - Sets PC to vector value - # - Consumes 7 cycles - c64.cpu.reset() - - # Initialize pygame AFTER VIC is created - if args.display == "pygame": - if not c64.init_pygame_display(): - log.warning("Pygame initialization failed, falling back to terminal mode") - c64.display_mode = "terminal" - - # Enable mouse if requested (only works with pygame) - if getattr(args, 'mouse', False): - if c64.display_mode == "pygame": - c64.enable_mouse( - enabled=True, - port=getattr(args, 'mouse_port', 1), - sensitivity=getattr(args, 'mouse_sensitivity', 1.0) - ) - else: - log.warning("Mouse input only available in pygame mode") - - # Enable paddle if requested (only works with pygame, mutually exclusive with mouse) - if getattr(args, 'paddle', False): - if getattr(args, 'mouse', False): - log.warning("Cannot enable both mouse and paddle - paddle takes precedence") - c64._mouse_enabled = False - if c64.display_mode == "pygame": - c64.enable_paddle( - enabled=True, - port=getattr(args, 'paddle_port', 1) - ) - else: - log.warning("Paddle input only available in pygame mode") - - # Enable lightpen if requested (only works with pygame, mutually exclusive with mouse/paddle) - if getattr(args, 'lightpen', False): - if getattr(args, 'mouse', False) or getattr(args, 'paddle', False): - log.warning("Cannot enable lightpen with mouse or paddle - lightpen takes precedence") - c64._mouse_enabled = False - c64._paddle_enabled = False - if c64.display_mode == "pygame": - c64.enable_lightpen(enabled=True) - else: - log.warning("Lightpen input only available in pygame mode") - - # Enable keyboard joystick emulation if requested - if getattr(args, 'joystick', False): - c64.enable_joystick( - enabled=True, - port=getattr(args, 'joystick_port', 2) - ) - - # In no-roms mode, log that we're running headless - if args.no_roms: - log.info(f"Running in headless mode (no ROMs)") - - # Load program AFTER reset (so it doesn't get cleared) - program_end_addr = None - if args.program: - actual_load_addr, program_end_addr = c64.load_program( - args.program, load_address=args.load_address - ) - # In no-roms mode, set PC to the program's load address - if args.no_roms: - c64.cpu.PC = actual_load_addr - log.info(f"PC set to ${c64.cpu.PC:04X}") - elif args.no_roms: - log.error("--no-roms requires --program to be specified") - return 1 - - # If disassemble mode, show disassembly and exit - if args.disassemble is not None: - c64.show_disassembly(args.disassemble, num_instructions=args.num_instructions) - if args.dump_mem: - c64.dump_memory(args.dump_mem[0], args.dump_mem[1]) - return 0 - - # Dump initial state if verbose - if args.verbose: - c64.dump_registers() - if args.dump_mem: - c64.dump_memory(args.dump_mem[0], args.dump_mem[1]) - - # Handle --run: boot to BASIC, load program, inject RUN command, then continue - if args.run and args.program and not args.no_roms: - log.info("Auto-run enabled: booting until KERNAL waits for input...") - # Boot until KERNAL is waiting for keyboard input (more reliable than stop_on_basic) - c64.run(max_cycles=args.max_cycles, stop_on_kernal_input=True, throttle=args.throttle, stop_on_illegal_instruction=args.stop_on_illegal_instruction) - # Re-load the program AFTER boot (KERNAL clears $0801 during boot) - # This is the same as a real C64's LOAD command - actual_load_addr, program_end_addr = c64.load_program( - args.program, load_address=args.load_address - ) - log.info(f"Program loaded at ${actual_load_addr:04X}-${program_end_addr - 1:04X}") - # Update BASIC pointers so RUN knows where the program ends - if program_end_addr is not None: - c64.update_basic_pointers(program_end_addr) - # Inject "RUN" + RETURN into keyboard buffer - c64.inject_keyboard_buffer("RUN\r") - log.info("RUN command injected, continuing execution...") - # Continue running - if args.display == "repl": - c64.run_repl(max_cycles=args.max_cycles) - else: - c64.run(max_cycles=args.max_cycles, throttle=args.throttle, stop_on_illegal_instruction=args.stop_on_illegal_instruction) - elif args.display == "repl": - # REPL mode: interactive terminal with keyboard input - c64.run_repl(max_cycles=args.max_cycles) - else: - c64.run(max_cycles=args.max_cycles, stop_on_basic=args.stop_on_basic, throttle=args.throttle, stop_on_illegal_instruction=args.stop_on_illegal_instruction) - - # Dump final state - c64.dump_registers() - - if args.dump_mem: - c64.dump_memory(args.dump_mem[0], args.dump_mem[1]) - - logging.getLogger("mos6502").setLevel(logging.INFO) - - # Show screen if requested - if args.show_screen: - c64.show_screen() - - return 0 - - except Exception as e: - if args.verbose: - log.exception("Error") - else: - log.error(f"Error: {e}") # noqa: TRY400 - return 1 - -if __name__ == "__main__": - sys.exit(main()) +# Drive module is optional (not available on Pico) +try: + from c64.drive import ( + Drive1541, + IECBus, + D64Image, + ThreadedDrive1541, + ThreadedIECBus, + MultiprocessDrive1541, + MultiprocessIECBus, + SharedIECState, + ) +except ImportError: + # Drive module not available + Drive1541 = None + IECBus = None + D64Image = None + ThreadedDrive1541 = None + ThreadedIECBus = None + MultiprocessDrive1541 = None + MultiprocessIECBus = None + SharedIECState = None + +__all__ = [ + # Main class + "C64", + # Cartridge-related + "Cartridge", + "CartridgeTestResults", + "StaticROMCartridge", + "ErrorCartridge", + "CARTRIDGE_TYPES", + "create_cartridge", + "create_error_cartridge_rom", + # Memory constants + "ROML_START", + "ROML_END", + "ROML_SIZE", + "ROMH_START", + "ROMH_END", + "IO1_START", + "IO1_END", + "IO2_START", + "IO2_END", + "BASIC_ROM_START", + "BASIC_ROM_END", + "BASIC_ROM_SIZE", + "KERNAL_ROM_START", + "KERNAL_ROM_END", + "KERNAL_ROM_SIZE", + "CHAR_ROM_START", + "CHAR_ROM_END", + "CHAR_ROM_SIZE", + "VIC_START", + "VIC_END", + "SID_START", + "SID_END", + "COLOR_RAM_START", + "COLOR_RAM_END", + "CIA1_START", + "CIA1_END", + "CIA2_START", + "CIA2_END", + "BASIC_PROGRAM_START", + # Chip emulation + "CIA1", + "CIA2", + "SID", + "C64VIC", + "C64Memory", + # Video + "VideoTiming", + "ScreenDirtyTracker", + "COLORS", + "c64_to_ansi_fg", + "c64_to_ansi_bg", + "ANSI_RESET", + "PAL", + "NTSC", + # Input constants + "PADDLE_1_FIRE", + "PADDLE_2_FIRE", + "MOUSE_LEFT_BUTTON", + "MOUSE_RIGHT_BUTTON", + "LIGHTPEN_BUTTON", + "JOYSTICK_UP", + "JOYSTICK_DOWN", + "JOYSTICK_LEFT", + "JOYSTICK_RIGHT", + "JOYSTICK_FIRE", + # Drive + "Drive1541", + "IECBus", + "D64Image", + "ThreadedDrive1541", + "ThreadedIECBus", + "MultiprocessDrive1541", + "MultiprocessIECBus", + "SharedIECState", +] diff --git a/systems/c64/benchmark.py b/systems/c64/benchmark.py index b805ee7..b173890 100755 --- a/systems/c64/benchmark.py +++ b/systems/c64/benchmark.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """Benchmark C64 CPU execution speed.""" import argparse -import logging +from mos6502.compat import logging import sys import time -from pathlib import Path -from typing import Optional +from mos6502.compat import Path +from mos6502.compat import Optional, Tuple from c64 import C64, PAL, NTSC, c64_to_ansi_fg, c64_to_ansi_bg, ANSI_RESET from mos6502 import errors @@ -24,7 +24,7 @@ logging.getLogger(logger_name).setLevel(logging.CRITICAL) -def benchmark_c64(rom_dir: str, max_cycles: int, video_chip: str = "6569", verbose_cycles: bool = False, throttle: bool = False) -> tuple[float, int]: +def benchmark_c64(rom_dir: str, max_cycles: int, video_chip: str = "6569", verbose_cycles: bool = False, throttle: bool = False) -> Tuple[float, int]: """Benchmark C64 execution. Args: @@ -74,7 +74,7 @@ def benchmark_c64(rom_dir: str, max_cycles: int, video_chip: str = "6569", verbo return stats["elapsed_seconds"], stats["cycles_executed"] -def benchmark_boot(rom_dir: str, video_chip: str = "6569", debug: bool = False, verbose_cycles: bool = False, throttle: bool = False) -> tuple[float, int, int, str]: +def benchmark_boot(rom_dir: str, video_chip: str = "6569", debug: bool = False, verbose_cycles: bool = False, throttle: bool = False) -> Tuple[float, int, int, str]: """Benchmark C64 boot time until BASIC is ready. Args: @@ -213,7 +213,7 @@ def benchmark_disk_load( drive_rom: Optional[Path] = None, sync_drive: bool = False, throttle: bool = False -) -> tuple[float, int, str]: +) -> Tuple[float, int, str]: """Benchmark loading from disk (LOAD"*",8,1 then RUN). Args: diff --git a/systems/c64/c64.py b/systems/c64/c64.py new file mode 100644 index 0000000..60d0d63 --- /dev/null +++ b/systems/c64/c64.py @@ -0,0 +1,913 @@ +"""Commodore 64 Emulator - Core class.""" + +from mos6502.compat import logging +from mos6502.compat import Path +from mos6502.compat import Optional, Union, List, Dict, Tuple + +from mos6502 import CPU, CPUVariant, errors, add_cpu_arguments +from mos6502.core import INFINITE_CYCLES +from mos6502.memory import Byte, Word + +# Cartridge support is optional (not available on Pico due to kwargs limitations) +try: + from c64.cartridges import ( + Cartridge, + CartridgeTestResults, + StaticROMCartridge, + ErrorCartridge, + CARTRIDGE_TYPES, + create_cartridge, + create_error_cartridge_rom, + ROML_START, + ROML_END, + ROML_SIZE, + ROMH_START, + ROMH_END, + IO1_START, + IO1_END, + IO2_START, + IO2_END, + ) + _CARTRIDGES_AVAILABLE = True +except (ImportError, TypeError): + # Stub values for when cartridges module is not available + _CARTRIDGES_AVAILABLE = False + Cartridge = None + CartridgeTestResults = None + StaticROMCartridge = None + ErrorCartridge = None + CARTRIDGE_TYPES = {} + create_cartridge = None + create_error_cartridge_rom = None + ROML_START = 0x8000 + ROML_END = 0x9FFF + ROML_SIZE = 0x2000 + ROMH_START = 0xA000 + ROMH_END = 0xBFFF + IO1_START = 0xDE00 + IO1_END = 0xDEFF + IO2_START = 0xDF00 + IO2_END = 0xDFFF +from c64.cia1 import ( + CIA1, + PADDLE_1_FIRE, + PADDLE_2_FIRE, + MOUSE_LEFT_BUTTON, + MOUSE_RIGHT_BUTTON, + LIGHTPEN_BUTTON, + JOYSTICK_UP, + JOYSTICK_DOWN, + JOYSTICK_LEFT, + JOYSTICK_RIGHT, + JOYSTICK_FIRE, +) +from c64.cia2 import CIA2 +from c64.sid import SID +from c64.vic import ( + VideoTiming, + ScreenDirtyTracker, + C64VIC, + COLORS, + c64_to_ansi_fg, + c64_to_ansi_bg, + ANSI_RESET, + PAL, + NTSC, + VIC_6569, + VIC_6567R8, + VIC_6567R56A, +) +from c64.memory import ( + C64Memory, + BASIC_ROM_START, + BASIC_ROM_END, + BASIC_ROM_SIZE, + KERNAL_ROM_START, + KERNAL_ROM_END, + KERNAL_ROM_SIZE, + CHAR_ROM_START, + CHAR_ROM_END, + CHAR_ROM_SIZE, + VIC_START, + VIC_END, + SID_START, + SID_END, + COLOR_RAM_START, + COLOR_RAM_END, + CIA1_START, + CIA1_END, + CIA2_START, + CIA2_END, + BASIC_PROGRAM_START, +) + +# Import mixins +from c64.mixins import ( + C64DriveMixin, + C64CartridgeMixin, + C64DisplayMixin, + C64KeyboardMixin, + C64InputDevicesMixin, + C64DebugMixin, + C64ProgramMixin, + C64RunnerMixin, +) + +# Note: On MicroPython frozen modules, logging.basicConfig() can't be called +# with kwargs. Since the compat.py stub ignores log levels anyway, we skip it. +try: + logging.basicConfig(level=logging.CRITICAL) +except TypeError: + pass # MicroPython frozen module - kwargs not supported +log = logging.getLogger("c64") + +# Debug flags - set to True to enable verbose logging +DEBUG_CIA = False # CIA register reads/writes +DEBUG_VIC = False # VIC register operations +DEBUG_JIFFY = False # Jiffy clock updates +DEBUG_KEYBOARD = False # Keyboard events +DEBUG_SCREEN = False # Screen memory writes +DEBUG_CURSOR = False # Cursor position variables +DEBUG_KERNAL = False # Enable CPU logging when entering KERNAL ROM +DEBUG_BASIC = False # Enable CPU logging when entering BASIC ROM + + +class C64( + C64DriveMixin, + C64CartridgeMixin, + C64DisplayMixin, + C64KeyboardMixin, + C64InputDevicesMixin, + C64DebugMixin, + C64ProgramMixin, + C64RunnerMixin, +): + """Commodore 64 Emulator. + + Memory Map: + $0000-$0001: I/O Ports (6510 specific - stubbed) + $0002-$9FFF: RAM (40KB usable) + $A000-$BFFF: BASIC ROM (8KB) + $C000-$CFFF: RAM (4KB) + $D000-$DFFF: I/O and Color RAM (4KB - stubbed) + $E000-$FFFF: KERNAL ROM (8KB) + """ + + # BASIC memory pointers (zero page) + # These must be updated when loading a BASIC program for RUN to work + TXTTAB = 0x2B # Start of BASIC program text (2 bytes, little-endian) + VARTAB = 0x2D # Start of BASIC variables / end of program (2 bytes) + ARYTAB = 0x2F # Start of BASIC arrays (2 bytes) + STREND = 0x31 # End of BASIC arrays / bottom of strings (2 bytes) + + # KERNAL keyboard buffer (for injecting typed commands) + KEYBOARD_BUFFER = 0x0277 # 10-byte keyboard buffer + KEYBOARD_BUFFER_SIZE = 0x00C6 # Number of characters in buffer + + # Reset vector location + RESET_VECTOR_ADDR = 0xFFFC + + # Cartridge memory regions + ROML_START = 0x8000 # Low ROM (8KB) - active when EXROM=0 + ROML_END = 0x9FFF + ROML_SIZE = 0x2000 # 8KB + + ROMH_START = 0xA000 # High ROM (8KB) - overlaps BASIC ROM area + ROMH_END = 0xBFFF + ROMH_SIZE = 0x2000 # 8KB + + # Cartridge auto-start signature location + CART_SIGNATURE_ADDR = 0x8004 # "CBM80" signature for auto-start + + # CRT hardware type names (from VICE specification) + # Type 0 is the only one we currently support + # Source: http://rr.c64.org/wiki/CRT_ID + CRT_HARDWARE_TYPES = { + 0: "Normal cartridge", + 1: "Action Replay", + 2: "KCS Power Cartridge", + 3: "Final Cartridge III", + 4: "Simons Basic", + 5: "Ocean type 1", + 6: "Expert Cartridge", + 7: "Fun Play, Power Play", + 8: "Super Games", + 9: "Atomic Power", + 10: "Epyx Fastload", + 11: "Westermann Learning", + 12: "Rex Utility", + 13: "Final Cartridge I", + 14: "Magic Formel", + 15: "C64 Game System, System 3", + 16: "WarpSpeed", + 17: "Dinamic", + 18: "Zaxxon, Super Zaxxon (SEGA)", + 19: "Magic Desk, Domark, HES Australia", + 20: "Super Snapshot V5", + 21: "Comal-80", + 22: "Structured Basic", + 23: "Ross", + 24: "Dela EP64", + 25: "Dela EP7x8", + 26: "Dela EP256", + 27: "Rex EP256", + 28: "Mikro Assembler", + 29: "Final Cartridge Plus", + 30: "Action Replay 4", + 31: "StarDOS", + 32: "EasyFlash", + 33: "EasyFlash X-Bank", + 34: "Capture", + 35: "Action Replay 3", + 36: "Retro Replay, Nordic Replay", + 37: "MMC64", + 38: "MMC Replay", + 39: "IDE64", + 40: "Super Snapshot V4", + 41: "IEEE488", + 42: "Game Killer", + 43: "Prophet 64", + 44: "Exos", + 45: "Freeze Frame", + 46: "Freeze Machine", + 47: "Snapshot64", + 48: "Super Explode V5", + 49: "Magic Voice", + 50: "Action Replay 2", + 51: "MACH 5", + 52: "Diashow Maker", + 53: "Pagefox", + 54: "Kingsoft Business Basic", + 55: "Silver Rock 128", + 56: "Formel 64", + 57: "RGCD", + 58: "RR-Net MK3", + 59: "Easy Calc Result", + 60: "GMod2", + 61: "MAX BASIC", + 62: "GMod3", + 63: "ZIPP-CODE 48", + 64: "Blackbox V8", + 65: "Blackbox V3", + 66: "Blackbox V4", + 67: "REX RAM Floppy", + 68: "BIS Plus", + 69: "SD Box", + 70: "MultiMAX", + 71: "Blackbox V9", + 72: "LT Kernal", + 73: "CMD RAMlink", + 74: "Drean (H.E.R.O. bootleg)", + 75: "IEEE Flash 64", + 76: "Turtle Graphics II", + 77: "Freeze Frame MK2", + 78: "Partner 64", + 79: "Hyper-BASIC MK2", + 80: "Universal Cartridge 1", + 81: "Universal Cartridge 1.5", + 82: "Universal Cartridge 2", + 83: "BMP Data Turbo 2000", + 84: "Profi-DOS", + 85: "Magic Desk 16", + } + + + @classmethod + def args(cls, parser) -> None: + """Add C64-specific command-line arguments to an argument parser. + + Args: + parser: An argparse.ArgumentParser instance + """ + # Core emulator options + core_group = parser.add_argument_group("Core Options") + core_group.add_argument( + "--rom-dir", + type=Path, + default=Path("./roms"), + help="Directory containing ROM files (default: ./roms)", + ) + core_group.add_argument( + "--display", + type=str, + choices=["terminal", "pygame", "headless", "repl"], + default="pygame", + help="Display mode: pygame (default, graphical window), terminal (ASCII art), headless (no display), or repl (interactive terminal with keyboard input). Automatically falls back to terminal if pygame unavailable.", + ) + core_group.add_argument( + "--scale", + type=int, + default=2, + help="Pygame window scaling factor (default: 2 = 640x400)", + ) + core_group.add_argument( + "--video-chip", + type=str.upper, + choices=["6569", "6567R8", "6567R56A", "PAL", "NTSC"], + default="6569", + help="VIC-II chip variant: 6569 (PAL, default), 6567R8 (NTSC 1984+), " + "6567R56A (old NTSC 1982-1984). PAL/NTSC are aliases for 6569/6567R8.", + ) + core_group.add_argument( + "--no-irq", + action="store_true", + help="Disable IRQ injection (for debugging; system will hang waiting for IRQs)", + ) + core_group.add_argument( + "--throttle", + action="store_true", + default=True, + help="Throttle emulation to real-time speed (default: enabled)", + ) + core_group.add_argument( + "--no-throttle", + action="store_false", + dest="throttle", + help="Disable throttling - run at maximum speed (for benchmarks)", + ) + core_group.add_argument( + "--mouse", + action="store_true", + help="Enable 1351 proportional mouse emulation (pygame mode only)", + ) + core_group.add_argument( + "--mouse-port", + type=int, + choices=[1, 2], + default=1, + help="Joystick port for mouse (1 or 2, default: 1)", + ) + core_group.add_argument( + "--mouse-sensitivity", + type=float, + default=1.0, + help="Mouse sensitivity multiplier (default: 1.0)", + ) + core_group.add_argument( + "--paddle", + action="store_true", + help="Enable paddle emulation using mouse position (pygame mode only)", + ) + core_group.add_argument( + "--paddle-port", + type=int, + choices=[1, 2], + default=1, + help="Joystick port for paddles (1 or 2, default: 1)", + ) + core_group.add_argument( + "--lightpen", + action="store_true", + help="Enable lightpen emulation using mouse position (pygame mode only, port 1)", + ) + core_group.add_argument( + "--joystick", + action="store_true", + help="Enable keyboard joystick emulation (numpad or WASD+Space)", + ) + # CPU variant selection - uses core library function + add_cpu_arguments(parser, group_name="CPU Options") + + core_group.add_argument( + "--joystick-port", + type=int, + choices=[1, 2], + default=2, + help="Joystick port for keyboard emulation (1 or 2, default: 2)", + ) + + # Program loading options + program_group = parser.add_argument_group("Program Loading") + program_group.add_argument( + "--program", + type=Path, + help="Program file to load and run (.prg, .bin, etc.)", + ) + program_group.add_argument( + "--load-address", + type=lambda x: int(x, 0), + help="Override load address (hex or decimal, e.g., 0x0801 or 2049)", + ) + program_group.add_argument( + "--no-roms", + action="store_true", + help="Run without C64 ROMs (for testing standalone programs)", + ) + program_group.add_argument( + "--run", + action="store_true", + help="Auto-run program after loading (injects RUN command after boot)", + ) + + # Cartridge options + cart_group = parser.add_argument_group("Cartridge Options") + cart_group.add_argument( + "--cartridge", + type=Path, + help="Cartridge file to load (.crt format or raw binary)", + ) + cart_group.add_argument( + "--cartridge-type", + type=str.lower, + choices=["auto", "8k", "16k", "ultimax"], + default="auto", + help="Cartridge type: auto (detect from file), 8k, 16k, or ultimax (default: auto)", + ) + + # Disk drive options + drive_group = parser.add_argument_group("Disk Drive Options") + drive_group.add_argument( + "--disk", + type=Path, + help="D64 disk image to insert into drive 8", + ) + drive_group.add_argument( + "--drive-rom", + type=Path, + help="1541 DOS ROM file (default: /1541.rom)", + ) + drive_group.add_argument( + "--no-drive", + action="store_true", + help="Disable 1541 drive emulation (faster boot, no disk access)", + ) + drive_group.add_argument( + "--drive-runner", + choices=["synchronous", "threaded", "multiprocess"], + default="synchronous", + dest="drive_runner", + help="Drive emulation runner: synchronous (default), threaded, or multiprocess", + ) + + # Execution control options + exec_group = parser.add_argument_group("Execution Control") + exec_group.add_argument( + "--max-cycles", + type=int, + default=INFINITE_CYCLES, + help="Maximum CPU cycles to execute (default: infinite)", + ) + exec_group.add_argument( + "--stop-on-basic", + action="store_true", + help="Stop execution when BASIC prompt is ready (useful for benchmarking boot time)", + ) + exec_group.add_argument( + "--stop-on-illegal-instruction", + action="store_true", + help="Stop and dump crash report when an illegal instruction is executed", + ) + + # Output options + output_group = parser.add_argument_group("Output Options") + output_group.add_argument( + "--dump-mem", + nargs=2, + metavar=("START", "END"), + type=lambda x: int(x, 0), + help="Dump memory region after execution (hex or decimal addresses)", + ) + output_group.add_argument( + "--show-screen", + action="store_true", + help="Display screen RAM after execution (40x25 character display)", + ) + output_group.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose logging", + ) + + # Disassembly options + disasm_group = parser.add_argument_group("Disassembly") + disasm_group.add_argument( + "--disassemble", + type=lambda x: int(x, 0), + metavar="ADDRESS", + help="Disassemble at address and exit (hex or decimal)", + ) + disasm_group.add_argument( + "--num-instructions", + type=int, + default=20, + help="Number of instructions to disassemble (default: 20)", + ) + + # Debug flag arguments + debug_group = parser.add_argument_group("Debug Flags") + debug_group.add_argument( + "--debug-cia", + action="store_true", + help="Enable CIA register read/write logging", + ) + debug_group.add_argument( + "--debug-vic", + action="store_true", + help="Enable VIC register operation logging", + ) + debug_group.add_argument( + "--debug-jiffy", + action="store_true", + help="Enable jiffy clock update logging", + ) + debug_group.add_argument( + "--debug-keyboard", + action="store_true", + help="Enable keyboard event logging", + ) + debug_group.add_argument( + "--debug-screen", + action="store_true", + help="Enable screen memory write logging", + ) + debug_group.add_argument( + "--debug-cursor", + action="store_true", + help="Enable cursor position variable logging", + ) + debug_group.add_argument( + "--debug-kernal", + action="store_true", + help="Enable CPU logging when entering KERNAL ROM", + ) + debug_group.add_argument( + "--debug-basic", + action="store_true", + help="Enable CPU logging when entering BASIC ROM", + ) + debug_group.add_argument( + "--verbose-cycles", + action="store_true", + help="Enable per-cycle CPU logging (f/r/w/o markers) - very slow, for debugging only", + ) + + @classmethod + def from_args(cls, args) -> "C64": + """Create a C64 instance from parsed command-line arguments. + + Args: + args: Parsed argparse namespace with C64 arguments + + Returns: + Configured C64 instance + """ + return cls( + rom_dir=args.rom_dir, + display_mode=args.display, + scale=args.scale, + enable_irq=not getattr(args, 'no_irq', False), + video_chip=args.video_chip, + cpu_variant=getattr(args, 'cpu', '6502'), + verbose_cycles=getattr(args, 'verbose_cycles', False), + ) + + def __init__(self, rom_dir="./roms", display_mode="pygame", scale=2, enable_irq=True, video_chip="6569", cpu_variant="6502", verbose_cycles=False, preallocated_ram=None): + """Initialize the C64 emulator.""" + import sys + print("DEBUG C64.__init__: START") + + log.info("DEBUG: Setting _preallocated_ram") + self._preallocated_ram = preallocated_ram + + log.info("DEBUG: Setting rom_dir with Path()") + self.rom_dir = Path(rom_dir) if not isinstance(rom_dir, Path) else rom_dir + + log.info("DEBUG: Setting display_mode, scale, enable_irq") + self.display_mode = display_mode + self.scale = scale + self.enable_irq = enable_irq + + log.info("DEBUG: Processing video_chip") + video_chip_upper = video_chip.upper() + if video_chip_upper in ("6569", "PAL"): + self.video_timing = VIC_6569 + elif video_chip_upper in ("6567R8", "NTSC"): + self.video_timing = VIC_6567R8 + elif video_chip_upper == "6567R56A": + self.video_timing = VIC_6567R56A + else: + raise ValueError(f"Unknown video chip: {video_chip}") + + log.info("DEBUG: Setting video_chip attr") + self.video_chip = self.video_timing.chip_name + + log.info("DEBUG: Checking pygame mode") + if self.display_mode == "pygame": + try: + import pygame + pygame.init() + pygame.quit() + except (ImportError, Exception) as e: + log.warning(f"Pygame initialization failed: {e}") + log.warning("Falling back to terminal display mode") + self.display_mode = "terminal" + + log.info("DEBUG: Creating CPU") + log.info(f"DEBUG: cpu_variant type = {type(cpu_variant)}") + log.info(f"DEBUG: cpu_variant = {cpu_variant}") + self._cpu_variant = CPUVariant.from_string(cpu_variant) + # CPU(cpu_variant, verbose_cycles, preallocated_ram) - use positional args + self.cpu = CPU(self._cpu_variant, verbose_cycles, self._preallocated_ram) + log.info(f"Initialized CPU: {self.cpu.variant_name}") + log.info("DEBUG: After CPU init") + + log.info("DEBUG: Setting basic_rom") + self.basic_rom = None + log.info("DEBUG: Setting kernal_rom") + self.kernal_rom = None + log.info("DEBUG: Setting char_rom") + self.char_rom = None + log.info("DEBUG: Setting vic") + self.vic = None + log.info("DEBUG: Setting cia1") + self.cia1 = None + log.info("DEBUG: Setting cia2") + self.cia2 = None + + log.info("DEBUG: Setting cartridge_type") + self.cartridge_type = "none" + + log.info("DEBUG: Setting iec_bus") + self.iec_bus = None + log.info("DEBUG: Setting drive8") + self.drive8 = None + log.info("DEBUG: Setting drive_enabled") + self.drive_enabled = False + + log.info("DEBUG: Setting pygame attrs") + self.pygame_screen = None + self.pygame_surface = None + self.pygame_available = False + + log.info("DEBUG: Creating ScreenDirtyTracker") + self.dirty_tracker = ScreenDirtyTracker() + + log.info("DEBUG: Setting debug attrs") + self.basic_logging_enabled = False + self.last_pc_region = None + + log.info("DEBUG: Setting BASIC ready attrs") + self._basic_ready = False + self._stop_on_basic = False + + log.info("DEBUG: Setting KERNAL input attrs") + self._kernal_waiting_for_input = False + self._stop_on_kernal_input = False + + log.info("DEBUG: Setting execution_time") + self._execution_start_time = None + self._execution_end_time = None + + log.info("DEBUG: Importing deque") + from collections import deque + + log.info("DEBUG: Setting _speed_sample_count") + self._speed_sample_count = 10 + + log.info("DEBUG: Creating deque") + self._speed_samples = deque((), self._speed_sample_count) + + log.info("DEBUG: Setting _last_sample_time") + self._last_sample_time = 0.0 + log.info("DEBUG: Setting _last_sample_cycles") + self._last_sample_cycles = 0 + + log.info("DEBUG: Calling load_roms()") + self.load_roms() + log.info("DEBUG C64.__init__: END") + + def load_rom(self, filename: str, expected_size: int, description: str) -> bytes: + """Load a ROM file from the rom directory. + + Arguments: + filename: Name of the ROM file + expected_size: Expected size in bytes + description: Human-readable description for logging + + Returns: + ROM data as bytes + + Raises: + FileNotFoundError: If ROM file doesn't exist + ValueError: If ROM file size is incorrect + """ + rom_path = self.rom_dir / filename + + if not rom_path.exists(): + raise FileNotFoundError( + f"{description} ROM not found: {rom_path}\n" + f"Expected file: {filename} in directory: {self.rom_dir}" + ) + + rom_data = rom_path.read_bytes() + + if len(rom_data) != expected_size: + raise ValueError( + f"{description} ROM has incorrect size: {len(rom_data)} bytes " + f"(expected {expected_size} bytes)" + ) + + log.info(f"Loaded {description} ROM: {rom_path} ({len(rom_data)} bytes)") + return rom_data + + def load_roms(self) -> None: + """Load all C64 ROM files into memory. + + First tries to use embedded ROMs (baked into firmware by build_firmware.py). + Falls back to loading from filesystem if embedded ROMs aren't available. + """ + log.info("Loading ROMs...") + + # First, try to use embedded ROMs (from frozen roms module) + embedded_roms_loaded = False + try: + from roms import BASIC_ROM, KERNAL_ROM, CHAR_ROM + log.info("Found embedded ROMs in firmware") + + # Validate sizes + if len(BASIC_ROM) == BASIC_ROM_SIZE: + self.basic_rom = BASIC_ROM + log.info(f"Using embedded BASIC ROM ({len(BASIC_ROM)} bytes)") + else: + log.warning(f"Embedded BASIC ROM wrong size: {len(BASIC_ROM)}, expected {BASIC_ROM_SIZE}") + + if len(KERNAL_ROM) == KERNAL_ROM_SIZE: + self.kernal_rom = KERNAL_ROM + log.info(f"Using embedded KERNAL ROM ({len(KERNAL_ROM)} bytes)") + else: + log.warning(f"Embedded KERNAL ROM wrong size: {len(KERNAL_ROM)}, expected {KERNAL_ROM_SIZE}") + + if len(CHAR_ROM) == CHAR_ROM_SIZE: + self.char_rom = CHAR_ROM + log.info(f"Using embedded CHAR ROM ({len(CHAR_ROM)} bytes)") + else: + log.warning(f"Embedded CHAR ROM wrong size: {len(CHAR_ROM)}, expected {CHAR_ROM_SIZE}") + + # Check if all ROMs loaded successfully + if self.basic_rom and self.kernal_rom and self.char_rom: + embedded_roms_loaded = True + log.info("All embedded ROMs loaded successfully") + + except ImportError: + log.info("No embedded ROMs found, loading from filesystem") + + # If embedded ROMs weren't available or incomplete, load from filesystem + if not embedded_roms_loaded: + # Try common ROM filenames + basic_names = ["basic", "basic.rom", "basic.901226-01.bin"] + kernal_names = ["kernal", "kernal.rom", "kernal.901227-03.bin"] + char_names = ["char", "char.rom", "characters.rom", "characters.901225-01.bin", "chargen"] + + # Load BASIC ROM if not already loaded + if self.basic_rom is None: + for name in basic_names: + try: + self.basic_rom = self.load_rom(name, BASIC_ROM_SIZE, "BASIC") + break + except FileNotFoundError: + continue + else: + raise FileNotFoundError( + f"BASIC ROM not found. Tried: {', '.join(basic_names)} in {self.rom_dir}" + ) + + # Load KERNAL ROM if not already loaded + if self.kernal_rom is None: + for name in kernal_names: + try: + self.kernal_rom = self.load_rom(name, KERNAL_ROM_SIZE, "KERNAL") + break + except FileNotFoundError: + continue + else: + raise FileNotFoundError( + f"KERNAL ROM not found. Tried: {', '.join(kernal_names)} in {self.rom_dir}" + ) + + # Load CHAR ROM if not already loaded (optional) + if self.char_rom is None: + for name in char_names: + try: + self.char_rom = self.load_rom(name, CHAR_ROM_SIZE, "CHAR") + break + except FileNotFoundError: + continue + + if self.char_rom is None: + log.warning(f"CHAR ROM not found (optional). Tried: {', '.join(char_names)}") + + # DON'T write ROMs to CPU RAM! The C64Memory handler reads from ROM arrays directly. + # Writing them to RAM would: + # 1. Waste memory (duplicate data) + # 2. Cause banking issues (RAM vs ROM access) + # 3. Corrupt I/O space in case of CHAR ROM ($D000-$DFFF) + # + # The memory handler (installed at line 1086) provides ROM access via banking logic: + # - BASIC ROM at $A000-$BFFF when bit 0 of $0001 is set + # - KERNAL ROM at $E000-$FFFF when bit 1 of $0001 is set + # - CHAR ROM at $D000-$DFFF when bit 2 of $0001 is clear (I/O disabled) + # + # self._write_rom_to_memory(BASIC_ROM_START, self.basic_rom) + # self._write_rom_to_memory(KERNAL_ROM_START, self.kernal_rom) + # self._write_rom_to_memory(CHAR_ROM_START, self.char_rom) + + + # Now set up the CIA1, CIA2, SID, and VIC + # Use positional args - MicroPython frozen modules don't support kwargs + self.cia1 = CIA1(self.cpu) + self.cia2 = CIA2(self.cpu) + + # Link the CIAs for FLAG pin cross-triggering (IEC bus simulation) + self.cia1.set_other_cia(self.cia2) + self.cia2.set_other_cia(self.cia1) + + self.sid = SID() + # C64VIC(char_rom, cpu, cia2, video_timing) + self.vic = C64VIC(self.char_rom, self.cpu, self.cia2, self.video_timing) + + # Initialize memory + # C64Memory(ram, basic_rom, kernal_rom, char_rom, cia1, cia2, vic, sid, dirty_tracker) + self.memory = C64Memory( + self.cpu.ram, + self.basic_rom, + self.kernal_rom, + self.char_rom, + self.cia1, + self.cia2, + self.vic, + self.sid, + self.dirty_tracker, + ) + # Hook up the memory handler so CPU RAM accesses go through C64Memory + self.cpu.ram.memory_handler = self.memory + + # Give VIC access to C64Memory for VBlank snapshots + self.vic.set_memory(self.memory) + + # Set up periodic update callback for VIC, CIA1, CIA2, and disk drive + # VIC checks cycle count and triggers raster IRQs + # CIA1 counts down timers and triggers timer IRQs + # CIA2 counts down timers and triggers NMIs + # IEC bus and drive: The bus updates happen immediately when CIA2 port A + # is written, and the drive CPU is synchronized at that time. The periodic + # callback just ensures regular updates for VIC/CIA timers. + def update_peripherals(): + self.vic.update() + self.cia1.update() + self.cia2.update() + # Note: Drive CPU sync is now handled per-instruction via + # post_instruction_callback for cycle-accurate IEC timing + # In headless mode, clear frame_complete since there's no render thread + if self.display_mode == "headless" and self.vic.frame_complete.is_set(): + self.vic.frame_complete.clear() + + self.cpu.periodic_callback = update_peripherals + self.cpu.periodic_callback_interval = self.vic.cycles_per_line # Update every raster line + + log.info("All ROMs loaded into memory") + + # Note: PC is already set from reset vector by cpu.reset() in main() + # The reset() method handles the complete reset sequence including + # fetching the vector from $FFFC/$FFFD and setting PC accordingly + log.info(f"PC initialized to ${self.cpu.PC:04X} (from reset vector at ${self.RESET_VECTOR_ADDR:04X})") + + def get_video_standard(self) -> str: + """Get the video standard (PAL or NTSC) based on the current video chip. + + Returns: + "PAL" for chip 6569, "NTSC" for chips 6567R8 or 6567R56A + """ + if self.video_chip == "6569": + return "PAL" + else: # 6567R8 or 6567R56A + return "NTSC" + + def _write_rom_to_memory(self, start_addr: int, rom_data: bytes) -> None: + """Write ROM data to CPU memory. + + Arguments: + start_addr: Starting address in CPU memory + rom_data: ROM data to write + """ + for offset, byte_value in enumerate(rom_data): + self.cpu.ram[start_addr + offset] = byte_value + + log.debug(f"Wrote ROM to ${start_addr:04X}-${start_addr + len(rom_data) - 1:04X}") + + def reset(self) -> None: + """Reset the C64 (CPU reset). + + The CPU reset() method now handles the complete 6502 reset sequence: + - Sets S = 0xFD + - Sets P = 0x34 (I flag set, interrupts disabled) + - Fetches reset vector from $FFFC/$FFFD + - Sets PC to vector value + - Consumes 7 cycles + """ + log.info("Resetting C64...") + + # CPU reset handles the complete hardware reset sequence + self.cpu.reset() + + log.info(f"Reset complete: PC=${self.cpu.PC:04X}, S=${self.cpu.S & 0xFF:02X}") diff --git a/systems/c64/cartridges/__init__.py b/systems/c64/cartridges/__init__.py index 7f14f9c..f135d94 100644 --- a/systems/c64/cartridges/__init__.py +++ b/systems/c64/cartridges/__init__.py @@ -31,6 +31,7 @@ """ # Base classes, enums, and data structures +from mos6502.compat import Dict from .base import ( # ABC and runtime classes Cartridge, @@ -64,8 +65,11 @@ IO2_END, ) -# Test ROM builder for generating test cartridges -from .rom_builder import TestROMBuilder +# Test ROM builder for generating test cartridges - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None # Implemented cartridge types (registered in CARTRIDGE_TYPES) from .type_00_normal import StaticROMCartridge @@ -81,165 +85,176 @@ from .error import ErrorCartridge # Unimplemented cartridge types (NOT registered - generate error carts for testing) -from .type_02_kcs_power import KcsPowerCartridge -from .type_06_expert import ExpertCartridge -from .type_07_fun_play import FunPlayPowerPlayCartridge -from .type_08_super_games import SuperGamesCartridge -from .type_09_atomic_power import AtomicPowerCartridge -from .type_11_westermann import WestermannLearningCartridge -from .type_12_rex_utility import RexUtilityCartridge -from .type_14_magic_formel import MagicFormelCartridge -from .type_16_warpspeed import WarpspeedCartridge -from .type_18_zaxxon import ZaxxonSuperZaxxonCartridge -from .type_20_super_snapshot_v5 import SuperSnapshotV5Cartridge -from .type_21_comal80 import Comal80Cartridge -from .type_22_structured_basic import StructuredBasicCartridge -from .type_23_ross import RossCartridge -from .type_24_dela_ep64 import DelaEp64Cartridge -from .type_25_dela_ep7x8 import DelaEp7X8Cartridge -from .type_26_dela_ep256 import DelaEp256Cartridge -from .type_27_rex_ep256 import RexEp256Cartridge -from .type_28_mikro_assembler import MikroAssemblerCartridge -from .type_29_final_cartridge_plus import FinalCartridgePlusCartridge -from .type_30_action_replay_4 import ActionReplay4Cartridge -from .type_31_stardos import StardosCartridge -from .type_32_easyflash import EasyflashCartridge -from .type_33_easyflash_xbank import EasyflashXBankCartridge -from .type_34_capture import CaptureCartridge -from .type_35_action_replay_3 import ActionReplay3Cartridge -from .type_36_retro_replay import RetroReplayCartridge -from .type_37_mmc64 import Mmc64Cartridge -from .type_38_mmc_replay import MmcReplayCartridge -from .type_39_ide64 import Ide64Cartridge -from .type_40_super_snapshot_v4 import SuperSnapshotV4Cartridge -from .type_41_ieee488 import Ieee488Cartridge -from .type_42_game_killer import GameKillerCartridge -from .type_43_prophet64 import Prophet64Cartridge -from .type_44_exos import ExosCartridge -from .type_45_freeze_frame import FreezeFrameCartridge -from .type_46_freeze_machine import FreezeMachineCartridge -from .type_47_snapshot64 import Snapshot64Cartridge -from .type_48_super_explode_v5 import SuperExplodeV5Cartridge -from .type_49_magic_voice import MagicVoiceCartridge -from .type_50_action_replay_2 import ActionReplay2Cartridge -from .type_51_mach5 import Mach5Cartridge -from .type_52_diashow_maker import DiashowMakerCartridge -from .type_53_pagefox import PagefoxCartridge -from .type_54_kingsoft import KingsoftBusinessBasicCartridge -from .type_55_silver_rock_128 import SilverRock128Cartridge -from .type_56_formel64 import Formel64Cartridge -from .type_57_rgcd import RgcdCartridge -from .type_58_rrnet_mk3 import RrNetMk3Cartridge -from .type_59_easy_calc import EasyCalcResultCartridge -from .type_60_gmod2 import Gmod2Cartridge -from .type_61_max_basic import MaxBasicCartridge -from .type_62_gmod3 import Gmod3Cartridge -from .type_63_zippcode48 import ZippCode48Cartridge -from .type_64_blackbox_v8 import BlackboxV8Cartridge -from .type_65_blackbox_v3 import BlackboxV3Cartridge -from .type_66_blackbox_v4 import BlackboxV4Cartridge -from .type_67_rex_ram_floppy import RexRamFloppyCartridge -from .type_68_bis_plus import BisPlusCartridge -from .type_69_sd_box import SdBoxCartridge -from .type_70_multimax import MultimaxCartridge -from .type_71_blackbox_v9 import BlackboxV9Cartridge -from .type_72_lt_kernal import LtKernalCartridge -from .type_73_cmd_ramlink import CmdRamlinkCartridge -from .type_74_drean import DreanCartridge -from .type_75_ieee_flash_64 import IeeeFlash64Cartridge -from .type_76_turtle_graphics_ii import TurtleGraphicsIiCartridge -from .type_77_freeze_frame_mk2 import FreezeFrameMk2Cartridge -from .type_78_partner64 import Partner64Cartridge -from .type_79_hyper_basic_mk2 import HyperBasicMk2Cartridge -from .type_80_universal_1 import UniversalCartridge1Cartridge -from .type_81_universal_15 import UniversalCartridge15Cartridge -from .type_82_universal_2 import UniversalCartridge2Cartridge -from .type_83_bmp_turbo_2000 import BmpDataTurbo2000Cartridge -from .type_84_profi_dos import ProfiDosCartridge -from .type_85_magic_desk_16 import MagicDesk16Cartridge +# These are optional for MicroPython/Pico to save memory +_UNIMPLEMENTED_CARTRIDGES_AVAILABLE = False +try: + from .type_02_kcs_power import KcsPowerCartridge + from .type_06_expert import ExpertCartridge + from .type_07_fun_play import FunPlayPowerPlayCartridge + from .type_08_super_games import SuperGamesCartridge + from .type_09_atomic_power import AtomicPowerCartridge + from .type_11_westermann import WestermannLearningCartridge + from .type_12_rex_utility import RexUtilityCartridge + from .type_14_magic_formel import MagicFormelCartridge + from .type_16_warpspeed import WarpspeedCartridge + from .type_18_zaxxon import ZaxxonSuperZaxxonCartridge + from .type_20_super_snapshot_v5 import SuperSnapshotV5Cartridge + from .type_21_comal80 import Comal80Cartridge + from .type_22_structured_basic import StructuredBasicCartridge + from .type_23_ross import RossCartridge + from .type_24_dela_ep64 import DelaEp64Cartridge + from .type_25_dela_ep7x8 import DelaEp7X8Cartridge + from .type_26_dela_ep256 import DelaEp256Cartridge + from .type_27_rex_ep256 import RexEp256Cartridge + from .type_28_mikro_assembler import MikroAssemblerCartridge + from .type_29_final_cartridge_plus import FinalCartridgePlusCartridge + from .type_30_action_replay_4 import ActionReplay4Cartridge + from .type_31_stardos import StardosCartridge + from .type_32_easyflash import EasyflashCartridge + from .type_33_easyflash_xbank import EasyflashXBankCartridge + from .type_34_capture import CaptureCartridge + from .type_35_action_replay_3 import ActionReplay3Cartridge + from .type_36_retro_replay import RetroReplayCartridge + from .type_37_mmc64 import Mmc64Cartridge + from .type_38_mmc_replay import MmcReplayCartridge + from .type_39_ide64 import Ide64Cartridge + from .type_40_super_snapshot_v4 import SuperSnapshotV4Cartridge + from .type_41_ieee488 import Ieee488Cartridge + from .type_42_game_killer import GameKillerCartridge + from .type_43_prophet64 import Prophet64Cartridge + from .type_44_exos import ExosCartridge + from .type_45_freeze_frame import FreezeFrameCartridge + from .type_46_freeze_machine import FreezeMachineCartridge + from .type_47_snapshot64 import Snapshot64Cartridge + from .type_48_super_explode_v5 import SuperExplodeV5Cartridge + from .type_49_magic_voice import MagicVoiceCartridge + from .type_50_action_replay_2 import ActionReplay2Cartridge + from .type_51_mach5 import Mach5Cartridge + from .type_52_diashow_maker import DiashowMakerCartridge + from .type_53_pagefox import PagefoxCartridge + from .type_54_kingsoft import KingsoftBusinessBasicCartridge + from .type_55_silver_rock_128 import SilverRock128Cartridge + from .type_56_formel64 import Formel64Cartridge + from .type_57_rgcd import RgcdCartridge + from .type_58_rrnet_mk3 import RrNetMk3Cartridge + from .type_59_easy_calc import EasyCalcResultCartridge + from .type_60_gmod2 import Gmod2Cartridge + from .type_61_max_basic import MaxBasicCartridge + from .type_62_gmod3 import Gmod3Cartridge + from .type_63_zippcode48 import ZippCode48Cartridge + from .type_64_blackbox_v8 import BlackboxV8Cartridge + from .type_65_blackbox_v3 import BlackboxV3Cartridge + from .type_66_blackbox_v4 import BlackboxV4Cartridge + from .type_67_rex_ram_floppy import RexRamFloppyCartridge + from .type_68_bis_plus import BisPlusCartridge + from .type_69_sd_box import SdBoxCartridge + from .type_70_multimax import MultimaxCartridge + from .type_71_blackbox_v9 import BlackboxV9Cartridge + from .type_72_lt_kernal import LtKernalCartridge + from .type_73_cmd_ramlink import CmdRamlinkCartridge + from .type_74_drean import DreanCartridge + from .type_75_ieee_flash_64 import IeeeFlash64Cartridge + from .type_76_turtle_graphics_ii import TurtleGraphicsIiCartridge + from .type_77_freeze_frame_mk2 import FreezeFrameMk2Cartridge + from .type_78_partner64 import Partner64Cartridge + from .type_79_hyper_basic_mk2 import HyperBasicMk2Cartridge + from .type_80_universal_1 import UniversalCartridge1Cartridge + from .type_81_universal_15 import UniversalCartridge15Cartridge + from .type_82_universal_2 import UniversalCartridge2Cartridge + from .type_83_bmp_turbo_2000 import BmpDataTurbo2000Cartridge + from .type_84_profi_dos import ProfiDosCartridge + from .type_85_magic_desk_16 import MagicDesk16Cartridge + _UNIMPLEMENTED_CARTRIDGES_AVAILABLE = True +except ImportError: + # Unimplemented cartridges not available (MicroPython/Pico deployment) + pass # Registry and factory from .registry import CARTRIDGE_TYPES, create_cartridge # Map of unimplemented cartridge types for test cart generation -UNIMPLEMENTED_CARTRIDGE_TYPES: dict[int, type[Cartridge]] = { - 2: KcsPowerCartridge, - 6: ExpertCartridge, - 7: FunPlayPowerPlayCartridge, - 8: SuperGamesCartridge, - 9: AtomicPowerCartridge, - 11: WestermannLearningCartridge, - 12: RexUtilityCartridge, - 14: MagicFormelCartridge, - 16: WarpspeedCartridge, - 18: ZaxxonSuperZaxxonCartridge, - 20: SuperSnapshotV5Cartridge, - 21: Comal80Cartridge, - 22: StructuredBasicCartridge, - 23: RossCartridge, - 24: DelaEp64Cartridge, - 25: DelaEp7X8Cartridge, - 26: DelaEp256Cartridge, - 27: RexEp256Cartridge, - 28: MikroAssemblerCartridge, - 29: FinalCartridgePlusCartridge, - 30: ActionReplay4Cartridge, - 31: StardosCartridge, - 32: EasyflashCartridge, - 33: EasyflashXBankCartridge, - 34: CaptureCartridge, - 35: ActionReplay3Cartridge, - 36: RetroReplayCartridge, - 37: Mmc64Cartridge, - 38: MmcReplayCartridge, - 39: Ide64Cartridge, - 40: SuperSnapshotV4Cartridge, - 41: Ieee488Cartridge, - 42: GameKillerCartridge, - 43: Prophet64Cartridge, - 44: ExosCartridge, - 45: FreezeFrameCartridge, - 46: FreezeMachineCartridge, - 47: Snapshot64Cartridge, - 48: SuperExplodeV5Cartridge, - 49: MagicVoiceCartridge, - 50: ActionReplay2Cartridge, - 51: Mach5Cartridge, - 52: DiashowMakerCartridge, - 53: PagefoxCartridge, - 54: KingsoftBusinessBasicCartridge, - 55: SilverRock128Cartridge, - 56: Formel64Cartridge, - 57: RgcdCartridge, - 58: RrNetMk3Cartridge, - 59: EasyCalcResultCartridge, - 60: Gmod2Cartridge, - 61: MaxBasicCartridge, - 62: Gmod3Cartridge, - 63: ZippCode48Cartridge, - 64: BlackboxV8Cartridge, - 65: BlackboxV3Cartridge, - 66: BlackboxV4Cartridge, - 67: RexRamFloppyCartridge, - 68: BisPlusCartridge, - 69: SdBoxCartridge, - 70: MultimaxCartridge, - 71: BlackboxV9Cartridge, - 72: LtKernalCartridge, - 73: CmdRamlinkCartridge, - 74: DreanCartridge, - 75: IeeeFlash64Cartridge, - 76: TurtleGraphicsIiCartridge, - 77: FreezeFrameMk2Cartridge, - 78: Partner64Cartridge, - 79: HyperBasicMk2Cartridge, - 80: UniversalCartridge1Cartridge, - 81: UniversalCartridge15Cartridge, - 82: UniversalCartridge2Cartridge, - 83: BmpDataTurbo2000Cartridge, - 84: ProfiDosCartridge, - 85: MagicDesk16Cartridge, -} +# Only available when unimplemented cartridges are loaded (not on MicroPython/Pico) +if _UNIMPLEMENTED_CARTRIDGES_AVAILABLE: + UNIMPLEMENTED_CARTRIDGE_TYPES: Dict[int, type[Cartridge]] = { + 2: KcsPowerCartridge, + 6: ExpertCartridge, + 7: FunPlayPowerPlayCartridge, + 8: SuperGamesCartridge, + 9: AtomicPowerCartridge, + 11: WestermannLearningCartridge, + 12: RexUtilityCartridge, + 14: MagicFormelCartridge, + 16: WarpspeedCartridge, + 18: ZaxxonSuperZaxxonCartridge, + 20: SuperSnapshotV5Cartridge, + 21: Comal80Cartridge, + 22: StructuredBasicCartridge, + 23: RossCartridge, + 24: DelaEp64Cartridge, + 25: DelaEp7X8Cartridge, + 26: DelaEp256Cartridge, + 27: RexEp256Cartridge, + 28: MikroAssemblerCartridge, + 29: FinalCartridgePlusCartridge, + 30: ActionReplay4Cartridge, + 31: StardosCartridge, + 32: EasyflashCartridge, + 33: EasyflashXBankCartridge, + 34: CaptureCartridge, + 35: ActionReplay3Cartridge, + 36: RetroReplayCartridge, + 37: Mmc64Cartridge, + 38: MmcReplayCartridge, + 39: Ide64Cartridge, + 40: SuperSnapshotV4Cartridge, + 41: Ieee488Cartridge, + 42: GameKillerCartridge, + 43: Prophet64Cartridge, + 44: ExosCartridge, + 45: FreezeFrameCartridge, + 46: FreezeMachineCartridge, + 47: Snapshot64Cartridge, + 48: SuperExplodeV5Cartridge, + 49: MagicVoiceCartridge, + 50: ActionReplay2Cartridge, + 51: Mach5Cartridge, + 52: DiashowMakerCartridge, + 53: PagefoxCartridge, + 54: KingsoftBusinessBasicCartridge, + 55: SilverRock128Cartridge, + 56: Formel64Cartridge, + 57: RgcdCartridge, + 58: RrNetMk3Cartridge, + 59: EasyCalcResultCartridge, + 60: Gmod2Cartridge, + 61: MaxBasicCartridge, + 62: Gmod3Cartridge, + 63: ZippCode48Cartridge, + 64: BlackboxV8Cartridge, + 65: BlackboxV3Cartridge, + 66: BlackboxV4Cartridge, + 67: RexRamFloppyCartridge, + 68: BisPlusCartridge, + 69: SdBoxCartridge, + 70: MultimaxCartridge, + 71: BlackboxV9Cartridge, + 72: LtKernalCartridge, + 73: CmdRamlinkCartridge, + 74: DreanCartridge, + 75: IeeeFlash64Cartridge, + 76: TurtleGraphicsIiCartridge, + 77: FreezeFrameMk2Cartridge, + 78: Partner64Cartridge, + 79: HyperBasicMk2Cartridge, + 80: UniversalCartridge1Cartridge, + 81: UniversalCartridge15Cartridge, + 82: UniversalCartridge2Cartridge, + 83: BmpDataTurbo2000Cartridge, + 84: ProfiDosCartridge, + 85: MagicDesk16Cartridge, + } +else: + UNIMPLEMENTED_CARTRIDGE_TYPES: Dict[int, type[Cartridge]] = {} __all__ = [ diff --git a/systems/c64/cartridges/base.py b/systems/c64/cartridges/base.py index e272221..5648cc9 100644 --- a/systems/c64/cartridges/base.py +++ b/systems/c64/cartridges/base.py @@ -8,15 +8,14 @@ - Error cartridge ROM generation utilities """ -from __future__ import annotations -import logging +from mos6502.compat import logging import re import struct -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from enum import IntEnum -from typing import Protocol +from mos6502.compat import ABC, abstractmethod +from mos6502.compat import dataclass, field +from mos6502.compat import IntEnum +from mos6502.compat import Protocol, Union, List, Dict, Tuple log = logging.getLogger("c64.cartridge") @@ -236,7 +235,7 @@ class CartridgeImage(CartridgeVariant): Inherits variant configuration and adds ROM data and serialization. This is the result of calling create_test_cartridge(). """ - rom_data: dict = None # {"roml": bytes, "romh": bytes} or {"banks": list[bytes]} + rom_data: dict = None # {"roml": bytes, "romh": bytes} or {"banks": List[bytes]} hardware_type: int = 0 def __post_init__(self): @@ -330,172 +329,120 @@ def _build_chips(self) -> bytes: """Build all CHIP packets based on rom_data structure.""" chips = b"" + # _build_chip_packet args: bank, load_addr, data if "roml" in self.rom_data: if self.extra.get("single_chip") and "romh" in self.rom_data: # Combined ROML+ROMH as single 16KB chip combined = self.rom_data["roml"] + self.rom_data["romh"] - chips += self._build_chip_packet( - bank=0, load_addr=ROML_START, data=combined - ) + chips += self._build_chip_packet(0, ROML_START, combined) else: # ROML as separate chip - chips += self._build_chip_packet( - bank=0, load_addr=ROML_START, data=self.rom_data["roml"] - ) + chips += self._build_chip_packet(0, ROML_START, self.rom_data["roml"]) if "romh" in self.rom_data: # ROMH as separate chip (16KB mode at $A000) - chips += self._build_chip_packet( - bank=0, load_addr=ROMH_START, data=self.rom_data["romh"] - ) + chips += self._build_chip_packet(0, ROMH_START, self.rom_data["romh"]) if "ultimax_romh" in self.rom_data: # Ultimax ROMH as separate chip at $E000 (with ROML present) - chips += self._build_chip_packet( - bank=0, load_addr=ULTIMAX_ROMH_START, data=self.rom_data["ultimax_romh"] - ) + chips += self._build_chip_packet(0, ULTIMAX_ROMH_START, self.rom_data["ultimax_romh"]) elif "romh" in self.rom_data: # ROMH alone (Ultimax mode - loads at $E000) # Use extra["ultimax_romh_addr"] if specified, else ULTIMAX_ROMH_START romh_addr = self.extra.get("ultimax_romh_addr", ULTIMAX_ROMH_START) - chips += self._build_chip_packet( - bank=0, load_addr=romh_addr, data=self.rom_data["romh"] - ) + chips += self._build_chip_packet(0, romh_addr, self.rom_data["romh"]) elif "ultimax_romh" in self.rom_data: # Ultimax ROMH alone at $E000 - chips += self._build_chip_packet( - bank=0, load_addr=ULTIMAX_ROMH_START, data=self.rom_data["ultimax_romh"] - ) + chips += self._build_chip_packet(0, ULTIMAX_ROMH_START, self.rom_data["ultimax_romh"]) if "banks" in self.rom_data: for i, bank_data in enumerate(self.rom_data["banks"]): - chips += self._build_chip_packet( - bank=i, load_addr=ROML_START, data=bank_data - ) + chips += self._build_chip_packet(i, ROML_START, bank_data) return chips # Mapper requirements for each hardware type # This defines what each mapper type needs to function -MAPPER_REQUIREMENTS: dict[int | CartridgeType, MapperRequirements] = { +# MapperRequirements field order (for positional args - kwargs don't work in MicroPython frozen): +# uses_roml, uses_romh, uses_ultimax_romh, num_banks, bank_size, +# uses_io1, uses_io2, control_registers, has_ram, ram_size +MAPPER_REQUIREMENTS: Union[Dict[int, CartridgeType], MapperRequirements] = { # Type 0: Static ROM - simplest type, no banking - CartridgeType.NORMAL: MapperRequirements( - uses_roml=True, - uses_romh=True, # 16KB mode - uses_ultimax_romh=True, # Ultimax mode - num_banks=1, - ), + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=True, num_banks=1) + CartridgeType.NORMAL: MapperRequirements(True, True, True, 1), # Type 1: Action Replay - freezer with bank switching + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=False, num_banks=4, bank_size=0x2000, + # uses_io1=True, uses_io2=True, control_registers=[...], has_ram=True, ram_size=0x2000) CartridgeType.ACTION_REPLAY: MapperRequirements( - uses_roml=True, - uses_romh=True, - num_banks=4, - uses_io1=True, - uses_io2=True, - has_ram=True, - ram_size=0x2000, # 8KB RAM - control_registers=[ - ("$DE00", "Control Reg"), - ], + True, True, False, 4, 0x2000, True, True, + [("$DE00", "Control Reg")], True, 0x2000 ), # Type 2: KCS Power Cartridge + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=False, num_banks=1, bank_size=0x2000, + # uses_io1=True, uses_io2=True, control_registers=[...]) CartridgeType.KCS_POWER: MapperRequirements( - uses_roml=True, - uses_romh=True, - num_banks=1, - uses_io1=True, - uses_io2=True, - control_registers=[ - ("$DE00", "Config"), - ("$DF00", "Freeze"), - ], + True, True, False, 1, 0x2000, True, True, + [("$DE00", "Config"), ("$DF00", "Freeze")] ), # Type 3: Final Cartridge III + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=False, num_banks=4, bank_size=0x2000, + # uses_io1=True, uses_io2=True, control_registers=[...]) CartridgeType.FINAL_CARTRIDGE_III: MapperRequirements( - uses_roml=True, - uses_romh=True, - num_banks=4, - uses_io1=True, - uses_io2=True, - control_registers=[ - ("$DFFF", "Control"), - ], + True, True, False, 4, 0x2000, True, True, + [("$DFFF", "Control")] ), # Type 4: Simons' BASIC + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=False, num_banks=1, bank_size=0x2000, + # uses_io1=True, uses_io2=True, control_registers=[...]) CartridgeType.SIMONS_BASIC: MapperRequirements( - uses_roml=True, - uses_romh=True, - num_banks=1, - uses_io1=True, - uses_io2=True, - control_registers=[ - ("$DE00", "Enable ROMH"), - ("$DF00", "Disable ROMH"), - ], + True, True, False, 1, 0x2000, True, True, + [("$DE00", "Enable ROMH"), ("$DF00", "Disable ROMH")] ), # Type 5: Ocean type 1 + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=False, num_banks=64, bank_size=0x2000, + # uses_io1=True, uses_io2=False, control_registers=[...]) CartridgeType.OCEAN_TYPE_1: MapperRequirements( - uses_roml=True, - uses_romh=True, - num_banks=64, # Up to 512KB - uses_io1=True, - control_registers=[ - ("$DE00", "Bank Select"), - ], + True, True, False, 64, 0x2000, True, False, + [("$DE00", "Bank Select")] ), # Type 10: Epyx FastLoad + # (uses_roml=True, uses_romh=False, uses_ultimax_romh=False, num_banks=1, bank_size=0x2000, + # uses_io1=True, uses_io2=True, control_registers=[...]) CartridgeType.EPYX_FASTLOAD: MapperRequirements( - uses_roml=True, - num_banks=1, - uses_io1=True, # Reading IO1 enables cartridge - uses_io2=True, # IO2 shows last 256 bytes of ROM (always visible) - control_registers=[ - ("$DE00", "Enable (read)"), - ("$DF00", "ROM stub"), - ], + True, False, False, 1, 0x2000, True, True, + [("$DE00", "Enable (read)"), ("$DF00", "ROM stub")] ), # Type 13: Final Cartridge I + # (uses_roml=True, uses_romh=True, uses_ultimax_romh=False, num_banks=1, bank_size=0x2000, + # uses_io1=True, uses_io2=True, control_registers=[...]) CartridgeType.FINAL_CARTRIDGE_I: MapperRequirements( - uses_roml=True, - uses_romh=True, - num_banks=1, - uses_io1=True, # Any IO1 access disables cartridge - uses_io2=True, # Any IO2 access enables cartridge - control_registers=[ - ("$DE00", "Disable (any access)"), - ("$DF00", "Enable (any access)"), - ], + True, True, False, 1, 0x2000, True, True, + [("$DE00", "Disable (any access)"), ("$DF00", "Enable (any access)")] ), # Type 15: C64 Game System / System 3 + # (uses_roml=True, uses_romh=False, uses_ultimax_romh=False, num_banks=64, bank_size=0x2000, + # uses_io1=True, uses_io2=False, control_registers=[...]) CartridgeType.C64_GAME_SYSTEM: MapperRequirements( - uses_roml=True, - num_banks=64, # Up to 512KB (64 x 8KB banks) - uses_io1=True, - control_registers=[ - ("$DE00", "Bank Select (read disables)"), - ], + True, False, False, 64, 0x2000, True, False, + [("$DE00", "Bank Select (read disables)")] ), # Type 17: Dinamic + # (uses_roml=True, uses_romh=False, uses_ultimax_romh=False, num_banks=16, bank_size=0x2000, + # uses_io1=True, uses_io2=False, control_registers=[...]) CartridgeType.DINAMIC: MapperRequirements( - uses_roml=True, - num_banks=16, # 128KB typical - uses_io1=True, - control_registers=[ - ("$DE00", "Bank Select"), - ], + True, False, False, 16, 0x2000, True, False, + [("$DE00", "Bank Select")] ), # Type 19: Magic Desk / Domark / HES Australia + # (uses_roml=True, uses_romh=False, uses_ultimax_romh=False, num_banks=64, bank_size=0x2000, + # uses_io1=True, uses_io2=False, control_registers=[...]) CartridgeType.MAGIC_DESK: MapperRequirements( - uses_roml=True, - num_banks=64, # Up to 512KB (64 x 8KB banks) - uses_io1=True, - control_registers=[ - ("$DE00", "Bank/Disable"), - ], + True, False, False, 64, 0x2000, True, False, + [("$DE00", "Bank/Disable")] ), } -def generate_mapper_tests(hw_type: int) -> list[MapperTest]: +def generate_mapper_tests(hw_type: int) -> List[MapperTest]: """Generate the list of tests for a mapper type. This is the single source of truth for both: @@ -516,37 +463,35 @@ def generate_mapper_tests(hw_type: int) -> list[MapperTest]: bank_size = reqs.bank_size # Generate bank tests + # MapperTest field order: name, test_id, passed (default False) if reqs.num_banks > 1: for bank_num in range(reqs.num_banks): bank_offset = bank_num * bank_size - tests.append(MapperTest( - name=f"Bank {bank_num} ${bank_offset:04X}", - test_id=f"bank_{bank_num}", - )) + tests.append(MapperTest(f"Bank {bank_num} ${bank_offset:04X}", f"bank_{bank_num}")) else: # Single bank - just test ROML/ROMH presence if reqs.uses_roml: - tests.append(MapperTest(name="ROML $8000", test_id="roml")) + tests.append(MapperTest("ROML $8000", "roml")) if reqs.uses_romh: - tests.append(MapperTest(name="ROMH $A000", test_id="romh")) + tests.append(MapperTest("ROMH $A000", "romh")) if reqs.uses_ultimax_romh: - tests.append(MapperTest(name="ROMH $E000", test_id="ultimax_romh")) + tests.append(MapperTest("ROMH $E000", "ultimax_romh")) # Generate I/O and control register tests ctrl_addr_set = {a for a, _ in reqs.control_registers} if reqs.uses_io1 and "$DE00" not in ctrl_addr_set: - tests.append(MapperTest(name="IO1 $DE00-$DEFF", test_id="io1")) + tests.append(MapperTest("IO1 $DE00-$DEFF", "io1")) if reqs.uses_io2 and "$DF00" not in ctrl_addr_set: if reqs.has_ram: - tests.append(MapperTest(name="IO2 $DF00 RAM", test_id="io2_ram")) + tests.append(MapperTest("IO2 $DF00 RAM", "io2_ram")) else: - tests.append(MapperTest(name="IO2 $DF00-$DFFF", test_id="io2")) + tests.append(MapperTest("IO2 $DF00-$DFFF", "io2")) # Control registers for reg_addr, name in reqs.control_registers: safe_id = reg_addr.replace("$", "").lower() - tests.append(MapperTest(name=f"{reg_addr} {name}", test_id=f"reg_{safe_id}")) + tests.append(MapperTest(f"{reg_addr} {name}", f"reg_{safe_id}")) return tests @@ -599,7 +544,7 @@ def __post_init__(self): if self.mapper_addresses is None: self.mapper_addresses = {} - def to_display_lines(self) -> list[str]: + def to_display_lines(self) -> List[str]: """Convert results to display lines for error cartridge. Uses generate_mapper_tests() as the single source of truth for @@ -712,7 +657,7 @@ def colorize_test_name(name: str) -> str: return lines -def parse_color_markup(text: str) -> list[tuple[str, int]]: +def parse_color_markup(text: str) -> List[Tuple[str, int]]: """Parse color markup in text and return list of (char, color) tuples. Color markup codes: @@ -763,7 +708,7 @@ def parse_color_markup(text: str) -> list[tuple[str, int]]: return result -def create_error_cartridge_rom(error_lines: list[str], border_color: int = 0x02) -> bytes: +def create_error_cartridge_rom(error_lines: List[str], border_color: int = 0x02) -> bytes: """Create an 8KB cartridge ROM that displays an error/info message. This is the single source of truth for error cartridge generation, @@ -1052,7 +997,7 @@ def reset(self) -> None: # --- Test cartridge generation --- @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for this cartridge type. Each variant represents a different mode or configuration that should @@ -1107,12 +1052,9 @@ def create_error_cartridge( CartridgeImage for a Type 0 8KB error display cart """ lines = results.to_display_lines() - rom_bytes = create_error_cartridge_rom(lines, border_color=0x02) # Red border + # create_error_cartridge_rom args: error_lines, border_color (default 0x02 = red) + rom_bytes = create_error_cartridge_rom(lines, 0x02) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=0, - game=1, - extra=variant.extra, - rom_data={"roml": rom_bytes}, - hardware_type=0, # Type 0 for simplest compatibility + variant.description, 0, 1, variant.extra, {"roml": rom_bytes}, 0 ) diff --git a/systems/c64/cartridges/error.py b/systems/c64/cartridges/error.py index 839e8c7..5963719 100644 --- a/systems/c64/cartridges/error.py +++ b/systems/c64/cartridges/error.py @@ -4,7 +4,6 @@ is created to display information about the unsupported type. """ -from __future__ import annotations from .type_00_normal import StaticROMCartridge diff --git a/systems/c64/cartridges/registry.py b/systems/c64/cartridges/registry.py index bc344a0..0aa6f9b 100644 --- a/systems/c64/cartridges/registry.py +++ b/systems/c64/cartridges/registry.py @@ -4,9 +4,8 @@ and the factory function for creating cartridge instances. """ -from __future__ import annotations -from typing import Optional +from mos6502.compat import Optional, Union, Dict, List from .base import Cartridge, CartridgeType from .type_00_normal import StaticROMCartridge @@ -22,7 +21,7 @@ # Registry of cartridge classes by hardware type -CARTRIDGE_TYPES: dict[int | CartridgeType, type[Cartridge]] = { +CARTRIDGE_TYPES: Union[Dict[int, CartridgeType], type[Cartridge]] = { CartridgeType.NORMAL: StaticROMCartridge, CartridgeType.ACTION_REPLAY: ActionReplayCartridge, CartridgeType.FINAL_CARTRIDGE_III: FinalCartridgeIIICartridge, @@ -41,7 +40,7 @@ def create_cartridge( roml_data: Optional[bytes] = None, romh_data: Optional[bytes] = None, ultimax_romh_data: Optional[bytes] = None, - banks: Optional[list[bytes]] = None, + banks: Optional[List[bytes]] = None, name: str = "", ) -> Cartridge: """Factory function to create appropriate cartridge instance. diff --git a/systems/c64/cartridges/rom_builder.py b/systems/c64/cartridges/rom_builder.py index aaebc88..ee6d51b 100644 --- a/systems/c64/cartridges/rom_builder.py +++ b/systems/c64/cartridges/rom_builder.py @@ -10,7 +10,6 @@ their create_test_cartridge() methods. """ -from __future__ import annotations from .base import ROML_START, ROML_SIZE @@ -66,6 +65,7 @@ # Logic ORA_IMMEDIATE_0x09, ) +from mos6502.compat import List __all__ = [ @@ -101,7 +101,7 @@ RAM_ROUTINE_ADDR = 0xC000 -def text_to_screen_codes(text: str) -> list[int]: +def text_to_screen_codes(text: str) -> List[int]: """Convert ASCII text to C64 screen codes.""" screen_codes = [] for ch in text: @@ -414,7 +414,7 @@ def emit_check_a_equals(self, expected: int, fail_label: str) -> None: self.branches_to_fix.append((len(self.code), fail_label)) self.code.append(0x00) # Placeholder - def emit_bytes(self, bytes_list: list[int]) -> None: + def emit_bytes(self, bytes_list: List[int]) -> None: """Emit raw bytes directly into the code stream. Args: diff --git a/systems/c64/cartridges/type_00_normal.py b/systems/c64/cartridges/type_00_normal.py index 5f66227..e129514 100644 --- a/systems/c64/cartridges/type_00_normal.py +++ b/systems/c64/cartridges/type_00_normal.py @@ -5,10 +5,9 @@ addresses with no banking hardware. """ -from __future__ import annotations -import logging -from typing import Optional +from mos6502.compat import logging +from mos6502.compat import Optional, List from .base import ( Cartridge, @@ -22,7 +21,11 @@ ULTIMAX_ROMH_START, ULTIMAX_ROMH_SIZE, ) -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE log = logging.getLogger("c64.cartridge") @@ -135,14 +138,15 @@ def read_ultimax_romh(self, addr: int) -> int: # --- Test cartridge generation --- @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 0.""" + # CartridgeVariant field order: description, exrom, game, extra return [ - CartridgeVariant("8k", exrom=0, game=1), - CartridgeVariant("16k", exrom=0, game=0), - CartridgeVariant("16k_single_chip", exrom=0, game=0, extra={"single_chip": True}), - CartridgeVariant("ultimax", exrom=1, game=0), - CartridgeVariant("ultimax_with_roml", exrom=1, game=0, extra={"include_roml": True}), + CartridgeVariant("8k", 0, 1), + CartridgeVariant("16k", 0, 0), + CartridgeVariant("16k_single_chip", 0, 0, {"single_chip": True}), + CartridgeVariant("ultimax", 1, 0), + CartridgeVariant("ultimax_with_roml", 1, 0, {"include_roml": True}), ] @classmethod @@ -202,13 +206,10 @@ def _create_8k_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: signature = b"ROML-OK!" roml[0x1FF0:0x1FF0 + len(signature)] = signature + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"roml": bytes(roml)}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"roml": bytes(roml)}, cls.HARDWARE_TYPE ) @classmethod @@ -264,13 +265,10 @@ def _create_16k_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage romh[0x0000:0x0008] = b"RH-START" # At $A000 romh[0x1FF0:0x1FF8] = b"RH-END!!" # At $BFF0 + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"roml": bytes(roml), "romh": bytes(romh)}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"roml": bytes(roml), "romh": bytes(romh)}, cls.HARDWARE_TYPE ) @classmethod @@ -332,11 +330,8 @@ def _create_ultimax_test_cartridge( roml[0x1FF0:0x1FF8] = b"ROML-OK!" rom_data["roml"] = bytes(roml) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data=rom_data, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + rom_data, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_01_action_replay.py b/systems/c64/cartridges/type_01_action_replay.py index 4d8e274..36ebe97 100644 --- a/systems/c64/cartridges/type_01_action_replay.py +++ b/systems/c64/cartridges/type_01_action_replay.py @@ -4,9 +4,8 @@ 32KB ROM organized as 4 x 8KB banks, plus 8KB RAM. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import ( Cartridge, @@ -17,8 +16,13 @@ ROMH_START, IO2_START, ) -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -54,7 +58,7 @@ class ActionReplayCartridge(Cartridge): HARDWARE_TYPE = 1 ACTIVE_BANK_SIZE = ROML_SIZE # 8KB banks - def __init__(self, banks: list[bytes], name: str = ""): + def __init__(self, banks: List[bytes], name: str = ""): """Initialize Action Replay cartridge. Args: @@ -210,10 +214,11 @@ def write_io2(self, addr: int, data: int) -> None: SIGNATURE_ADDR = 0x9FF5 # Each bank has its bank number here @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 1.""" + # CartridgeVariant field order: description, exrom, game, extra return [ - CartridgeVariant("", exrom=0, game=0, extra={"bank_count": 4}), + CartridgeVariant("", 0, 0, {"bank_count": 4}), ] @classmethod @@ -270,11 +275,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: bank[0x1FF5] = i # Bank number as signature banks.append(bytes(bank)) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"banks": banks}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"banks": banks}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_02_kcs_power.py b/systems/c64/cartridges/type_02_kcs_power.py index 231426a..7bcf042 100644 --- a/systems/c64/cartridges/type_02_kcs_power.py +++ b/systems/c64/cartridges/type_02_kcs_power.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class KcsPowerCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_03_final_cartridge_iii.py b/systems/c64/cartridges/type_03_final_cartridge_iii.py index 9417b14..ef54783 100644 --- a/systems/c64/cartridges/type_03_final_cartridge_iii.py +++ b/systems/c64/cartridges/type_03_final_cartridge_iii.py @@ -4,9 +4,8 @@ cartridge with 64KB ROM organized as 4 x 16KB banks. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import ( Cartridge, @@ -19,8 +18,13 @@ IO1_START, IO2_START, ) -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -66,7 +70,7 @@ class FinalCartridgeIIICartridge(Cartridge): BANK_SIZE = ROML_SIZE + ROMH_SIZE # 16KB per bank NUM_BANKS = 4 - def __init__(self, banks: list[bytes], name: str = ""): + def __init__(self, banks: List[bytes], name: str = ""): """Initialize Final Cartridge III. Args: @@ -228,10 +232,11 @@ def write_io2(self, addr: int, data: int) -> None: SIGNATURE_ADDR = 0x9FF5 # Each bank has its bank number here @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 3.""" + # CartridgeVariant field order: description, exrom, game, extra return [ - CartridgeVariant("", exrom=0, game=0, extra={"bank_count": 4}), + CartridgeVariant("", 0, 0, {"bank_count": 4}), ] @classmethod @@ -288,11 +293,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: romh = bytearray(ROMH_SIZE) banks.append(bytes(roml) + bytes(romh)) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"banks": banks}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"banks": banks}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_04_simons_basic.py b/systems/c64/cartridges/type_04_simons_basic.py index 5dfd458..fa75af7 100644 --- a/systems/c64/cartridges/type_04_simons_basic.py +++ b/systems/c64/cartridges/type_04_simons_basic.py @@ -4,13 +4,17 @@ with additional commands. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import Cartridge, CartridgeVariant, CartridgeImage, ROML_START, ROML_SIZE, ROMH_START, ROMH_SIZE -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -114,10 +118,11 @@ def write_io2(self, addr: int, data: int) -> None: # --- Test cartridge generation --- @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 4.""" + # CartridgeVariant field order: description, exrom, game, extra return [ - CartridgeVariant("", exrom=0, game=1), # 8KB mode initially + CartridgeVariant("", 0, 1), # 8KB mode initially ] @classmethod @@ -169,11 +174,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: romh_data = bytearray(ROMH_SIZE) romh_data[0x1FF0:0x1FF8] = b"RH-SIGN!" # ROMH signature + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"roml": bytes(roml_data), "romh": bytes(romh_data)}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"roml": bytes(roml_data), "romh": bytes(romh_data)}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_05_ocean.py b/systems/c64/cartridges/type_05_ocean.py index f3e012e..5b97d3b 100644 --- a/systems/c64/cartridges/type_05_ocean.py +++ b/systems/c64/cartridges/type_05_ocean.py @@ -4,13 +4,17 @@ organized as up to 64 x 8KB banks. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import Cartridge, CartridgeVariant, CartridgeImage, ROML_START, ROML_SIZE, ROMH_START -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -49,7 +53,7 @@ class OceanType1Cartridge(Cartridge): HARDWARE_TYPE = 5 BANK_SIZE = ROML_SIZE # 8KB banks - def __init__(self, banks: list[bytes], name: str = "", use_16kb_mode: bool = False): + def __init__(self, banks: List[bytes], name: str = "", use_16kb_mode: bool = False): """Initialize Ocean Type 1 cartridge. Args: @@ -125,12 +129,13 @@ def write_io1(self, addr: int, data: int) -> None: # --- Test cartridge generation --- @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 5.""" + # CartridgeVariant field order: description, exrom, game, extra return [ - CartridgeVariant("128k", exrom=0, game=1, extra={"bank_count": 16}), - CartridgeVariant("256k", exrom=0, game=1, extra={"bank_count": 32}), - CartridgeVariant("512k", exrom=0, game=1, extra={"bank_count": 64}), + CartridgeVariant("128k", 0, 1, {"bank_count": 16}), + CartridgeVariant("256k", 0, 1, {"bank_count": 32}), + CartridgeVariant("512k", 0, 1, {"bank_count": 64}), ] # Bank select register and signature location @@ -188,11 +193,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: bank[0x1FF5] = i # Bank number as signature banks.append(bytes(bank)) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"banks": banks}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"banks": banks}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_06_expert.py b/systems/c64/cartridges/type_06_expert.py index 35cc26c..885739f 100644 --- a/systems/c64/cartridges/type_06_expert.py +++ b/systems/c64/cartridges/type_06_expert.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ExpertCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_07_fun_play.py b/systems/c64/cartridges/type_07_fun_play.py index 3d7dd68..9c49ad3 100644 --- a/systems/c64/cartridges/type_07_fun_play.py +++ b/systems/c64/cartridges/type_07_fun_play.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class FunPlayPowerPlayCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_08_super_games.py b/systems/c64/cartridges/type_08_super_games.py index a7d1661..4cea785 100644 --- a/systems/c64/cartridges/type_08_super_games.py +++ b/systems/c64/cartridges/type_08_super_games.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class SuperGamesCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_09_atomic_power.py b/systems/c64/cartridges/type_09_atomic_power.py index 7bc6d73..dd4ffe9 100644 --- a/systems/c64/cartridges/type_09_atomic_power.py +++ b/systems/c64/cartridges/type_09_atomic_power.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class AtomicPowerCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_10_epyx_fastload.py b/systems/c64/cartridges/type_10_epyx_fastload.py index cb470f9..27072bd 100644 --- a/systems/c64/cartridges/type_10_epyx_fastload.py +++ b/systems/c64/cartridges/type_10_epyx_fastload.py @@ -4,13 +4,17 @@ cartridge that uses a capacitor-based enable/disable mechanism. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import Cartridge, CartridgeVariant, CartridgeImage, ROML_START, ROML_SIZE, IO2_START -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -153,10 +157,10 @@ def read_io2(self, addr: int) -> int: # --- Test cartridge generation --- @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 10.""" return [ - CartridgeVariant("", exrom=0, game=1), # 8KB mode + CartridgeVariant("", 0, 1), # 8KB mode ] @classmethod @@ -204,11 +208,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: # IO2 signature at $1FF0 (maps to $DFF0 via IO2 mirror) rom_data[0x1FF0:0x1FF8] = b"IO-SIGN!" + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"roml": bytes(rom_data)}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"roml": bytes(rom_data)}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_11_westermann.py b/systems/c64/cartridges/type_11_westermann.py index 21b0f54..1249acf 100644 --- a/systems/c64/cartridges/type_11_westermann.py +++ b/systems/c64/cartridges/type_11_westermann.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class WestermannLearningCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_12_rex_utility.py b/systems/c64/cartridges/type_12_rex_utility.py index 43c6cd3..bbbb5c7 100644 --- a/systems/c64/cartridges/type_12_rex_utility.py +++ b/systems/c64/cartridges/type_12_rex_utility.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RexUtilityCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_13_final_cartridge_i.py b/systems/c64/cartridges/type_13_final_cartridge_i.py index 0bcfd86..7763c65 100644 --- a/systems/c64/cartridges/type_13_final_cartridge_i.py +++ b/systems/c64/cartridges/type_13_final_cartridge_i.py @@ -4,9 +4,8 @@ cartridge with IO-based enable/disable mechanism. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import ( Cartridge, @@ -17,8 +16,13 @@ ROMH_START, ROMH_SIZE, ) -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List, Union log = logging.getLogger("c64.cartridge") @@ -62,7 +66,7 @@ class FinalCartridgeICartridge(Cartridge): def __init__( self, roml_data: bytes, - romh_data: bytes | None = None, + romh_data: Union[bytes, None]= None, name: str = "", ): """Initialize Final Cartridge I. @@ -185,10 +189,10 @@ def write_io2(self, addr: int, data: int) -> None: # --- Test cartridge generation --- @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 13.""" return [ - CartridgeVariant("", exrom=0, game=0), # 16KB mode + CartridgeVariant("", 0, 0), # 16KB mode ] @classmethod @@ -235,11 +239,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: romh_data = bytearray(ROMH_SIZE) romh_data[0x1FF0:0x1FF8] = b"RH-SIGN!" # ROMH signature + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"roml": bytes(roml_data), "romh": bytes(romh_data)}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"roml": bytes(roml_data), "romh": bytes(romh_data)}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_14_magic_formel.py b/systems/c64/cartridges/type_14_magic_formel.py index 3bca1cf..4ac1dd0 100644 --- a/systems/c64/cartridges/type_14_magic_formel.py +++ b/systems/c64/cartridges/type_14_magic_formel.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MagicFormelCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_15_c64gs.py b/systems/c64/cartridges/type_15_c64gs.py index 957ff68..db5764e 100644 --- a/systems/c64/cartridges/type_15_c64gs.py +++ b/systems/c64/cartridges/type_15_c64gs.py @@ -4,13 +4,17 @@ organized as up to 64 x 8KB banks. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import Cartridge, CartridgeVariant, CartridgeImage, ROML_START, ROML_SIZE -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -48,7 +52,7 @@ class C64GSCartridge(Cartridge): HARDWARE_TYPE = 15 BANK_SIZE = ROML_SIZE # 8KB banks - def __init__(self, banks: list[bytes], name: str = ""): + def __init__(self, banks: List[bytes], name: str = ""): """Initialize C64GS cartridge. Args: @@ -136,12 +140,12 @@ def write_io1(self, addr: int, data: int) -> None: SIGNATURE_ADDR = 0x9FF5 # Each bank has its bank number here @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 15.""" return [ - CartridgeVariant("64k", exrom=0, game=1, extra={"bank_count": 8}), - CartridgeVariant("128k", exrom=0, game=1, extra={"bank_count": 16}), - CartridgeVariant("512k", exrom=0, game=1, extra={"bank_count": 64}), + CartridgeVariant("64k", 0, 1, {"bank_count": 8}), + CartridgeVariant("128k", 0, 1, {"bank_count": 16}), + CartridgeVariant("512k", 0, 1, {"bank_count": 64}), ] @classmethod @@ -193,11 +197,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: bank[0x1FF5] = i # Bank number as signature banks.append(bytes(bank)) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"banks": banks}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"banks": banks}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_16_warpspeed.py b/systems/c64/cartridges/type_16_warpspeed.py index d808773..8f64201 100644 --- a/systems/c64/cartridges/type_16_warpspeed.py +++ b/systems/c64/cartridges/type_16_warpspeed.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class WarpspeedCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_17_dinamic.py b/systems/c64/cartridges/type_17_dinamic.py index abf6759..4aad09f 100644 --- a/systems/c64/cartridges/type_17_dinamic.py +++ b/systems/c64/cartridges/type_17_dinamic.py @@ -4,14 +4,18 @@ Ocean Type 1 cartridges but with a 4-bit bank select (max 16 banks). """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import CartridgeVariant, CartridgeImage, ROML_START, ROML_SIZE -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from .type_05_ocean import OceanType1Cartridge from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -65,10 +69,10 @@ def write_io1(self, addr: int, data: int) -> None: SIGNATURE_ADDR = 0x9FF5 # Each bank has its bank number here @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 17.""" return [ - CartridgeVariant("128k", exrom=0, game=1, extra={"bank_count": 16}), + CartridgeVariant("128k", 0, 1, {"bank_count": 16}), ] @classmethod @@ -120,11 +124,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: bank[0x1FF5] = i # Bank number as signature banks.append(bytes(bank)) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"banks": banks}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"banks": banks}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_18_zaxxon.py b/systems/c64/cartridges/type_18_zaxxon.py index 01f00f7..94382f6 100644 --- a/systems/c64/cartridges/type_18_zaxxon.py +++ b/systems/c64/cartridges/type_18_zaxxon.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ZaxxonSuperZaxxonCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_19_magic_desk.py b/systems/c64/cartridges/type_19_magic_desk.py index 477ef2b..a31b5d3 100644 --- a/systems/c64/cartridges/type_19_magic_desk.py +++ b/systems/c64/cartridges/type_19_magic_desk.py @@ -4,13 +4,17 @@ organized as up to 64 x 8KB banks. """ -from __future__ import annotations -import logging +from mos6502.compat import logging from .base import Cartridge, CartridgeVariant, CartridgeImage, ROML_START, ROML_SIZE -from .rom_builder import TestROMBuilder +# Test ROM builder - optional for MicroPython/Pico +try: + from .rom_builder import TestROMBuilder +except ImportError: + TestROMBuilder = None from c64.colors import COLOR_BLUE, COLOR_YELLOW, COLOR_WHITE +from mos6502.compat import List log = logging.getLogger("c64.cartridge") @@ -56,7 +60,7 @@ class MagicDeskCartridge(Cartridge): HARDWARE_TYPE = 19 BANK_SIZE = ROML_SIZE # 8KB banks - def __init__(self, banks: list[bytes], name: str = ""): + def __init__(self, banks: List[bytes], name: str = ""): """Initialize Magic Desk cartridge. Args: @@ -141,14 +145,14 @@ def write_io1(self, addr: int, data: int) -> None: SIGNATURE_ADDR = 0x9FF5 # Each bank has its bank number here @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return all valid configuration variants for Type 19.""" return [ - CartridgeVariant("32k", exrom=0, game=1, extra={"bank_count": 4}), - CartridgeVariant("64k", exrom=0, game=1, extra={"bank_count": 8}), - CartridgeVariant("128k", exrom=0, game=1, extra={"bank_count": 16}), - CartridgeVariant("256k", exrom=0, game=1, extra={"bank_count": 32}), - CartridgeVariant("512k", exrom=0, game=1, extra={"bank_count": 64}), + CartridgeVariant("32k", 0, 1, {"bank_count": 4}), + CartridgeVariant("64k", 0, 1, {"bank_count": 8}), + CartridgeVariant("128k", 0, 1, {"bank_count": 16}), + CartridgeVariant("256k", 0, 1, {"bank_count": 32}), + CartridgeVariant("512k", 0, 1, {"bank_count": 64}), ] @classmethod @@ -200,11 +204,8 @@ def create_test_cartridge(cls, variant: CartridgeVariant) -> CartridgeImage: bank[0x1FF5] = i # Bank number as signature banks.append(bytes(bank)) + # CartridgeImage field order: description, exrom, game, extra, rom_data, hardware_type return CartridgeImage( - description=variant.description, - exrom=variant.exrom, - game=variant.game, - extra=variant.extra, - rom_data={"banks": banks}, - hardware_type=cls.HARDWARE_TYPE, + variant.description, variant.exrom, variant.game, variant.extra, + {"banks": banks}, cls.HARDWARE_TYPE ) diff --git a/systems/c64/cartridges/type_20_super_snapshot_v5.py b/systems/c64/cartridges/type_20_super_snapshot_v5.py index 581d336..7076fc3 100644 --- a/systems/c64/cartridges/type_20_super_snapshot_v5.py +++ b/systems/c64/cartridges/type_20_super_snapshot_v5.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class SuperSnapshotV5Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_21_comal80.py b/systems/c64/cartridges/type_21_comal80.py index 3293be2..4216aba 100644 --- a/systems/c64/cartridges/type_21_comal80.py +++ b/systems/c64/cartridges/type_21_comal80.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Comal80Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_22_structured_basic.py b/systems/c64/cartridges/type_22_structured_basic.py index 9b6170d..c4009dc 100644 --- a/systems/c64/cartridges/type_22_structured_basic.py +++ b/systems/c64/cartridges/type_22_structured_basic.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class StructuredBasicCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_23_ross.py b/systems/c64/cartridges/type_23_ross.py index f9a1f6a..0195e59 100644 --- a/systems/c64/cartridges/type_23_ross.py +++ b/systems/c64/cartridges/type_23_ross.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RossCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_24_dela_ep64.py b/systems/c64/cartridges/type_24_dela_ep64.py index 4c950f6..a3c02c0 100644 --- a/systems/c64/cartridges/type_24_dela_ep64.py +++ b/systems/c64/cartridges/type_24_dela_ep64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class DelaEp64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_25_dela_ep7x8.py b/systems/c64/cartridges/type_25_dela_ep7x8.py index b207fa2..0c502a0 100644 --- a/systems/c64/cartridges/type_25_dela_ep7x8.py +++ b/systems/c64/cartridges/type_25_dela_ep7x8.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class DelaEp7X8Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_26_dela_ep256.py b/systems/c64/cartridges/type_26_dela_ep256.py index aed0294..22e3dbd 100644 --- a/systems/c64/cartridges/type_26_dela_ep256.py +++ b/systems/c64/cartridges/type_26_dela_ep256.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class DelaEp256Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_27_rex_ep256.py b/systems/c64/cartridges/type_27_rex_ep256.py index 31737ab..52e156f 100644 --- a/systems/c64/cartridges/type_27_rex_ep256.py +++ b/systems/c64/cartridges/type_27_rex_ep256.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RexEp256Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_28_mikro_assembler.py b/systems/c64/cartridges/type_28_mikro_assembler.py index 25f8c02..25e2519 100644 --- a/systems/c64/cartridges/type_28_mikro_assembler.py +++ b/systems/c64/cartridges/type_28_mikro_assembler.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MikroAssemblerCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_29_final_cartridge_plus.py b/systems/c64/cartridges/type_29_final_cartridge_plus.py index 5603038..191c4aa 100644 --- a/systems/c64/cartridges/type_29_final_cartridge_plus.py +++ b/systems/c64/cartridges/type_29_final_cartridge_plus.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class FinalCartridgePlusCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_30_action_replay_4.py b/systems/c64/cartridges/type_30_action_replay_4.py index a905711..33b1f8a 100644 --- a/systems/c64/cartridges/type_30_action_replay_4.py +++ b/systems/c64/cartridges/type_30_action_replay_4.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ActionReplay4Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_31_stardos.py b/systems/c64/cartridges/type_31_stardos.py index 2ffdae5..e1f0fde 100644 --- a/systems/c64/cartridges/type_31_stardos.py +++ b/systems/c64/cartridges/type_31_stardos.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class StardosCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_32_easyflash.py b/systems/c64/cartridges/type_32_easyflash.py index b782fbd..5488a39 100644 --- a/systems/c64/cartridges/type_32_easyflash.py +++ b/systems/c64/cartridges/type_32_easyflash.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class EasyflashCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_33_easyflash_xbank.py b/systems/c64/cartridges/type_33_easyflash_xbank.py index a08a8de..c88d64b 100644 --- a/systems/c64/cartridges/type_33_easyflash_xbank.py +++ b/systems/c64/cartridges/type_33_easyflash_xbank.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class EasyflashXBankCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_34_capture.py b/systems/c64/cartridges/type_34_capture.py index cc8aded..041d3f7 100644 --- a/systems/c64/cartridges/type_34_capture.py +++ b/systems/c64/cartridges/type_34_capture.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class CaptureCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_35_action_replay_3.py b/systems/c64/cartridges/type_35_action_replay_3.py index 520f6d7..1fa1eb5 100644 --- a/systems/c64/cartridges/type_35_action_replay_3.py +++ b/systems/c64/cartridges/type_35_action_replay_3.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ActionReplay3Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_36_retro_replay.py b/systems/c64/cartridges/type_36_retro_replay.py index 4bb4b13..666135b 100644 --- a/systems/c64/cartridges/type_36_retro_replay.py +++ b/systems/c64/cartridges/type_36_retro_replay.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RetroReplayCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_37_mmc64.py b/systems/c64/cartridges/type_37_mmc64.py index cf49909..67e7a60 100644 --- a/systems/c64/cartridges/type_37_mmc64.py +++ b/systems/c64/cartridges/type_37_mmc64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Mmc64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_38_mmc_replay.py b/systems/c64/cartridges/type_38_mmc_replay.py index 4007ccf..d04320b 100644 --- a/systems/c64/cartridges/type_38_mmc_replay.py +++ b/systems/c64/cartridges/type_38_mmc_replay.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MmcReplayCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_39_ide64.py b/systems/c64/cartridges/type_39_ide64.py index ff419da..929286e 100644 --- a/systems/c64/cartridges/type_39_ide64.py +++ b/systems/c64/cartridges/type_39_ide64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Ide64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_40_super_snapshot_v4.py b/systems/c64/cartridges/type_40_super_snapshot_v4.py index 8f9df45..df6ce92 100644 --- a/systems/c64/cartridges/type_40_super_snapshot_v4.py +++ b/systems/c64/cartridges/type_40_super_snapshot_v4.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class SuperSnapshotV4Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_41_ieee488.py b/systems/c64/cartridges/type_41_ieee488.py index 12e1df1..9718d5c 100644 --- a/systems/c64/cartridges/type_41_ieee488.py +++ b/systems/c64/cartridges/type_41_ieee488.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Ieee488Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_42_game_killer.py b/systems/c64/cartridges/type_42_game_killer.py index 5a27e64..6cea5b5 100644 --- a/systems/c64/cartridges/type_42_game_killer.py +++ b/systems/c64/cartridges/type_42_game_killer.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class GameKillerCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_43_prophet64.py b/systems/c64/cartridges/type_43_prophet64.py index 8ec6dba..4922440 100644 --- a/systems/c64/cartridges/type_43_prophet64.py +++ b/systems/c64/cartridges/type_43_prophet64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Prophet64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_44_exos.py b/systems/c64/cartridges/type_44_exos.py index 9a4d322..3f4c75f 100644 --- a/systems/c64/cartridges/type_44_exos.py +++ b/systems/c64/cartridges/type_44_exos.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ExosCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_45_freeze_frame.py b/systems/c64/cartridges/type_45_freeze_frame.py index 7e976cb..7f810e8 100644 --- a/systems/c64/cartridges/type_45_freeze_frame.py +++ b/systems/c64/cartridges/type_45_freeze_frame.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class FreezeFrameCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_46_freeze_machine.py b/systems/c64/cartridges/type_46_freeze_machine.py index a00be82..7ad17ff 100644 --- a/systems/c64/cartridges/type_46_freeze_machine.py +++ b/systems/c64/cartridges/type_46_freeze_machine.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class FreezeMachineCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_47_snapshot64.py b/systems/c64/cartridges/type_47_snapshot64.py index 03e1159..c46f4ec 100644 --- a/systems/c64/cartridges/type_47_snapshot64.py +++ b/systems/c64/cartridges/type_47_snapshot64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Snapshot64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_48_super_explode_v5.py b/systems/c64/cartridges/type_48_super_explode_v5.py index 9fcbe31..987b19b 100644 --- a/systems/c64/cartridges/type_48_super_explode_v5.py +++ b/systems/c64/cartridges/type_48_super_explode_v5.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class SuperExplodeV5Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_49_magic_voice.py b/systems/c64/cartridges/type_49_magic_voice.py index e39d851..3b24ddd 100644 --- a/systems/c64/cartridges/type_49_magic_voice.py +++ b/systems/c64/cartridges/type_49_magic_voice.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MagicVoiceCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_50_action_replay_2.py b/systems/c64/cartridges/type_50_action_replay_2.py index 1176b5e..2d20f1f 100644 --- a/systems/c64/cartridges/type_50_action_replay_2.py +++ b/systems/c64/cartridges/type_50_action_replay_2.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ActionReplay2Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_51_mach5.py b/systems/c64/cartridges/type_51_mach5.py index 07ba237..d35dfa0 100644 --- a/systems/c64/cartridges/type_51_mach5.py +++ b/systems/c64/cartridges/type_51_mach5.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Mach5Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_52_diashow_maker.py b/systems/c64/cartridges/type_52_diashow_maker.py index 2b3102b..a1694e5 100644 --- a/systems/c64/cartridges/type_52_diashow_maker.py +++ b/systems/c64/cartridges/type_52_diashow_maker.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class DiashowMakerCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_53_pagefox.py b/systems/c64/cartridges/type_53_pagefox.py index f9bf094..ae52753 100644 --- a/systems/c64/cartridges/type_53_pagefox.py +++ b/systems/c64/cartridges/type_53_pagefox.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class PagefoxCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_54_kingsoft.py b/systems/c64/cartridges/type_54_kingsoft.py index 890e91c..ad1c834 100644 --- a/systems/c64/cartridges/type_54_kingsoft.py +++ b/systems/c64/cartridges/type_54_kingsoft.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class KingsoftBusinessBasicCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_55_silver_rock_128.py b/systems/c64/cartridges/type_55_silver_rock_128.py index cb4bc6f..b3bc473 100644 --- a/systems/c64/cartridges/type_55_silver_rock_128.py +++ b/systems/c64/cartridges/type_55_silver_rock_128.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class SilverRock128Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_56_formel64.py b/systems/c64/cartridges/type_56_formel64.py index 97c3434..e5f3d65 100644 --- a/systems/c64/cartridges/type_56_formel64.py +++ b/systems/c64/cartridges/type_56_formel64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Formel64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_57_rgcd.py b/systems/c64/cartridges/type_57_rgcd.py index 3a245eb..c8484b1 100644 --- a/systems/c64/cartridges/type_57_rgcd.py +++ b/systems/c64/cartridges/type_57_rgcd.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RgcdCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_58_rrnet_mk3.py b/systems/c64/cartridges/type_58_rrnet_mk3.py index 64205ad..acae673 100644 --- a/systems/c64/cartridges/type_58_rrnet_mk3.py +++ b/systems/c64/cartridges/type_58_rrnet_mk3.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RrNetMk3Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_59_easy_calc.py b/systems/c64/cartridges/type_59_easy_calc.py index 01e9dcf..87dec21 100644 --- a/systems/c64/cartridges/type_59_easy_calc.py +++ b/systems/c64/cartridges/type_59_easy_calc.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class EasyCalcResultCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_60_gmod2.py b/systems/c64/cartridges/type_60_gmod2.py index 813334e..02906f2 100644 --- a/systems/c64/cartridges/type_60_gmod2.py +++ b/systems/c64/cartridges/type_60_gmod2.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Gmod2Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_61_max_basic.py b/systems/c64/cartridges/type_61_max_basic.py index 923e87d..2020f5d 100644 --- a/systems/c64/cartridges/type_61_max_basic.py +++ b/systems/c64/cartridges/type_61_max_basic.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MaxBasicCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_62_gmod3.py b/systems/c64/cartridges/type_62_gmod3.py index 21145a1..fac5ea2 100644 --- a/systems/c64/cartridges/type_62_gmod3.py +++ b/systems/c64/cartridges/type_62_gmod3.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Gmod3Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_63_zippcode48.py b/systems/c64/cartridges/type_63_zippcode48.py index 2fed045..f3aec66 100644 --- a/systems/c64/cartridges/type_63_zippcode48.py +++ b/systems/c64/cartridges/type_63_zippcode48.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ZippCode48Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_64_blackbox_v8.py b/systems/c64/cartridges/type_64_blackbox_v8.py index 465869d..482d188 100644 --- a/systems/c64/cartridges/type_64_blackbox_v8.py +++ b/systems/c64/cartridges/type_64_blackbox_v8.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class BlackboxV8Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_65_blackbox_v3.py b/systems/c64/cartridges/type_65_blackbox_v3.py index 23d18cf..d51bae1 100644 --- a/systems/c64/cartridges/type_65_blackbox_v3.py +++ b/systems/c64/cartridges/type_65_blackbox_v3.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class BlackboxV3Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_66_blackbox_v4.py b/systems/c64/cartridges/type_66_blackbox_v4.py index 126b737..4b27db9 100644 --- a/systems/c64/cartridges/type_66_blackbox_v4.py +++ b/systems/c64/cartridges/type_66_blackbox_v4.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class BlackboxV4Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_67_rex_ram_floppy.py b/systems/c64/cartridges/type_67_rex_ram_floppy.py index b998faa..12a36ab 100644 --- a/systems/c64/cartridges/type_67_rex_ram_floppy.py +++ b/systems/c64/cartridges/type_67_rex_ram_floppy.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class RexRamFloppyCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_68_bis_plus.py b/systems/c64/cartridges/type_68_bis_plus.py index 462ba07..b1ca247 100644 --- a/systems/c64/cartridges/type_68_bis_plus.py +++ b/systems/c64/cartridges/type_68_bis_plus.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class BisPlusCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_69_sd_box.py b/systems/c64/cartridges/type_69_sd_box.py index ec2dd57..9cfeaa7 100644 --- a/systems/c64/cartridges/type_69_sd_box.py +++ b/systems/c64/cartridges/type_69_sd_box.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class SdBoxCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_70_multimax.py b/systems/c64/cartridges/type_70_multimax.py index 35925bf..b5bd689 100644 --- a/systems/c64/cartridges/type_70_multimax.py +++ b/systems/c64/cartridges/type_70_multimax.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MultimaxCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_71_blackbox_v9.py b/systems/c64/cartridges/type_71_blackbox_v9.py index 2cb3774..ce48375 100644 --- a/systems/c64/cartridges/type_71_blackbox_v9.py +++ b/systems/c64/cartridges/type_71_blackbox_v9.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class BlackboxV9Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_72_lt_kernal.py b/systems/c64/cartridges/type_72_lt_kernal.py index 21922de..24512f8 100644 --- a/systems/c64/cartridges/type_72_lt_kernal.py +++ b/systems/c64/cartridges/type_72_lt_kernal.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class LtKernalCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_73_cmd_ramlink.py b/systems/c64/cartridges/type_73_cmd_ramlink.py index 39e32f5..3d669e2 100644 --- a/systems/c64/cartridges/type_73_cmd_ramlink.py +++ b/systems/c64/cartridges/type_73_cmd_ramlink.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class CmdRamlinkCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_74_drean.py b/systems/c64/cartridges/type_74_drean.py index 5555dd6..d763d61 100644 --- a/systems/c64/cartridges/type_74_drean.py +++ b/systems/c64/cartridges/type_74_drean.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class DreanCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_75_ieee_flash_64.py b/systems/c64/cartridges/type_75_ieee_flash_64.py index 7173cce..ed04379 100644 --- a/systems/c64/cartridges/type_75_ieee_flash_64.py +++ b/systems/c64/cartridges/type_75_ieee_flash_64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class IeeeFlash64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_76_turtle_graphics_ii.py b/systems/c64/cartridges/type_76_turtle_graphics_ii.py index 1d810a4..994987a 100644 --- a/systems/c64/cartridges/type_76_turtle_graphics_ii.py +++ b/systems/c64/cartridges/type_76_turtle_graphics_ii.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class TurtleGraphicsIiCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_77_freeze_frame_mk2.py b/systems/c64/cartridges/type_77_freeze_frame_mk2.py index c7d0b60..8605cc6 100644 --- a/systems/c64/cartridges/type_77_freeze_frame_mk2.py +++ b/systems/c64/cartridges/type_77_freeze_frame_mk2.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class FreezeFrameMk2Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_78_partner64.py b/systems/c64/cartridges/type_78_partner64.py index dbc2848..dd2f481 100644 --- a/systems/c64/cartridges/type_78_partner64.py +++ b/systems/c64/cartridges/type_78_partner64.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class Partner64Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_79_hyper_basic_mk2.py b/systems/c64/cartridges/type_79_hyper_basic_mk2.py index 5995a1f..ce5712e 100644 --- a/systems/c64/cartridges/type_79_hyper_basic_mk2.py +++ b/systems/c64/cartridges/type_79_hyper_basic_mk2.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class HyperBasicMk2Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_80_universal_1.py b/systems/c64/cartridges/type_80_universal_1.py index d0acdd3..f6c17b2 100644 --- a/systems/c64/cartridges/type_80_universal_1.py +++ b/systems/c64/cartridges/type_80_universal_1.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class UniversalCartridge1Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_81_universal_15.py b/systems/c64/cartridges/type_81_universal_15.py index 47ac340..4255ac0 100644 --- a/systems/c64/cartridges/type_81_universal_15.py +++ b/systems/c64/cartridges/type_81_universal_15.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class UniversalCartridge15Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_82_universal_2.py b/systems/c64/cartridges/type_82_universal_2.py index 0bc0595..6ce0795 100644 --- a/systems/c64/cartridges/type_82_universal_2.py +++ b/systems/c64/cartridges/type_82_universal_2.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class UniversalCartridge2Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_83_bmp_turbo_2000.py b/systems/c64/cartridges/type_83_bmp_turbo_2000.py index 0889c75..e000a6d 100644 --- a/systems/c64/cartridges/type_83_bmp_turbo_2000.py +++ b/systems/c64/cartridges/type_83_bmp_turbo_2000.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class BmpDataTurbo2000Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_84_profi_dos.py b/systems/c64/cartridges/type_84_profi_dos.py index 7f7e39e..524c915 100644 --- a/systems/c64/cartridges/type_84_profi_dos.py +++ b/systems/c64/cartridges/type_84_profi_dos.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class ProfiDosCartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cartridges/type_85_magic_desk_16.py b/systems/c64/cartridges/type_85_magic_desk_16.py index ebe9c5b..929a089 100644 --- a/systems/c64/cartridges/type_85_magic_desk_16.py +++ b/systems/c64/cartridges/type_85_magic_desk_16.py @@ -3,7 +3,6 @@ NOT YET IMPLEMENTED - This is a placeholder that generates error cartridges. """ -from __future__ import annotations from .base import ( Cartridge, @@ -12,6 +11,7 @@ ROML_SIZE, create_error_cartridge_rom, ) +from mos6502.compat import List class MagicDesk16Cartridge(Cartridge): @@ -33,7 +33,7 @@ def read_roml(self, addr: int) -> int: return 0xFF @classmethod - def get_cartridge_variants(cls) -> list[CartridgeVariant]: + def get_cartridge_variants(cls) -> List[CartridgeVariant]: """Return single variant for error cart generation.""" return [CartridgeVariant("", exrom=0, game=1)] diff --git a/systems/c64/cia1.py b/systems/c64/cia1.py index 6603cd4..f0180b1 100644 --- a/systems/c64/cia1.py +++ b/systems/c64/cia1.py @@ -10,11 +10,26 @@ - IRQ generation """ -from __future__ import annotations -import logging -import threading -from typing import TYPE_CHECKING +from mos6502.compat import logging +from mos6502.compat import TYPE_CHECKING, Union + +# Threading - optional for MicroPython/Pico (single-threaded, no lock needed) +try: + import threading +except ImportError: + # Provide a dummy Lock for single-threaded MicroPython + class _DummyLock: + def __enter__(self): + return self + def __exit__(self, *args): + pass + def acquire(self, blocking=True, timeout=-1): + return True + def release(self): + pass + class threading: # noqa: N801 + Lock = _DummyLock if TYPE_CHECKING: from mos6502.core import MOS6502CPU @@ -70,7 +85,7 @@ class CIA1: - IRQ generation """ - def __init__(self, cpu: MOS6502CPU) -> None: + def __init__(self, cpu: "MOS6502CPU") -> None: # 16 registers, mirrored through $DC00–$DC0F self.regs = [0x00] * 16 @@ -1045,7 +1060,7 @@ def release_key(self, row: int, col: int) -> None: # Clear press time tracking self._key_press_times.pop((row, col), None) - def get_key_press_time(self, row: int, col: int) -> float | None: + def get_key_press_time(self, row: int, col: int) -> Union[float, None]: """Get when a key was pressed, or None if not pressed. Used to enforce minimum key hold duration for fast typists. diff --git a/systems/c64/cia2.py b/systems/c64/cia2.py index 34e8c4e..c3b7c01 100644 --- a/systems/c64/cia2.py +++ b/systems/c64/cia2.py @@ -11,10 +11,9 @@ - NMI generation (directly to CPU) """ -from __future__ import annotations -import logging -from typing import TYPE_CHECKING +from mos6502.compat import logging +from mos6502.compat import TYPE_CHECKING if TYPE_CHECKING: from mos6502.core import MOS6502CPU @@ -35,7 +34,7 @@ class CIA2: - NMI generation (directly to CPU) """ - def __init__(self, cpu: MOS6502CPU) -> None: + def __init__(self, cpu: "MOS6502CPU") -> None: # 16 registers, mirrored through $DD00–$DD0F self.regs = [0x00] * 16 diff --git a/systems/c64/drive/__init__.py b/systems/c64/drive/__init__.py index c8aa74a..f5bc01e 100644 --- a/systems/c64/drive/__init__.py +++ b/systems/c64/drive/__init__.py @@ -21,10 +21,27 @@ from .d64 import D64Image from .drive1541 import Drive1541 from .iec_bus import IECBus -from .threaded_iec_bus import ThreadedIECBus -from .threaded_drive import ThreadedDrive1541 -from .multiprocess_iec_bus import MultiprocessIECBus, SharedIECState -from .multiprocess_drive import MultiprocessDrive1541 + +# Threaded/multiprocess drive modules - optional for MicroPython/Pico +# These require threading/multiprocessing which aren't available on Pico +_THREADED_AVAILABLE = False +_MULTIPROCESS_AVAILABLE = False +try: + from .threaded_iec_bus import ThreadedIECBus + from .threaded_drive import ThreadedDrive1541 + _THREADED_AVAILABLE = True +except ImportError: + ThreadedIECBus = None + ThreadedDrive1541 = None + +try: + from .multiprocess_iec_bus import MultiprocessIECBus, SharedIECState + from .multiprocess_drive import MultiprocessDrive1541 + _MULTIPROCESS_AVAILABLE = True +except ImportError: + MultiprocessIECBus = None + SharedIECState = None + MultiprocessDrive1541 = None __all__ = [ "VIA6522", @@ -36,4 +53,6 @@ "MultiprocessIECBus", "SharedIECState", "MultiprocessDrive1541", + "_THREADED_AVAILABLE", + "_MULTIPROCESS_AVAILABLE", ] diff --git a/systems/c64/drive/d64.py b/systems/c64/drive/d64.py index 53a63f9..d4a5b6b 100644 --- a/systems/c64/drive/d64.py +++ b/systems/c64/drive/d64.py @@ -29,12 +29,11 @@ - https://www.c64-wiki.com/wiki/D64 """ -from __future__ import annotations -import logging -from pathlib import Path -from typing import List, Optional, Tuple, Union -from dataclasses import dataclass +from mos6502.compat import logging +from mos6502.compat import Path +from mos6502.compat import List, Optional, Tuple, Union +from mos6502.compat import dataclass log = logging.getLogger("d64") diff --git a/systems/c64/drive/drive1541.py b/systems/c64/drive/drive1541.py index 7edad47..3a631b4 100644 --- a/systems/c64/drive/drive1541.py +++ b/systems/c64/drive/drive1541.py @@ -38,11 +38,10 @@ - https://sta.c64.org/cbm1541mem.html """ -from __future__ import annotations -import logging -from pathlib import Path -from typing import TYPE_CHECKING, Optional +from mos6502.compat import logging +from mos6502.compat import Path +from mos6502.compat import TYPE_CHECKING, Optional, List from .via6522 import VIA6522 from .d64 import D64Image, SECTORS_PER_TRACK, TRACK_SPEED_ZONE @@ -206,7 +205,7 @@ def __init__(self, device_number: int = 8) -> None: self.device_number = device_number # CPU will be set by the C64 when the drive is attached - self.cpu: Optional[MOS6502CPU] = None + self.cpu: Optional["MOS6502CPU"] = None # Memory subsystem self.memory = Drive1541Memory(self) @@ -284,7 +283,7 @@ def __init__(self, device_number: int = 8) -> None: # CB2 LOW (bits 7-5 = 110) = Write mode # CB2 HIGH (bits 7-5 = 111) = Read mode self._write_mode = False - self._gcr_write_buffer: list[int] = [] + self._gcr_write_buffer: List[int] = [] self._write_track = 0 # Track being written to self._d64_path: Optional[Path] = None # Path for persistence diff --git a/systems/c64/drive/gcr.py b/systems/c64/drive/gcr.py index 27b1d31..a8172fa 100644 --- a/systems/c64/drive/gcr.py +++ b/systems/c64/drive/gcr.py @@ -33,10 +33,9 @@ - http://www.baltissen.org/newhtm/1541c.htm """ -from __future__ import annotations -import logging -from typing import List, Optional, Tuple, TYPE_CHECKING +from mos6502.compat import logging +from mos6502.compat import List, Optional, Tuple, TYPE_CHECKING if TYPE_CHECKING: from .d64 import D64Image @@ -404,7 +403,7 @@ def __init__(self, track_num: int, num_sectors: int, speed_zone: int) -> None: for i in range(self.track_size): self.data[i] = GAP_BYTE - def build_from_d64(self, d64: D64Image, disk_id: bytes) -> None: + def build_from_d64(self, d64: "D64Image", disk_id: bytes) -> None: """Build GCR track data from D64 image. Args: @@ -534,7 +533,7 @@ def get_sector_offset(self, sector: int) -> int: """ return sector * 372 - def update_sector_from_d64(self, d64: D64Image, sector: int, disk_id: bytes) -> None: + def update_sector_from_d64(self, d64: "D64Image", sector: int, disk_id: bytes) -> None: """Update a single sector from D64 image data. This re-encodes just the specified sector, preserving the rest of the track. @@ -585,7 +584,7 @@ class GCRDisk: to read/write data. """ - def __init__(self, d64: Optional[D64Image] = None) -> None: + def __init__(self, d64: Optional["D64Image"] = None) -> None: """Initialize GCR disk. Args: diff --git a/systems/c64/drive/iec_bus.py b/systems/c64/drive/iec_bus.py index 8863015..0e9eee4 100644 --- a/systems/c64/drive/iec_bus.py +++ b/systems/c64/drive/iec_bus.py @@ -37,10 +37,9 @@ - https://www.c64-wiki.com/wiki/Serial_Port """ -from __future__ import annotations -import logging -from typing import TYPE_CHECKING, List, Optional +from mos6502.compat import logging +from mos6502.compat import TYPE_CHECKING, List, Optional if TYPE_CHECKING: from ..cia2 import CIA2 @@ -59,8 +58,8 @@ class IECBus: def __init__(self) -> None: """Initialize the IEC bus.""" # Connected devices - self.cia2: Optional[CIA2] = None # C64's CIA2 - self.drives: List[Drive1541] = [] + self.cia2: Optional["CIA2"] = None # C64's CIA2 + self.drives: List["Drive1541"] = [] # Bus line states (True = released/high, False = asserted/low) # These represent the actual bus state after open-collector resolution @@ -83,7 +82,7 @@ def __init__(self) -> None: # Avoids recomputing bus state when nothing has changed self._last_input_state = None # Invalid initial value to force first update - def connect_c64(self, cia2: CIA2) -> None: + def connect_c64(self, cia2: "CIA2") -> None: """Connect C64's CIA2 to the bus. Args: @@ -92,7 +91,7 @@ def connect_c64(self, cia2: CIA2) -> None: self.cia2 = cia2 log.info("C64 connected to IEC bus") - def connect_drive(self, drive: Drive1541) -> None: + def connect_drive(self, drive: "Drive1541") -> None: """Connect a drive to the bus. Args: @@ -103,7 +102,7 @@ def connect_drive(self, drive: Drive1541) -> None: drive.iec_bus = self # Give drive reference to bus for immediate updates log.info(f"Drive {drive.device_number} connected to IEC bus") - def disconnect_drive(self, drive: Drive1541) -> None: + def disconnect_drive(self, drive: "Drive1541") -> None: """Disconnect a drive from the bus. Args: diff --git a/systems/c64/drive/multiprocess_drive.py b/systems/c64/drive/multiprocess_drive.py index e0f0cf1..93541c7 100644 --- a/systems/c64/drive/multiprocess_drive.py +++ b/systems/c64/drive/multiprocess_drive.py @@ -10,15 +10,14 @@ - Commands (disk insert/eject) via multiprocessing.Queue """ -from __future__ import annotations -import logging +from mos6502.compat import logging import multiprocessing import os import sys import time -from pathlib import Path -from typing import Optional +from mos6502.compat import Path +from mos6502.compat import Optional # Use fork context on POSIX for better performance # (spawn has overhead of reimporting modules) @@ -82,10 +81,10 @@ def drive_process_main( shared_state = SharedIECState(name=shared_mem_name, create=False) # Create drive instance - drive = Drive1541(device_number=device_number) + drive = Drive1541(device_number) # Create CPU for the drive (1541 uses standard 6502) - drive_cpu = CPU(cpu_variant=CPUVariant.NMOS_6502, verbose_cycles=False) + drive_cpu = CPU(CPUVariant.NMOS_6502, False) drive.cpu = drive_cpu drive_cpu.ram.memory_handler = drive.memory diff --git a/systems/c64/drive/multiprocess_iec_bus.py b/systems/c64/drive/multiprocess_iec_bus.py index 7574f56..e210412 100644 --- a/systems/c64/drive/multiprocess_iec_bus.py +++ b/systems/c64/drive/multiprocess_iec_bus.py @@ -19,12 +19,11 @@ - Lines go high only when ALL devices release them """ -from __future__ import annotations -import logging +from mos6502.compat import logging import struct from multiprocessing import shared_memory -from typing import TYPE_CHECKING, Optional +from mos6502.compat import TYPE_CHECKING, Optional, Tuple if TYPE_CHECKING: from ..cia2 import CIA2 @@ -141,7 +140,7 @@ def set_c64_outputs(self, atn_out: bool, clk_out: bool, data_out: bool) -> None: value |= C64_DATA_OUT_BIT self._shm.buf[OFFSET_C64_OUTPUTS] = value - def get_c64_outputs(self) -> tuple[bool, bool, bool]: + def get_c64_outputs(self) -> Tuple[bool, bool, bool]: """Get C64's IEC output state. Returns: @@ -173,7 +172,7 @@ def set_drive_outputs(self, clk_out: bool, data_out: bool, atna_out: bool) -> No value |= DRIVE_ATNA_OUT_BIT self._shm.buf[OFFSET_DRIVE_OUTPUTS] = value - def get_drive_outputs(self) -> tuple[bool, bool, bool]: + def get_drive_outputs(self) -> Tuple[bool, bool, bool]: """Get drive's IEC output state. Returns: @@ -188,7 +187,7 @@ def get_drive_outputs(self) -> tuple[bool, bool, bool]: # --- Bus State Computation --- - def get_bus_state(self, is_drive: bool = False) -> tuple[bool, bool, bool]: + def get_bus_state(self, is_drive: bool = False) -> Tuple[bool, bool, bool]: """Compute combined bus state using open-collector logic. Args: @@ -353,14 +352,14 @@ def __init__(self, shared_state: SharedIECState) -> None: shared_state: SharedIECState instance for IPC """ self._shared = shared_state - self.cia2: Optional[CIA2] = None + self.cia2: Optional["CIA2"] = None # Cached bus state for get_c64_input() speed self.atn = True self.clk = True self.data = True - def connect_c64(self, cia2: CIA2) -> None: + def connect_c64(self, cia2: "CIA2") -> None: """Connect C64's CIA2 to the bus. Args: diff --git a/systems/c64/drive/threaded_drive.py b/systems/c64/drive/threaded_drive.py index 606bec8..2616159 100644 --- a/systems/c64/drive/threaded_drive.py +++ b/systems/c64/drive/threaded_drive.py @@ -13,12 +13,11 @@ actively accessing the disk, as the two CPUs can run in parallel. """ -from __future__ import annotations -import logging +from mos6502.compat import logging import threading import time -from typing import TYPE_CHECKING, Optional +from mos6502.compat import TYPE_CHECKING, Optional from .drive1541 import Drive1541, VIA1_DATA_OUT, VIA1_CLK_OUT, VIA1_ATN_ACK from .threaded_iec_bus import ThreadedIECBus diff --git a/systems/c64/drive/threaded_iec_bus.py b/systems/c64/drive/threaded_iec_bus.py index b4f9a88..5c21142 100644 --- a/systems/c64/drive/threaded_iec_bus.py +++ b/systems/c64/drive/threaded_iec_bus.py @@ -27,11 +27,10 @@ - https://www.c64-wiki.com/wiki/Serial_Port """ -from __future__ import annotations -import logging +from mos6502.compat import logging import threading -from typing import TYPE_CHECKING, Dict, Optional +from mos6502.compat import TYPE_CHECKING, Dict, Optional if TYPE_CHECKING: from ..cia2 import CIA2 @@ -60,8 +59,8 @@ def __init__(self) -> None: self._cycle_condition = threading.Condition(self._lock) # Connected devices (protected by lock for modification) - self.cia2: Optional[CIA2] = None - self._drives: Dict[int, Drive1541] = {} # device_number -> drive + self.cia2: Optional["CIA2"] = None + self._drives: Dict[int, "Drive1541"] = {} # device_number -> drive self._drives_list: list = [] # Cached list for fast iteration in update() # Bus line states (True = released/high, False = asserted/low) @@ -96,7 +95,7 @@ def drives(self): with self._lock: return list(self._drives.values()) - def connect_c64(self, cia2: CIA2) -> None: + def connect_c64(self, cia2: "CIA2") -> None: """Connect C64's CIA2 to the bus. Args: @@ -106,7 +105,7 @@ def connect_c64(self, cia2: CIA2) -> None: self.cia2 = cia2 log.info("C64 connected to threaded IEC bus") - def connect_drive(self, drive: Drive1541) -> None: + def connect_drive(self, drive: "Drive1541") -> None: """Connect a drive to the bus. Args: @@ -120,7 +119,7 @@ def connect_drive(self, drive: Drive1541) -> None: drive.iec_bus = self log.info(f"Drive {drive.device_number} connected to threaded IEC bus") - def disconnect_drive(self, drive: Drive1541) -> None: + def disconnect_drive(self, drive: "Drive1541") -> None: """Disconnect a drive from the bus. Args: diff --git a/systems/c64/drive/via6522.py b/systems/c64/drive/via6522.py index fdd9a77..08aa999 100644 --- a/systems/c64/drive/via6522.py +++ b/systems/c64/drive/via6522.py @@ -30,10 +30,9 @@ - http://archive.6502.org/datasheets/mos_6522_preliminary_nov_1977.pdf """ -from __future__ import annotations -import logging -from typing import TYPE_CHECKING, Callable, Optional +from mos6502.compat import logging +from mos6502.compat import TYPE_CHECKING, Callable, Optional if TYPE_CHECKING: pass diff --git a/systems/c64/memory.py b/systems/c64/memory.py index 3198eda..872c124 100644 --- a/systems/c64/memory.py +++ b/systems/c64/memory.py @@ -6,22 +6,36 @@ - C64Memory class that implements the C64's memory banking and I/O mapping """ -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, Optional - -from c64.cartridges import ( - Cartridge, - ROML_START, - ROML_END, - ROMH_START, - ROMH_END, - IO1_START, - IO1_END, - IO2_START, - IO2_END, -) + +from mos6502.compat import logging +from mos6502.compat import TYPE_CHECKING, Optional + +# Cartridge support is optional (not available on Pico due to kwargs limitations) +try: + from c64.cartridges import ( + Cartridge, + ROML_START, + ROML_END, + ROMH_START, + ROMH_END, + IO1_START, + IO1_END, + IO2_START, + IO2_END, + ) + _CARTRIDGES_AVAILABLE = True +except (ImportError, TypeError): + # Stub values for when cartridges module is not available + _CARTRIDGES_AVAILABLE = False + Cartridge = None + ROML_START = 0x8000 + ROML_END = 0x9FFF + ROMH_START = 0xA000 + ROMH_END = 0xBFFF + IO1_START = 0xDE00 + IO1_END = 0xDEFF + IO2_START = 0xDF00 + IO2_END = 0xDFFF if TYPE_CHECKING: from c64.cia1 import CIA1 @@ -81,7 +95,7 @@ class C64Memory: - Cartridge ROM support (ROML, ROMH, Ultimax mode) """ - def __init__(self, ram, *, basic_rom, kernal_rom, char_rom, cia1, cia2, vic, sid, dirty_tracker=None) -> None: + def __init__(self, ram, basic_rom, kernal_rom, char_rom, cia1, cia2, vic, sid, dirty_tracker=None) -> None: # Store reference to flat RAM array for direct access (avoids delegation loop) # This eliminates branching on every RAM access self._ram = ram.data # Direct reference to flat bytearray diff --git a/systems/c64/mixins/__init__.py b/systems/c64/mixins/__init__.py new file mode 100644 index 0000000..be84cef --- /dev/null +++ b/systems/c64/mixins/__init__.py @@ -0,0 +1,25 @@ +"""C64 mixin classes for modular functionality. + +These mixins split the C64 class into logical components for better +maintainability and smaller file sizes (important for MicroPython/Pico). +""" + +from .drive import C64DriveMixin +from .cartridge import C64CartridgeMixin +from .display import C64DisplayMixin +from .keyboard import C64KeyboardMixin +from .input_devices import C64InputDevicesMixin +from .debug import C64DebugMixin +from .program import C64ProgramMixin +from .runner import C64RunnerMixin + +__all__ = [ + "C64DriveMixin", + "C64CartridgeMixin", + "C64DisplayMixin", + "C64KeyboardMixin", + "C64InputDevicesMixin", + "C64DebugMixin", + "C64ProgramMixin", + "C64RunnerMixin", +] diff --git a/systems/c64/mixins/cartridge.py b/systems/c64/mixins/cartridge.py new file mode 100644 index 0000000..d9da574 --- /dev/null +++ b/systems/c64/mixins/cartridge.py @@ -0,0 +1,477 @@ +"""C64 Cartridge Mixin. + +Mixin for cartridge loading and management. +""" + +from mos6502.compat import logging, Path, List, Dict +from c64.cartridges import ( + Cartridge, + CartridgeTestResults, + StaticROMCartridge, + ErrorCartridge, + CARTRIDGE_TYPES, + create_cartridge, + create_error_cartridge_rom, + ROML_START, + ROML_END, + ROML_SIZE, + ROMH_START, + ROMH_END, + IO1_START, + IO1_END, + IO2_START, + IO2_END, +) + +log = logging.getLogger("c64") + + +class C64CartridgeMixin: + """Mixin for cartridge loading and management.""" + + def load_cartridge(self, path: Path, cart_type: str = "auto") -> None: + """Load a cartridge ROM file. + + Supports: + - Raw binary files (.bin, .rom): 8KB or 16KB + - CRT files (.crt): Standard C64 cartridge format with header + + Arguments: + path: Path to cartridge file + cart_type: "auto" (detect from file), "8k", or "16k" + + Raises: + FileNotFoundError: If cartridge file doesn't exist + ValueError: If cartridge format is invalid or unsupported + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"Cartridge file not found: {path}") + + data = path.read_bytes() + suffix = path.suffix.lower() + + # Check for CRT format (has "C64 CARTRIDGE" signature) + if suffix == ".crt" or data[:16] == b"C64 CARTRIDGE ": + self._load_crt_cartridge(data, path) + else: + # Raw binary format + self._load_raw_cartridge(data, path, cart_type) + + # Log cartridge status + if self.memory is not None and self.memory.cartridge is not None: + cart = self.memory.cartridge + log.info( + f"Cartridge loaded: {path.name} " + f"(type: {self.cartridge_type}, EXROM={0 if not cart.exrom else 1}, GAME={0 if not cart.game else 1})" + ) + + def _load_raw_cartridge(self, data: bytes, path: Path, cart_type: str) -> None: + """Load a raw binary cartridge file. + + Arguments: + data: Raw cartridge data + path: Path to cartridge file (for error messages) + cart_type: "auto", "8k", "16k", or "ultimax" + + Raises: + ValueError: If size doesn't match expected cartridge size + """ + size = len(data) + + # Determine cartridge type from size and content if auto + if cart_type == "auto": + if size == self.ROML_SIZE: + # 8KB file - check if it's a standard 8K cart or Ultimax + # Standard 8K carts have CBM80 signature at offset 4 ($8004) + # Ultimax carts have reset vector at end pointing to $E000-$FFFF + has_cbm80 = data[4:9] == b"CBM80" + + if has_cbm80: + cart_type = "8k" + log.debug(f"Auto-detected 8K cartridge (CBM80 signature found)") + else: + # Check reset vector at end of file (offsets $1FFC/$1FFD) + # For Ultimax, this should point to $E000-$FFFF range + reset_lo = data[0x1FFC] + reset_hi = data[0x1FFD] + reset_vector = reset_lo | (reset_hi << 8) + + if 0xE000 <= reset_vector <= 0xFFFF: + cart_type = "ultimax" + log.info( + f"Auto-detected Ultimax cartridge: reset vector ${reset_vector:04X} " + f"points to cartridge ROM space" + ) + else: + # Default to 8K if we can't determine + cart_type = "8k" + log.debug( + f"Assuming 8K cartridge (no CBM80, reset vector ${reset_vector:04X})" + ) + elif size == self.ROML_SIZE + self.ROMH_SIZE: + cart_type = "16k" + else: + raise ValueError( + f"Cannot auto-detect cartridge type for {path.name}: " + f"size {size} bytes (expected 8192 or 16384)" + ) + + # Validate size matches specified type and create Cartridge object + if cart_type == "8k": + if size != self.ROML_SIZE: + raise ValueError( + f"8K cartridge {path.name} has wrong size: " + f"{size} bytes (expected {self.ROML_SIZE})" + ) + cartridge = StaticROMCartridge( + roml_data=data, + romh_data=None, + name=path.stem, + ) + self.cartridge_type = "8k" + elif cart_type == "16k": + if size != self.ROML_SIZE + self.ROMH_SIZE: + raise ValueError( + f"16K cartridge {path.name} has wrong size: " + f"{size} bytes (expected {self.ROML_SIZE + self.ROMH_SIZE})" + ) + cartridge = StaticROMCartridge( + roml_data=data[:self.ROML_SIZE], + romh_data=data[self.ROML_SIZE:], + name=path.stem, + ) + self.cartridge_type = "16k" + elif cart_type == "ultimax": + if size != self.ROMH_SIZE: + raise ValueError( + f"Ultimax cartridge {path.name} has wrong size: " + f"{size} bytes (expected {self.ROMH_SIZE})" + ) + cartridge = StaticROMCartridge( + roml_data=None, + romh_data=None, + ultimax_romh_data=data, + name=path.stem, + ) + self.cartridge_type = "ultimax" + else: + raise ValueError(f"Unknown cartridge type: {cart_type}") + + # Attach cartridge to memory handler + if self.memory is not None: + self.memory.cartridge = cartridge + + log.info(f"Loaded raw {cart_type.upper()} cartridge: {path.name} ({size} bytes)") + + def _create_error_cartridge(self, error_lines: List[str]) -> bytes: + """Create an 8KB cartridge ROM that displays an error message. + + This is used when an unsupported cartridge type is loaded, to give + the user a friendly on-screen message instead of crashing. + + Arguments: + error_lines: List of text lines to display (max ~38 chars each) + + Returns: + 8KB cartridge ROM data + """ + # Use the shared function from cartridges module (single source of truth) + return create_error_cartridge_rom(error_lines, border_color=0x02) + + def _load_error_cartridge_with_results(self, results: CartridgeTestResults) -> None: + """Generate and load an error cartridge displaying test results. + + Arguments: + results: CartridgeTestResults with current pass/fail state + """ + error_lines = results.to_display_lines() + error_roml_data = self._create_error_cartridge(error_lines) + + # Create ErrorCartridge object + cartridge = ErrorCartridge( + roml_data=error_roml_data, + original_type=results.hardware_type, + original_name=results.cart_name, + ) + self.cartridge_type = "error" + + # Attach cartridge to memory handler + if self.memory is not None: + self.memory.cartridge = cartridge + + log.info(f"Loaded error cartridge with test results for type {results.hardware_type}") + + def _load_crt_cartridge(self, data: bytes, path: Path) -> None: + """Load a CRT format cartridge file. + + CRT format: + - 64-byte header with signature, hardware type, EXROM/GAME lines + - CHIP packets containing ROM data with load addresses + + Arguments: + data: CRT file data + path: Path to cartridge file (for error messages) + + Raises: + ValueError: If CRT format is invalid or unsupported + """ + # Create test results - starts with all FAILs + results = CartridgeTestResults() + + # Validate CRT header size + if len(data) < 64: + # Generate error cart with current results (all FAIL) + self._load_error_cartridge_with_results(results) + return + + results.header_size_valid = True + + # Check signature + signature = data[:16] + if signature != b"C64 CARTRIDGE ": + # Generate error cart with current results + self._load_error_cartridge_with_results(results) + return + + results.signature_valid = True + + # Parse header (big-endian values) + header_length = int.from_bytes(data[0x10:0x14], "big") + version_hi = data[0x14] + version_lo = data[0x15] + hardware_type = int.from_bytes(data[0x16:0x18], "big") + exrom_line = data[0x18] + game_line = data[0x19] + cart_name = data[0x20:0x40].rstrip(b"\x00").decode("latin-1", errors="replace") + + # Update results with parsed values + results.version_valid = True # We parsed it successfully + results.hardware_type = hardware_type + results.hardware_name = self.CRT_HARDWARE_TYPES.get(hardware_type, f"Unknown type {hardware_type}") + results.exrom_line = exrom_line + results.game_line = game_line + results.cart_name = cart_name + + log.info( + f"CRT header: name='{cart_name}', version={version_hi}.{version_lo}, " + f"hardware_type={hardware_type} ({results.hardware_name}), EXROM={exrom_line}, GAME={game_line}" + ) + + # Check if hardware type is supported + mapper_supported = hardware_type in CARTRIDGE_TYPES + if mapper_supported: + results.mapper_supported = True + else: + log.warning( + f"Unsupported cartridge type {hardware_type} ({results.hardware_name}). " + f"Will parse CHIP packets for diagnostics." + ) + + # Parse CHIP packets (even for unsupported types, for diagnostics) + offset = header_length + + # For Type 0 (standard cartridge) + roml_data = None + romh_data = None + ultimax_romh_data = None + + # For banked cartridges (Type 1+) + banks: Dict[int, bytes] = {} # bank_number -> rom_data + + # Ultimax mode detection from CRT header + # EXROM=1, GAME=0 indicates Ultimax mode + is_ultimax = (exrom_line == 1 and game_line == 0) + + while offset < len(data): + if offset + 16 > len(data): + break # Not enough data for another CHIP header + + chip_sig = data[offset:offset + 4] + if chip_sig != b"CHIP": + # Invalid CHIP signature - generate error cart with results so far + self._load_error_cartridge_with_results(results) + return + + packet_length = int.from_bytes(data[offset + 4:offset + 8], "big") + chip_type = int.from_bytes(data[offset + 8:offset + 10], "big") + bank_number = int.from_bytes(data[offset + 10:offset + 12], "big") + load_address = int.from_bytes(data[offset + 12:offset + 14], "big") + rom_size = int.from_bytes(data[offset + 14:offset + 16], "big") + + log.debug( + f"CHIP packet: type={chip_type}, bank={bank_number}, " + f"load=${load_address:04X}, size={rom_size}" + ) + + # Track that we found a CHIP packet + results.chip_count += 1 + results.chip_packets_found = True + + # Only handle ROM chips (type 0) for now + if chip_type != 0: + log.warning(f"Skipping non-ROM CHIP type {chip_type}") + offset += packet_length + continue + + # Extract ROM data + rom_data = data[offset + 16:offset + 16 + rom_size] + + if hardware_type == 0: + # Type 0: Standard cartridge - single bank only + if bank_number != 0: + log.warning(f"Skipping bank {bank_number} for Type 0 cartridge") + offset += packet_length + continue + + if load_address == self.ROML_START: + # Check if this is a 16KB ROM that needs to be split + if rom_size > self.ROML_SIZE: + # Split 16KB ROM: first 8KB to ROML, second 8KB to ROMH + roml_data = rom_data[:self.ROML_SIZE] + romh_data = rom_data[self.ROML_SIZE:] + results.roml_valid = True + results.romh_valid = True + log.info(f"Loaded ROML: ${load_address:04X}-${load_address + self.ROML_SIZE - 1:04X} ({self.ROML_SIZE} bytes)") + log.info(f"Loaded ROMH: ${self.ROMH_START:04X}-${self.ROMH_START + len(romh_data) - 1:04X} ({len(romh_data)} bytes)") + else: + roml_data = rom_data + results.roml_valid = True + log.info(f"Loaded ROML: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + elif load_address == self.ROMH_START: + romh_data = rom_data + results.romh_valid = True + log.info(f"Loaded ROMH: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + elif load_address == KERNAL_ROM_START: + # Ultimax mode: ROM at $E000-$FFFF replaces KERNAL + ultimax_romh_data = rom_data + results.ultimax_romh_valid = True + log.info(f"Loaded Ultimax ROMH: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + else: + log.warning(f"Unknown CHIP load address: ${load_address:04X}") + elif hardware_type == 3: + # Type 3: Final Cartridge III - 4 x 16KB banks + # Each bank is a single 16KB CHIP packet at $8000 + if load_address == self.ROML_START: + banks[bank_number] = rom_data + results.roml_valid = True + results.bank_switching_valid = len(banks) > 1 + log.info(f"Loaded FC3 bank {bank_number}: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + else: + log.warning(f"Unknown CHIP load address for Type 3: ${load_address:04X}") + elif hardware_type == 4 or hardware_type == 13: + # Type 4: Simons' BASIC - 16KB cartridge with ROML + ROMH + # Type 13: Final Cartridge I - 16KB cartridge with ROML + ROMH + # Two CHIP packets: one for ROML at $8000, one for ROMH at $A000 + if load_address == self.ROML_START: + roml_data = rom_data + results.roml_valid = True + log.info(f"Loaded ROML: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + elif load_address == self.ROMH_START: + romh_data = rom_data + results.romh_valid = True + log.info(f"Loaded ROMH: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + else: + log.warning(f"Unknown CHIP load address for Type {hardware_type}: ${load_address:04X}") + else: + # Banked cartridges (Type 1, 5+): Collect all banks + # Each bank is typically 8KB at $8000 + if load_address == self.ROML_START: + banks[bank_number] = rom_data + results.roml_valid = True # At least one bank loaded to ROML region + results.bank_switching_valid = len(banks) > 1 # Multiple banks = bank switching works + log.info(f"Loaded bank {bank_number}: ${load_address:04X}-${load_address + rom_size - 1:04X} ({rom_size} bytes)") + else: + log.warning(f"Unexpected load address ${load_address:04X} for bank {bank_number}") + + offset += packet_length + + # If mapper not supported, generate error cart with diagnostics + if not mapper_supported: + self._load_error_cartridge_with_results(results) + return + + # Create appropriate cartridge object based on hardware type + if hardware_type == 0: + # Validate we got valid ROM data for Type 0 + if ultimax_romh_data is None and roml_data is None: + # No usable ROM data - generate error cart + self._load_error_cartridge_with_results(results) + return + + cartridge = create_cartridge( + hardware_type=0, + roml_data=roml_data, + romh_data=romh_data, + ultimax_romh_data=ultimax_romh_data, + name=cart_name, + ) + + # Determine cartridge type from what we loaded + if ultimax_romh_data is not None: + self.cartridge_type = "ultimax" + elif romh_data is not None: + self.cartridge_type = "16k" + else: + self.cartridge_type = "8k" + elif hardware_type == 4: + # Type 4: Simons' BASIC - needs both ROML and ROMH + if roml_data is None or romh_data is None: + # Missing ROM data - generate error cart + self._load_error_cartridge_with_results(results) + return + + cartridge = create_cartridge( + hardware_type=4, + roml_data=roml_data, + romh_data=romh_data, + name=cart_name, + ) + self.cartridge_type = "simons_basic" + elif hardware_type == 13: + # Type 13: Final Cartridge I - needs both ROML and ROMH + if roml_data is None: + # Missing ROM data - generate error cart + self._load_error_cartridge_with_results(results) + return + + cartridge = create_cartridge( + hardware_type=13, + roml_data=roml_data, + romh_data=romh_data, + name=cart_name, + ) + self.cartridge_type = "final_cartridge_i" + else: + # Banked cartridges - convert bank dict to sorted list + if not banks: + # No bank data - generate error cart + self._load_error_cartridge_with_results(results) + return + + # Create sorted list of banks (fill missing banks with empty data) + max_bank = max(banks.keys()) + bank_list = [] + for i in range(max_bank + 1): + if i in banks: + bank_list.append(banks[i]) + else: + # Fill missing banks with empty 8KB + log.warning(f"Bank {i} missing, filling with empty data") + bank_list.append(bytes(ROML_SIZE)) + + cartridge = create_cartridge( + hardware_type=hardware_type, + banks=bank_list, + name=cart_name, + ) + self.cartridge_type = results.hardware_name.lower().replace(" ", "_") + + # Mark as fully loaded + results.fully_loaded = True + + # Attach cartridge to memory handler + if self.memory is not None: + self.memory.cartridge = cartridge + + log.info(f"Loaded CRT cartridge: '{cart_name}' (type {hardware_type}: {results.hardware_name})") diff --git a/systems/c64/mixins/debug.py b/systems/c64/mixins/debug.py new file mode 100644 index 0000000..887d4fd --- /dev/null +++ b/systems/c64/mixins/debug.py @@ -0,0 +1,627 @@ +"""C64 Debug Mixin. + +Mixin for debugging and diagnostics. +""" + +from mos6502.compat import logging, Optional, List +from c64.memory import ( + BASIC_ROM_START, + BASIC_ROM_END, + KERNAL_ROM_START, + KERNAL_ROM_END, + CHAR_ROM_START, + CHAR_ROM_END, +) + +log = logging.getLogger("c64") + + +class C64DebugMixin: + """Mixin for debugging and diagnostics.""" + + def get_pc_region(self) -> str: + """Determine which memory region PC is currently in. + + Takes memory banking into account (via $0001 port). + + Returns: + String describing the region: "RAM", "BASIC", "KERNAL", "I/O", "CHAR", or "???" + """ + pc = self.cpu.PC + port = self.memory.port + + if pc < BASIC_ROM_START: + return "RAM" + elif BASIC_ROM_START <= pc <= BASIC_ROM_END: + # BASIC ROM only visible if bit 0 of port is set + return "BASIC" if (port & 0x01) else "RAM" + elif CHAR_ROM_START <= pc <= CHAR_ROM_END: + # I/O vs CHAR vs RAM depends on bits in port + if port & 0x04: + return "I/O" + elif (port & 0x03) == 0x01: + return "CHAR" + else: + return "RAM" + elif KERNAL_ROM_START <= pc <= KERNAL_ROM_END: + # KERNAL ROM only visible if bit 1 of port is set + return "KERNAL" if (port & 0x02) else "RAM" + else: + return "???" + + def _get_instruction_color(self, opcode: int) -> str: + """Get ANSI color code for instruction based on type. + + Args: + opcode: The instruction opcode byte + + Returns: + ANSI escape sequence for the instruction color: + - Light blue (96): Common instructions identical across variants + - Yellow (93): Instructions that differ between variants (ADC, SBC, JMP, BRK) + - Red (91): Illegal/undocumented instructions + """ + from mos6502 import instructions + + # ANSI color codes + LIGHT_BLUE = "\033[96m" # Light cyan/blue + YELLOW = "\033[93m" # Yellow + RED = "\033[91m" # Red + RESET = "\033[0m" + + if opcode not in instructions.OPCODE_LOOKUP: + return RED # Unknown opcode + + opcode_obj = instructions.OPCODE_LOOKUP[opcode] + package = opcode_obj.package + + # Check if it's an illegal instruction + if ".illegal." in package: + return RED + + # Check if it's a variant-specific instruction + # These have different implementations for 6502 vs 65C02 + variant_instructions = { + "_adc", # Decimal mode differs + "_sbc", # Decimal mode differs + "_jmp", # Indirect JMP page boundary bug + "_brk", # Flag handling differs + } + for variant_inst in variant_instructions: + if variant_inst in package: + return YELLOW + + return LIGHT_BLUE + + def _format_cpu_status(self, prefix: str = "C64") -> str: + """Format C64 CPU status line. + + Args: + prefix: Label prefix for the status line + + Returns: + Formatted status string with colorized instruction display: + - Light blue: Common instructions identical across variants + - Yellow: Instructions that differ between variants (ADC, SBC, JMP, BRK) + - Red: Illegal/undocumented instructions + """ + from mos6502.flags import format_flags + RESET = "\033[0m" + + flags = format_flags(self.cpu._flags.value) + region = self.get_pc_region() + pc = self.cpu.PC + + # Get opcode for colorization + try: + opcode = self.cpu.ram[pc] + color = self._get_instruction_color(opcode) + except (KeyError, IndexError): + color = "\033[91m" # Red for unknown + opcode = None + + # Disassemble current instruction + try: + inst_str = self.disassemble_instruction(pc) + inst_display = inst_str.strip() + except (KeyError, ValueError, IndexError): + try: + b0 = self.cpu.ram[pc] + b1 = self.cpu.ram[pc + 1] + b2 = self.cpu.ram[pc + 2] + inst_display = f"{b0:02X} {b1:02X} {b2:02X} ???" + except IndexError: + inst_display = "???" + + # Apply color to instruction display + colored_inst = f"{color}{inst_display:20s}{RESET}" + + return (f"{prefix}: Cycles: {self.cpu.cycles_executed:,} | " + f"PC=${pc:04X}[{region}] {colored_inst} | " + f"A=${self.cpu.A:02X} X=${self.cpu.X:02X} " + f"Y=${self.cpu.Y:02X} S=${self.cpu.S & 0xFF:02X} P={flags}") + + def _format_drive_status(self) -> str: + """Format 1541 drive CPU status line. + + Returns: + Formatted status string with colorized instruction display: + - Light blue: Common instructions identical across variants + - Yellow: Instructions that differ between variants (ADC, SBC, JMP, BRK) + - Red: Illegal/undocumented instructions + Empty string if no drive attached. + """ + if not self.drive8 or not self.drive8.cpu: + return "" + + from mos6502.flags import format_flags + from mos6502 import instructions + RESET = "\033[0m" + + drive_cpu = self.drive8.cpu + flags = format_flags(drive_cpu._flags.value) + region = self.get_drive_pc_region() + pc = drive_cpu.PC + + # Disassemble current instruction from drive memory + try: + b0 = self.drive8.memory.read(pc) + b1 = self.drive8.memory.read(pc + 1) + b2 = self.drive8.memory.read(pc + 2) + + # Get color for this opcode + color = self._get_instruction_color(b0) + + # Use the instruction set to get mnemonic and operand size + if b0 in instructions.InstructionSet.map: + inst_info = instructions.InstructionSet.map[b0] + try: + num_bytes = int(inst_info.get("bytes", 1)) + except (ValueError, TypeError): + num_bytes = 1 + assembler = inst_info.get("assembler", "???") + mnemonic = assembler.split()[0] if assembler != "???" else "???" + mode = inst_info.get("addressing", "") + elif b0 in instructions.OPCODE_LOOKUP: + opcode_obj = instructions.OPCODE_LOOKUP[b0] + func_name = opcode_obj.function + mnemonic = func_name.split("_")[0].upper() + if "implied" in func_name or "accumulator" in func_name: + num_bytes = 1 + elif "relative" in func_name or "immediate" in func_name or "zeropage" in func_name: + num_bytes = 2 + else: + num_bytes = 3 + mode = "" + else: + num_bytes = 1 + mnemonic = "???" + mode = "" + + # Build hex dump + hex_bytes = [f"{self.drive8.memory.read(pc + i):02X}" + for i in range(min(num_bytes, 3))] + hex_str = " ".join(hex_bytes).ljust(8) + + # Build operand display + if num_bytes == 1: + operand_str = "" + elif num_bytes == 2: + operand_str = f" ${b1:02X}" + else: + operand = (b2 << 8) | b1 + operand_str = f" ${operand:04X}" + + if mode: + inst_display = f"{hex_str} {mnemonic}{operand_str} ; {mode}" + else: + inst_display = f"{hex_str} {mnemonic}{operand_str}" + + except Exception: + inst_display = "???" + color = "\033[91m" # Red for errors + + # Apply color to instruction display + colored_inst = f"{color}{inst_display:20s}{RESET}" + + return (f"1541: Cycles: {drive_cpu.cycles_executed:,} | " + f"PC=${pc:04X}[{region}] {colored_inst} | " + f"A=${drive_cpu.A:02X} X=${drive_cpu.X:02X} " + f"Y=${drive_cpu.Y:02X} S=${drive_cpu.S & 0xFF:02X} P={flags}") + + @property + + def dump_memory(self, start: int, end: int, bytes_per_line: int = 16) -> None: + """Dump memory contents for debugging. + + Arguments: + start: Starting address + end: Ending address + bytes_per_line: Number of bytes to display per line + """ + print(f"\nMemory dump ${start:04X}-${end:04X}:") + print(" ", end="") + for i in range(bytes_per_line): + print(f" {i:02X}", end="") + print() + + for addr in range(start, end + 1, bytes_per_line): + print(f"{addr:04X}: ", end="") + for offset in range(bytes_per_line): + if addr + offset <= end: + byte_val = self.cpu.ram[addr + offset] + print(f" {byte_val:02X}", end="") + else: + print(" ", end="") + print() + + def dump_crash_report(self, exception: Exception = None) -> None: + """Dump comprehensive crash report for illegal instruction or other CPU errors. + + Arguments: + exception: The exception that triggered the crash (optional) + """ + print("\n" + "=" * 70) + print("CRASH REPORT - Illegal Instruction") + print("=" * 70) + + # Show exception info + if exception: + print(f"\nException: {type(exception).__name__}: {exception}") + + # CPU state + pc = int(self.cpu.PC) + opcode = self.cpu.ram[pc] + print(f"\nCPU State at crash:") + print(f" PC: ${pc:04X} (opcode: ${opcode:02X})") + print(f" A: ${self.cpu.A:02X}") + print(f" X: ${self.cpu.X:02X}") + print(f" Y: ${self.cpu.Y:02X}") + print(f" S: ${self.cpu.S & 0xFF:02X} (stack pointer)") + print(f" P: ${self.cpu._flags.value:02X} (N={int(self.cpu.N)} V={int(self.cpu.V)} B={int(self.cpu.B)} D={int(self.cpu.D)} I={int(self.cpu.I)} Z={int(self.cpu.Z)} C={int(self.cpu.C)})") + print(f" Cycles: {self.cpu.cycles_executed:,}") + + # Memory region + region = "RAM" + if 0xA000 <= pc <= 0xBFFF and (self.memory.read(1) & 0x03): + region = "BASIC ROM" + elif 0xD000 <= pc <= 0xDFFF: + region = "I/O" if (self.memory.read(1) & 0x04) else "CHAR ROM" + elif 0xE000 <= pc <= 0xFFFF and (self.memory.read(1) & 0x02): + region = "KERNAL ROM" + print(f" Region: {region}") + + # Stack contents (show 16 bytes from current SP) + sp = self.cpu.S & 0xFF + print(f"\nStack (${sp:02X} -> $FF):") + stack_addr = 0x0100 + sp + print(" ", end="") + for i in range(16): + print(f" {i:02X}", end="") + print() + for row_start in range(stack_addr, 0x0200, 16): + if row_start >= 0x0200: + break + print(f" {row_start:04X}:", end="") + for offset in range(16): + addr = row_start + offset + if addr < 0x0200: + print(f" {self.cpu.ram[addr]:02X}", end="") + else: + print(" ", end="") + print() + + # Disassembly around crash + print(f"\nDisassembly around PC ${pc:04X}:") + try: + start_addr = max(0, pc - 16) + self.show_disassembly(start_addr, 20) + except Exception as e: + print(f" Could not disassemble: {e}") + + # Memory around PC + print(f"\nMemory around PC ${pc:04X}:") + mem_start = max(0, pc - 32) + mem_end = min(0xFFFF, pc + 32) + self.dump_memory(mem_start, mem_end) + + # Zero page (important for 6502) + print("\nZero page ($00-$FF):") + self.dump_memory(0x00, 0xFF) + + # Key C64 memory locations + print("\nKey C64 Memory Locations:") + print(f" $00 (DDR): ${self.cpu.ram[0x00]:02X}") + print(f" $01 (Bank): ${self.cpu.ram[0x01]:02X}") + print(f" $90 (KERNAL ST): ${self.cpu.ram[0x90]:02X}") + print(f" $9D (Direct): ${self.cpu.ram[0x9D]:02X}") + print(f" $2B-$2C (TXTTAB):${self.cpu.ram[0x2B]:02X}{self.cpu.ram[0x2C]:02X}") + print(f" $2D-$2E (VARTAB):${self.cpu.ram[0x2D]:02X}{self.cpu.ram[0x2E]:02X}") + + # Vectors + print("\nInterrupt Vectors:") + nmi = self.cpu.ram[0xFFFA] | (self.cpu.ram[0xFFFB] << 8) + reset = self.cpu.ram[0xFFFC] | (self.cpu.ram[0xFFFD] << 8) + irq = self.cpu.ram[0xFFFE] | (self.cpu.ram[0xFFFF] << 8) + print(f" NMI: ${nmi:04X}") + print(f" RESET: ${reset:04X}") + print(f" IRQ: ${irq:04X}") + + print("\n" + "=" * 70) + print("END CRASH REPORT") + print("=" * 70 + "\n") + + def get_speed_stats(self) -> Optional[dict]: + """Calculate CPU execution speed statistics. + + Returns: + Dictionary with speed stats, or None if timing data not available: + - elapsed_seconds: Wall-clock time elapsed + - cycles_executed: Total CPU cycles executed + - cycles_per_second: Actual execution rate (lifetime average) + - rolling_cycles_per_second: Rolling average over last 10 seconds (if available) + - real_cpu_freq: Real C64 CPU frequency for this chip + - speedup: Ratio of actual speed to real hardware speed (lifetime) + - rolling_speedup: Ratio based on rolling average (if available) + - chip_name: VIC-II chip name (6569, 6567R8, etc.) + """ + if self._execution_start_time is None: + return None + + import time + end_time = self._execution_end_time or time.perf_counter() + elapsed = end_time - self._execution_start_time + + if elapsed <= 0: + return None + + cycles_executed = self.cpu.cycles_executed + cycles_per_second = cycles_executed / elapsed + real_cpu_freq = self.video_timing.cpu_freq + speedup = cycles_per_second / real_cpu_freq + + result = { + "elapsed_seconds": elapsed, + "cycles_executed": cycles_executed, + "cycles_per_second": cycles_per_second, + "real_cpu_freq": real_cpu_freq, + "speedup": speedup, + "chip_name": self.video_timing.chip_name, + } + + # Add rolling average if we have samples + if self._speed_samples: + rolling_cps = sum(self._speed_samples) / len(self._speed_samples) + result["rolling_cycles_per_second"] = rolling_cps + result["rolling_speedup"] = rolling_cps / real_cpu_freq + + return result + + def dump_registers(self) -> None: + """Dump CPU register state.""" + print(f"\nCPU Registers:") + print(f" PC: ${self.cpu.PC:04X}") + print(f" A: ${self.cpu.A:02X} ({self.cpu.A})") + print(f" X: ${self.cpu.X:02X} ({self.cpu.X})") + print(f" Y: ${self.cpu.Y:02X} ({self.cpu.Y})") + print(f" S: ${self.cpu.S:04X}") + print(f" Flags: C={self.cpu.C} Z={self.cpu.Z} I={self.cpu.I} " + f"D={self.cpu.D} B={self.cpu.B} V={self.cpu.V} N={self.cpu.N}") + print(f" Cycles executed: {self.cpu.cycles_executed}") + + # Show speed stats if available + stats = self.get_speed_stats() + if stats: + chip = stats['chip_name'] + region = "PAL" if chip == "6569" else "NTSC" + actual_mhz = stats['cycles_per_second'] / 1e6 + print(f" Speed: {stats['cycles_per_second']:,.0f} ({actual_mhz:.3f}MHz) cycles/sec " + f"({stats['speedup']:.1%} of {region} ({chip}) C64 @ " + f"{stats['real_cpu_freq']/1e6:.3f}MHz)") + + def disassemble_at(self, address: int, num_instructions: int = 10) -> List[str]: + """Disassemble instructions starting at address. + + Arguments: + address: Starting address + num_instructions: Number of instructions to disassemble + + Returns: + List of disassembly strings + """ + from mos6502 import instructions + + lines = [] + current_addr = address + + for _ in range(num_instructions): + if current_addr > 0xFFFF: + break + + opcode = self.cpu.ram[current_addr] + + # First try InstructionSet.map (has full metadata) + if opcode in instructions.InstructionSet.map: + inst_info = instructions.InstructionSet.map[opcode] + # Convert bytes/cycles to int (they might be strings in the map) + try: + num_bytes = int(inst_info.get("bytes", 1)) + except (ValueError, TypeError): + num_bytes = 1 + + # Extract mnemonic from assembler string (e.g., "LDX #{oper}" -> "LDX") + assembler = inst_info.get("assembler", "???") + mnemonic = assembler.split()[0] if assembler != "???" else "???" + mode = inst_info.get("addressing", "") + + # If not in map, try OPCODE_LOOKUP (just has opcode objects) + elif opcode in instructions.OPCODE_LOOKUP: + opcode_obj = instructions.OPCODE_LOOKUP[opcode] + # Extract mnemonic from function name (e.g., "sei_implied_0x78" -> "SEI") + func_name = opcode_obj.function + mnemonic = func_name.split("_")[0].upper() + + # Guess number of bytes from function name + if "implied" in func_name or "accumulator" in func_name: + num_bytes = 1 + elif "relative" in func_name or "immediate" in func_name or "zeropage" in func_name: + num_bytes = 2 + else: + num_bytes = 3 + + mode = "implied" + + else: + # Unknown/illegal opcode + hex_str = f"{opcode:02X}" + line = f"${current_addr:04X}: {hex_str} ??? ; ILLEGAL/UNKNOWN ${opcode:02X}" + lines.append(line) + current_addr += 1 + continue + + # Build hex dump + hex_bytes = [f"{self.cpu.ram[current_addr + i]:02X}" + for i in range(min(num_bytes, 3))] + hex_str = " ".join(hex_bytes).ljust(8) + + # Build operand display + if num_bytes == 1: + operand_str = "" + elif num_bytes == 2: + operand = self.cpu.ram[current_addr + 1] + operand_str = f" ${operand:02X}" + elif num_bytes == 3: + lo = self.cpu.ram[current_addr + 1] + hi = self.cpu.ram[current_addr + 2] + operand = (hi << 8) | lo + operand_str = f" ${operand:04X}" + else: + operand_str = "" + + # Mark illegal opcodes + if mnemonic == "???": + line = f"${current_addr:04X}: {hex_str} {mnemonic} ; ILLEGAL ${opcode:02X}" + else: + line = f"${current_addr:04X}: {hex_str} {mnemonic}{operand_str} ; {mode}" + lines.append(line) + current_addr += num_bytes + + return lines + + def show_disassembly(self, address: int, num_instructions: int = 10) -> None: + """Display disassembly at address.""" + print(f"\nDisassembly at ${address:04X}:") + print("-" * 60) + for line in self.disassemble_at(address, num_instructions): + print(line) + + def petscii_to_ascii(self, petscii_code: int) -> str: + """Convert PETSCII code to displayable ASCII character. + + Arguments: + petscii_code: PETSCII character code (0-255) + + Returns: + ASCII character or representation + """ + # Basic PETSCII to ASCII conversion (simplified) + # Uppercase letters (PETSCII 65-90 = ASCII 65-90) + if 65 <= petscii_code <= 90: + return chr(petscii_code) + # Lowercase letters (PETSCII 97-122 = ASCII 97-122) + if 97 <= petscii_code <= 122: + return chr(petscii_code) + # Digits (PETSCII 48-57 = ASCII 48-57) + if 48 <= petscii_code <= 57: + return chr(petscii_code) + # Space + if petscii_code == 32: + return " " + # Common punctuation + punctuation = { + 33: "!", 34: '"', 35: "#", 36: "$", 37: "%", 38: "&", 39: "'", + 40: "(", 41: ")", 42: "*", 43: "+", 44: ",", 45: "-", 46: ".", 47: "/", + 58: ":", 59: ";", 60: "<", 61: "=", 62: ">", 63: "?", 64: "@", + 91: "[", 93: "]", 95: "_" + } + if petscii_code in punctuation: + return punctuation[petscii_code] + # Screen codes 1-26 map to letters A-Z (reverse video in C64) + if 1 <= petscii_code <= 26: + return chr(64 + petscii_code) # Convert to uppercase letter + # Default: show as '.' for unprintable + return "." + + def disassemble_instruction(self, address: int) -> str: + """Disassemble a single instruction at the given address. + + Arguments: + address: Address of instruction to disassemble + + Returns: + Formatted disassembly string for the instruction + + """ + from mos6502 import instructions + + opcode = self.cpu.ram[address] + + # First try InstructionSet.map (has full metadata) + if opcode in instructions.InstructionSet.map: + inst_info = instructions.InstructionSet.map[opcode] + # Convert bytes/cycles to int (they might be strings in the map) + try: + num_bytes = int(inst_info.get("bytes", 1)) + except (ValueError, TypeError): + num_bytes = 1 + + # Extract mnemonic from assembler string (e.g., "LDX #{oper}" -> "LDX") + assembler = inst_info.get("assembler", "???") + mnemonic = assembler.split()[0] if assembler != "???" else "???" + mode = inst_info.get("addressing", "") + + # If not in map, try OPCODE_LOOKUP (just has opcode objects) + elif opcode in instructions.OPCODE_LOOKUP: + opcode_obj = instructions.OPCODE_LOOKUP[opcode] + # Extract mnemonic from function name (e.g., "sei_implied_0x78" -> "SEI") + func_name = opcode_obj.function + mnemonic = func_name.split("_")[0].upper() + + # Guess number of bytes from function name + if "implied" in func_name or "accumulator" in func_name: + num_bytes = 1 + elif "relative" in func_name or "immediate" in func_name or "zeropage" in func_name: + num_bytes = 2 + else: + num_bytes = 3 + + mode = "implied" + + else: + # Unknown/illegal opcode - return formatted string + return f"{opcode:02X} ??? ; ILLEGAL ${opcode:02X}" + + # Build hex dump + hex_bytes = [f"{self.cpu.ram[address + i]:02X}" + for i in range(min(num_bytes, 3))] + hex_str = " ".join(hex_bytes).ljust(8) + + # Build operand display + if num_bytes == 1: + operand_str = "" + elif num_bytes == 2: + operand = self.cpu.ram[address + 1] + operand_str = f" ${operand:02X}" + elif num_bytes == 3: + lo = self.cpu.ram[address + 1] + hi = self.cpu.ram[address + 2] + operand = (hi << 8) | lo + operand_str = f" ${operand:04X}" + else: + operand_str = "" + + # Return formatted string without the address prefix + if mnemonic == "???": + return f"{hex_str} {mnemonic} ; ILLEGAL ${opcode:02X}" + else: + return f"{hex_str} {mnemonic}{operand_str} ; {mode}" diff --git a/systems/c64/mixins/display.py b/systems/c64/mixins/display.py new file mode 100644 index 0000000..6facb82 --- /dev/null +++ b/systems/c64/mixins/display.py @@ -0,0 +1,888 @@ +"""C64 Display Mixin. + +Mixin for display and rendering (pygame and terminal). +""" + +from mos6502.compat import logging +from c64.vic import ( + COLORS, + c64_to_ansi_fg, + c64_to_ansi_bg, + ANSI_RESET, +) + +log = logging.getLogger("c64") + + +class C64DisplayMixin: + """Mixin for display and rendering (pygame and terminal).""" + + def init_pygame_display(self) -> bool: + """Initialize pygame display. + + Returns: + True if pygame was successfully initialized, False otherwise + """ + try: + import pygame + self.pygame_available = True + except ImportError: + log.error("Pygame not installed. Install with: pip install pygame-ce") + return False + + try: + pygame.init() + + # Get display dimensions from VIC (hardware characteristics) + total_width = self.vic.total_width + total_height = self.vic.total_height + + # Create window with scaled dimensions + # Disable pygame vsync - we do our own frame timing via VIC + # This prevents pygame.display.flip() from blocking + width = total_width * self.scale + height = total_height * self.scale + try: + # pygame 2.0+ supports vsync parameter + self.pygame_screen = pygame.display.set_mode((width, height), vsync=0) + except TypeError: + # Older pygame doesn't support vsync parameter + self.pygame_screen = pygame.display.set_mode((width, height)) + pygame.display.set_caption(f"C64 Emulator - {self.get_video_standard()} ({self.video_chip})") + + # Enable key repeat (delay=300ms before repeat, interval=30ms between repeats) + # This matches typical terminal key repeat behavior + pygame.key.set_repeat(300, 30) + + # Initialize clipboard support for Ctrl+V paste + try: + pygame.scrap.init() + except Exception as e: + log.warning(f"Clipboard support unavailable: {e}") + + # Create the rendering surface (384x270 with border) + self.pygame_surface = pygame.Surface((total_width, total_height)) + + log.info(f"Pygame display initialized: {width}x{height} (scale={self.scale})") + return True + + except Exception as e: + log.error(f"Failed to initialize pygame: {e}") + self.pygame_available = False + return False + + def set_speed_sample_count(self, count: int) -> None: + """Set the number of samples for rolling average speed calculation. + + Args: + count: Number of samples to keep (e.g., 10 = 10 second rolling window). + Set to 0 to disable rolling average. + """ + from collections import deque + self._speed_sample_count = count + # Use positional args for MicroPython compatibility (no kwargs on deque) + if count > 0: + self._speed_samples = deque((), count) + else: + self._speed_samples = deque((), 1) # Keep at least 1 for the API + self._speed_samples.clear() + + def _record_speed_sample(self) -> bool: + """Record a speed sample for rolling average (rate-limited to once per second). + + Returns: + True if a sample was recorded, False if rate-limited. + """ + import time + now = time.time() + + # Only record once per second + if self._last_sample_time > 0 and now - self._last_sample_time < 1.0: + return False + + # Calculate cycles since last sample + current_cycles = self.cpu.cycles_executed + if self._last_sample_time > 0: + delta_time = now - self._last_sample_time + delta_cycles = current_cycles - self._last_sample_cycles + if delta_time > 0: + sample_cps = delta_cycles / delta_time + self._speed_samples.append(sample_cps) + + self._last_sample_time = now + self._last_sample_cycles = current_cycles + return True + + def _update_pygame_title(self, pygame) -> None: + """Update pygame window title with speed stats (rate-limited to once per second).""" + if not self._record_speed_sample(): + return + + # Use rolling average if we have samples + if self._speed_samples: + cycles_per_second = sum(self._speed_samples) / len(self._speed_samples) + real_cpu_freq = self.video_timing.cpu_freq + speedup = cycles_per_second / real_cpu_freq + chip = self.video_timing.chip_name + region = "PAL" if chip == "6569" else "NTSC" + actual_mhz = cycles_per_second / 1e6 + real_mhz = real_cpu_freq / 1e6 + title = (f"C64 Emulator - {region} ({chip}) - " + f"{actual_mhz:.3f}MHz ({speedup:.1%} of {real_mhz:.3f}MHz)") + pygame.display.set_caption(title) + + def show_screen(self) -> None: + """Display the C64 screen (40x25 characters from screen RAM at $0400).""" + screen_start = 0x0400 + screen_end = 0x07E7 + cols = 40 + rows = 25 + + print("\n" + "=" * 42) + print(" C64 SCREEN") + print("=" * 42) + + for row in range(rows): + line = "" + for col in range(cols): + addr = screen_start + (row * cols) + col + if addr <= screen_end: + petscii = int(self.cpu.ram[addr]) + line += self.petscii_to_ascii(petscii) + else: + line += " " + # Only print non-empty lines + if line.strip(): + print(line.rstrip()) + else: + print() + + print("=" * 42) + + def _render_terminal(self) -> None: + """Render C64 screen to terminal with dirty region optimization.""" + import sys as _sys + + screen_start = 0x0400 + cols = 40 + rows = 25 + + # Header row offset (4 lines: title, speed, separator, top border) + header_offset = 4 + + # Check if we need a full redraw + needs_full = self.dirty_tracker.needs_full_redraw() + + if needs_full: + # Full screen redraw + _sys.stdout.write("\033[2J\033[H") # Clear screen and move to top + + # Render C64 screen (40x25 characters) + _sys.stdout.write("=" * 42 + "\n") + _sys.stdout.write(" C64 SCREEN\n") + _sys.stdout.write("=" * 42 + "\n") + + for row in range(rows): + line = "" + for col in range(cols): + addr = screen_start + (row * cols) + col + petscii = int(self.cpu.ram[addr]) + line += self.petscii_to_ascii(petscii) + _sys.stdout.write(line + "\n") + + _sys.stdout.write("=" * 42 + "\n") + + elif self.dirty_tracker.has_changes(): + # Incremental update - only redraw dirty cells + dirty_cells = self.dirty_tracker.get_dirty_cells() + for row, col in dirty_cells: + # Move cursor to the cell position + # Terminal rows are 1-indexed, add header offset + term_row = row + header_offset + 1 + term_col = col + 1 + _sys.stdout.write(f"\033[{term_row};{term_col}H") + + # Get and render the character + addr = screen_start + (row * cols) + col + petscii = int(self.cpu.ram[addr]) + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(char) + + # Always update status line (at row 29: 3 header + 25 screen + 1 border) + # Add extra row if drive is attached + status_row = header_offset + rows + 2 + + # Move to status line and update it + _sys.stdout.write(f"\033[{status_row};1H") + status = self._format_cpu_status("C64") + # Clear line and write status + _sys.stdout.write("\033[K" + status) + + # Add drive status line if drive is attached + drive_status = self._format_drive_status() + if drive_status: + _sys.stdout.write(f"\n\033[K" + drive_status) + + # Clear dirty flags after rendering + self.dirty_tracker.clear() + + def _render_terminal_debug(self) -> None: + """Render C64 screen and CPU state to terminal (for pygame mode debug). + + This version uses ANSI escape codes to update in place without scrolling. + It shows the screen, CPU registers, and current instruction. + """ + import sys as _sys + + screen_start = 0x0400 + cols = 40 + rows = 25 + + # Get colors + bg_color = self.vic.regs[0x21] & 0x0F + bg_ansi = c64_to_ansi_bg(bg_color) + border_color = self.vic.regs[0x20] & 0x0F + border_ansi = c64_to_ansi_bg(border_color) + + # Clear screen and move to top + _sys.stdout.write("\033[2J\033[H") + + # Border and title + _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\n") + _sys.stdout.write(border_ansi + " " + ANSI_RESET + + " C64 (pygame mode) " + + border_ansi + " " * 24 + ANSI_RESET + "\n") + _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\n") + + # Screen content with colors + for row in range(rows): + # Left border + _sys.stdout.write(border_ansi + " " + ANSI_RESET) + + last_fg = -1 + for col in range(cols): + screen_addr = screen_start + (row * cols) + col + color_addr = row * cols + col + petscii = int(self.cpu.ram[screen_addr]) + fg_color = self.memory.ram_color[color_addr] & 0x0F + + # Check for reverse video (screen codes 128-255) + if petscii >= 128: + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(c64_to_ansi_bg(fg_color) + + c64_to_ansi_fg(bg_color) + char) + last_fg = -1 + else: + if fg_color != last_fg: + _sys.stdout.write(bg_ansi + c64_to_ansi_fg(fg_color)) + last_fg = fg_color + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(char) + + # Right border + _sys.stdout.write(ANSI_RESET + border_ansi + " " + ANSI_RESET + "\n") + + # Bottom border + _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\n") + + # C64 CPU status line + status = self._format_cpu_status("C64") + _sys.stdout.write(status + "\n") + + # Drive status line (if drive is attached) + drive_status = self._format_drive_status() + if drive_status: + _sys.stdout.write(drive_status + "\n") + + def _render_terminal_repl(self) -> None: + """Render C64 screen to terminal for REPL mode with color support. + + This version uses \r\n for line endings to work correctly in cbreak mode. + Colors are rendered using ANSI 256-color escape codes. + """ + import sys as _sys + + # Record speed sample for rolling average (once per second) + self._record_speed_sample() + + screen_start = 0x0400 + cols = 40 + rows = 25 + + # Get background color from VIC register $D021 + bg_color = self.vic.regs[0x21] & 0x0F + bg_ansi = c64_to_ansi_bg(bg_color) + + # Get border color from VIC register $D020 + border_color = self.vic.regs[0x20] & 0x0F + border_ansi = c64_to_ansi_bg(border_color) + + # Header row offset (4 lines: title, speed, separator, top border) + header_offset = 4 + + # Check if we need a full redraw + needs_full = self.dirty_tracker.needs_full_redraw() + + if needs_full: + # Full screen redraw + _sys.stdout.write("\033[2J\033[H") # Clear screen and move to top + + # Terminal header (not part of C64 display) + title_line = f"C64 REPL (Ctrl+C to exit) - {self.get_video_standard()} ({self.video_chip})" + stats = self.get_speed_stats() + if stats: + # Prefer rolling average if available, otherwise use lifetime average + cps = stats.get('rolling_cycles_per_second', stats['cycles_per_second']) + speedup = stats.get('rolling_speedup', stats['speedup']) + actual_mhz = cps / 1e6 + real_mhz = stats['real_cpu_freq'] / 1e6 + speed_line = f"{actual_mhz:.3f}MHz ({speedup:.1%} of {real_mhz:.3f}MHz)" + else: + speed_line = "(calculating speed...)" + _sys.stdout.write(f"{title_line}\r\n") + _sys.stdout.write(f"{speed_line}\r\n") + _sys.stdout.write("=" * 44 + "\r\n") + + # C64 screen with border (top border) + _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\r\n") + + for row in range(rows): + # Left border + _sys.stdout.write(border_ansi + " " + ANSI_RESET) + + # Screen content with colors + last_fg = -1 + for col in range(cols): + screen_addr = screen_start + (row * cols) + col + color_addr = row * cols + col + petscii = int(self.cpu.ram[screen_addr]) + fg_color = self.memory.ram_color[color_addr] & 0x0F + + # Check for reverse video (screen codes 128-255) + if petscii >= 128: + # Reverse video: swap fg and bg + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(c64_to_ansi_bg(fg_color) + + c64_to_ansi_fg(bg_color) + char) + last_fg = -1 # Force color reset + else: + # Normal: fg on bg + if fg_color != last_fg: + _sys.stdout.write(bg_ansi + c64_to_ansi_fg(fg_color)) + last_fg = fg_color + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(char) + + # Right border and reset + _sys.stdout.write(ANSI_RESET + border_ansi + " " + ANSI_RESET + "\r\n") + + # Bottom border + _sys.stdout.write(border_ansi + " " * 44 + ANSI_RESET + "\r\n") + + elif self.dirty_tracker.has_changes(): + # Incremental update - only redraw dirty cells + dirty_cells = self.dirty_tracker.get_dirty_cells() + for row, col in dirty_cells: + # Move cursor to the cell position + # Terminal rows are 1-indexed, add header offset, +2 for left border + term_row = row + header_offset + 1 + term_col = col + 3 # +2 for border, +1 for 1-indexing + _sys.stdout.write(f"\033[{term_row};{term_col}H") + + # Get screen and color data + screen_addr = screen_start + (row * cols) + col + color_addr = row * cols + col + petscii = int(self.cpu.ram[screen_addr]) + fg_color = self.memory.ram_color[color_addr] & 0x0F + + # Render with color + if petscii >= 128: + # Reverse video + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(c64_to_ansi_bg(fg_color) + + c64_to_ansi_fg(bg_color) + char + ANSI_RESET) + else: + char = self.petscii_to_ascii(petscii) + _sys.stdout.write(bg_ansi + c64_to_ansi_fg(fg_color) + char + ANSI_RESET) + + # Always update status line (at row 30: 3 header + 25 screen + 1 border + 1) + status_row = header_offset + rows + 2 + + # Move to status line and update it + _sys.stdout.write(f"\033[{status_row};1H") + status = self._format_cpu_status("C64") + # Clear line and write status + _sys.stdout.write("\033[K" + status) + + # Add drive status line if drive is attached + drive_status = self._format_drive_status() + if drive_status: + _sys.stdout.write(f"\n\033[K" + drive_status) + + # Clear dirty flags after rendering + self.dirty_tracker.clear() + + def _render_pygame(self) -> None: + """Render C64 screen to pygame window with dirty region optimization.""" + if not self.pygame_available or self.pygame_screen is None: + return + + try: + import pygame + + # Check if we've entered BASIC ROM (for conditional logging) + self._check_pc_region() + + # Handle pygame events (window close, keyboard, mouse, etc.) + for event in pygame.event.get(): + if event.type == pygame.QUIT: + raise errors.QuitRequestError("Window closed") + elif event.type == pygame.KEYDOWN: + if DEBUG_KEYBOARD: + log.info(f"*** PYGAME KEYDOWN EVENT: key={event.key} ***") + self._handle_pygame_keyboard(event, pygame) + elif event.type == pygame.KEYUP: + self._handle_pygame_keyboard(event, pygame) + elif event.type == pygame.MOUSEMOTION: + # Update mouse/paddle/lightpen position + if self._mouse_enabled: + # Mouse mode: relative motion (like 1351 proportional mouse) + self.update_mouse_motion(event.rel[0], event.rel[1]) + elif self._paddle_enabled: + # Paddle mode: absolute position scaled to window + # Mouse X → Paddle 1 (POTX), Mouse Y → Paddle 2 (POTY) + window_size = self.pygame_screen.get_size() + self.update_paddle_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) + elif self._lightpen_enabled: + # Lightpen mode: absolute position to VIC registers + window_size = self.pygame_screen.get_size() + self.update_lightpen_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) + elif event.type == pygame.MOUSEBUTTONDOWN: + # Mouse/paddle/lightpen button pressed + if self._mouse_enabled: + self.set_mouse_button(event.button, True) + elif self._paddle_enabled: + # Left click → Paddle 1 fire, Right click → Paddle 2 fire + self.set_paddle_button(event.button, True) + elif self._lightpen_enabled: + self.set_lightpen_button(event.button, True) + elif event.type == pygame.MOUSEBUTTONUP: + # Mouse/paddle/lightpen button released + if self._mouse_enabled: + self.set_mouse_button(event.button, False) + elif self._paddle_enabled: + self.set_paddle_button(event.button, False) + elif self._lightpen_enabled: + self.set_lightpen_button(event.button, False) + + # Check if VIC has a new frame ready (VBlank) + # VIC takes the snapshot at the exact moment of VBlank for consistency + new_frame = self.vic.frame_complete.is_set() + if new_frame: + self.vic.frame_complete.clear() + self._frame_count = getattr(self, '_frame_count', 0) + 1 + if self._frame_count <= 5 or self._frame_count % 50 == 0: + log.info(f"*** PYGAME: Caught frame {self._frame_count}, cycles={self.cpu.cycles_executed} ***") + + # Create memory wrappers for VIC + # Use VIC's snapshot (taken at VBlank) if available, otherwise live RAM + # The snapshot is only 16KB (one VIC bank), so we need to adjust addresses + class RAMWrapper: + def __init__(wrapper_self, snapshot, snapshot_bank, live_ram): + wrapper_self.snapshot = snapshot + wrapper_self.snapshot_bank = snapshot_bank + wrapper_self.live_ram = live_ram + + def __getitem__(wrapper_self, index): + if wrapper_self.snapshot is not None: + # Convert absolute address to bank-relative offset + # The snapshot covers snapshot_bank to snapshot_bank + 16KB + relative_index = index - wrapper_self.snapshot_bank + if 0 <= relative_index < len(wrapper_self.snapshot): + return wrapper_self.snapshot[relative_index] + # Address outside snapshot bank - fall through to live RAM + return int(wrapper_self.live_ram[index]) + + class ColorRAMWrapper: + def __init__(wrapper_self, snapshot, live_color): + wrapper_self.snapshot = snapshot + wrapper_self.live_color = live_color + + def __getitem__(wrapper_self, index): + if wrapper_self.snapshot is not None: + return wrapper_self.snapshot[index] & 0x0F + return wrapper_self.live_color[index] & 0x0F + + # Initialize glyph cache if needed + if not hasattr(self, '_glyph_cache'): + self._glyph_cache = {} + + vic = self.vic + ram_snapshot = vic.ram_snapshot + ram_snapshot_bank = vic.ram_snapshot_bank + color_snapshot = vic.color_snapshot + + def read_ram(addr): + if ram_snapshot is not None: + rel = addr - ram_snapshot_bank + if 0 <= rel < len(ram_snapshot): + return ram_snapshot[rel] + return int(self.cpu.ram[addr]) + + def read_color(idx): + if color_snapshot is not None: + return color_snapshot[idx] & 0x0F + return self.memory.ram_color[idx] & 0x0F + + surface = self.pygame_surface + + # --- Display-side rendering --- + border_color = vic.regs[0x20] & 0x0F + surface.fill(COLORS[border_color]) + + vic_bank = vic.get_vic_bank() + mem_control = vic.regs[0x18] + screen_base = vic_bank + ((mem_control & 0xF0) >> 4) * 0x0400 + char_bank_offset = ((mem_control & 0x0E) >> 1) * 0x0800 + + ecm = bool(vic.regs[0x11] & 0x40) + bmm = bool(vic.regs[0x11] & 0x20) + den = bool(vic.regs[0x11] & 0x10) + mcm = bool(vic.regs[0x16] & 0x10) + bg_color = vic.regs[0x21] & 0x0F + + hscroll = vic.regs[0x16] & 0x07 + vscroll = vic.regs[0x11] & 0x07 + x_origin = vic.border_left - hscroll + y_origin = vic.border_top - vscroll + + if den and not bmm and not ecm and not mcm: + # Standard text mode - cached glyph rendering + char_rom = vic.char_rom + for row in range(25): + for col in range(40): + cell_addr = screen_base + row * 40 + col + char_code = read_ram(cell_addr) + color = read_color(row * 40 + col) + + reverse = bool(char_code & 0x80) + char_code &= 0x7F + + glyph_addr = (char_code * 8) + char_bank_offset + glyph_addr &= 0x0FFF + glyph = char_rom[glyph_addr : glyph_addr + 8] + + if reverse: + fg, bg = bg_color, color + else: + fg, bg = color, bg_color + + cache_key = (tuple(glyph), fg, bg) + if cache_key not in self._glyph_cache: + glyph_surf = pygame.Surface((8, 8)) + fg_rgb = COLORS[fg] + bg_rgb = COLORS[bg] + for y in range(8): + line = glyph[y] + for x in range(8): + bit = (line >> (7 - x)) & 0x01 + glyph_surf.set_at((x, y), fg_rgb if bit else bg_rgb) + self._glyph_cache[cache_key] = glyph_surf + + base_x = x_origin + col * 8 + base_y = y_origin + row * 8 + surface.blit(self._glyph_cache[cache_key], (base_x, base_y)) + elif den: + # Other modes - fall back to VIC for now + ram_wrapper = RAMWrapper(ram_snapshot, ram_snapshot_bank, self.cpu.ram) + color_wrapper = ColorRAMWrapper(color_snapshot, self.memory.ram_color) + vic.render_frame(surface, ram_wrapper, color_wrapper) + + # Scale and blit to screen + scaled_surface = pygame.transform.scale( + surface, + (vic.total_width * self.scale, vic.total_height * self.scale) + ) + self.pygame_screen.blit(scaled_surface, (0, 0)) + pygame.display.flip() + + # Update window title with speed stats (rate-limited to once per second) + self._update_pygame_title(pygame) + + # Also render terminal repl output (screen with colors) + # Force full redraw since pygame already handled the dirty tracking + # DO NOT REMOVE - useful for debugging pygame mode via terminal + self.dirty_tracker.force_redraw() + self._render_terminal_repl() + + except Exception as e: + log.error(f"Error rendering pygame display: {e}") + + def _render_pygame_only(self) -> None: + """Render C64 screen to pygame window. + + All pygame rendering happens here in the display loop, not in the VIC core. + Uses glyph caching for fast text mode rendering. + """ + if not self.pygame_available or self.pygame_screen is None: + return + + try: + import pygame + import time as _time + + render_start = _time.perf_counter() + + # Check if VIC has a new frame ready (VBlank) + new_frame = self.vic.frame_complete.is_set() + if new_frame: + self.vic.frame_complete.clear() + self._frame_count = getattr(self, '_frame_count', 0) + 1 + + # Initialize glyph cache if needed + if not hasattr(self, '_glyph_cache'): + self._glyph_cache = {} + + # Get RAM snapshot or live RAM + vic = self.vic + ram_snapshot = vic.ram_snapshot + ram_snapshot_bank = vic.ram_snapshot_bank + color_snapshot = vic.color_snapshot + + # Helper to read RAM + def read_ram(addr): + if ram_snapshot is not None: + rel = addr - ram_snapshot_bank + if 0 <= rel < len(ram_snapshot): + return ram_snapshot[rel] + return int(self.cpu.ram[addr]) + + def read_color(idx): + if color_snapshot is not None: + return color_snapshot[idx] & 0x0F + return self.memory.ram_color[idx] & 0x0F + + surface = self.pygame_surface + + # --- Render frame (display-side, no VIC pygame code) --- + + # Border colour + border_color = vic.regs[0x20] & 0x0F + surface.fill(COLORS[border_color]) + + # Get VIC bank from CIA2 + vic_bank = vic.get_vic_bank() + + # Decode $D018 + mem_control = vic.regs[0x18] + screen_base = vic_bank + ((mem_control & 0xF0) >> 4) * 0x0400 + char_bank_offset = ((mem_control & 0x0E) >> 1) * 0x0800 + + # Mode flags + ecm = bool(vic.regs[0x11] & 0x40) + bmm = bool(vic.regs[0x11] & 0x20) + den = bool(vic.regs[0x11] & 0x10) + mcm = bool(vic.regs[0x16] & 0x10) + + bg_color = vic.regs[0x21] & 0x0F + + hscroll = vic.regs[0x16] & 0x07 + vscroll = vic.regs[0x11] & 0x07 + x_origin = vic.border_left - hscroll + y_origin = vic.border_top - vscroll + + if den and not bmm and not ecm and not mcm: + # Standard text mode - use cached glyph rendering + char_rom = vic.char_rom + + for row in range(25): + for col in range(40): + cell_addr = screen_base + row * 40 + col + char_code = read_ram(cell_addr) + + color = read_color(row * 40 + col) + + reverse = bool(char_code & 0x80) + char_code &= 0x7F + + glyph_addr = (char_code * 8) + char_bank_offset + glyph_addr &= 0x0FFF + glyph = char_rom[glyph_addr : glyph_addr + 8] + + if reverse: + fg, bg = bg_color, color + else: + fg, bg = color, bg_color + + # Cache lookup + cache_key = (tuple(glyph), fg, bg) + if cache_key not in self._glyph_cache: + # Render glyph to cache + glyph_surf = pygame.Surface((8, 8)) + fg_rgb = COLORS[fg] + bg_rgb = COLORS[bg] + for y in range(8): + line = glyph[y] + for x in range(8): + bit = (line >> (7 - x)) & 0x01 + glyph_surf.set_at((x, y), fg_rgb if bit else bg_rgb) + self._glyph_cache[cache_key] = glyph_surf + + # Blit cached glyph + base_x = x_origin + col * 8 + base_y = y_origin + row * 8 + surface.blit(self._glyph_cache[cache_key], (base_x, base_y)) + + elif den: + # Other modes - fall back to VIC renderer for now + # TODO: Move bitmap/multicolor/ECM rendering here too + class RAMWrapper: + def __getitem__(wrapper_self, index): + return read_ram(index) + + class ColorWrapper: + def __getitem__(wrapper_self, index): + return read_color(index) + + vic.render_frame(surface, RAMWrapper(), ColorWrapper()) + + # Scale and blit to screen + scaled_surface = pygame.transform.scale( + surface, + (vic.total_width * self.scale, vic.total_height * self.scale) + ) + self.pygame_screen.blit(scaled_surface, (0, 0)) + pygame.display.flip() + + # Log render time periodically + render_time = _time.perf_counter() - render_start + if self._frame_count <= 5 or self._frame_count % 60 == 0: + cache_size = len(self._glyph_cache) + log.critical( + f"*** RENDER: {render_time*1000:.1f}ms " + f"(frame {self._frame_count}, cache={cache_size}) ***" + ) + + except Exception as e: + log.error(f"Error rendering pygame display: {e}") + + def _pump_pygame_events(self) -> None: + """Process pygame events without rendering. + + Called when the frame_complete wait times out to keep the UI + responsive (handle window close, keyboard input, mouse input, etc.). + """ + if not self.pygame_available or self.pygame_screen is None: + return + + try: + import pygame + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + raise errors.QuitRequestError("Window closed") + elif event.type == pygame.KEYDOWN: + if DEBUG_KEYBOARD: + log.info(f"*** PYGAME KEYDOWN EVENT: key={event.key} ***") + self._handle_pygame_keyboard(event, pygame) + elif event.type == pygame.KEYUP: + self._handle_pygame_keyboard(event, pygame) + elif event.type == pygame.MOUSEMOTION: + # Update mouse/paddle/lightpen position + if self._mouse_enabled: + # Mouse mode: relative motion (like 1351 proportional mouse) + self.update_mouse_motion(event.rel[0], event.rel[1]) + elif self._paddle_enabled: + # Paddle mode: absolute position scaled to window + # Mouse X → Paddle 1 (POTX), Mouse Y → Paddle 2 (POTY) + window_size = self.pygame_screen.get_size() + self.update_paddle_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) + elif self._lightpen_enabled: + # Lightpen mode: absolute position to VIC registers + window_size = self.pygame_screen.get_size() + self.update_lightpen_position(event.pos[0], event.pos[1], window_size[0], window_size[1]) + elif event.type == pygame.MOUSEBUTTONDOWN: + # Mouse/paddle/lightpen button pressed + if self._mouse_enabled: + self.set_mouse_button(event.button, True) + elif self._paddle_enabled: + # Left click → Paddle 1 fire, Right click → Paddle 2 fire + self.set_paddle_button(event.button, True) + elif self._lightpen_enabled: + self.set_lightpen_button(event.button, True) + elif event.type == pygame.MOUSEBUTTONUP: + # Mouse/paddle/lightpen button released + if self._mouse_enabled: + self.set_mouse_button(event.button, False) + elif self._paddle_enabled: + self.set_paddle_button(event.button, False) + elif self._lightpen_enabled: + self.set_lightpen_button(event.button, False) + + except errors.QuitRequestError: + raise # Re-raise quit request to propagate up + except Exception as e: + log.error(f"Error pumping pygame events: {e}") + + # ASCII to C64 keyboard matrix mapping + # Maps ASCII characters to (row, col) positions in the C64 keyboard matrix + # Some characters require SHIFT to be pressed + ASCII_TO_MATRIX = { + # Letters (unshifted = uppercase on C64 in default mode) + 'A': (1, 2), 'B': (3, 4), 'C': (2, 4), 'D': (2, 2), 'E': (1, 6), + 'F': (2, 5), 'G': (3, 2), 'H': (3, 5), 'I': (4, 1), 'J': (4, 2), + 'K': (4, 5), 'L': (5, 2), 'M': (4, 4), 'N': (4, 7), 'O': (4, 6), + 'P': (5, 1), 'Q': (7, 6), 'R': (2, 1), 'S': (1, 5), 'T': (2, 6), + 'U': (3, 6), 'V': (3, 7), 'W': (1, 1), 'X': (2, 7), 'Y': (3, 1), + 'Z': (1, 4), + # Lowercase (same matrix position, C64 handles shift mode internally) + 'a': (1, 2), 'b': (3, 4), 'c': (2, 4), 'd': (2, 2), 'e': (1, 6), + 'f': (2, 5), 'g': (3, 2), 'h': (3, 5), 'i': (4, 1), 'j': (4, 2), + 'k': (4, 5), 'l': (5, 2), 'm': (4, 4), 'n': (4, 7), 'o': (4, 6), + 'p': (5, 1), 'q': (7, 6), 'r': (2, 1), 's': (1, 5), 't': (2, 6), + 'u': (3, 6), 'v': (3, 7), 'w': (1, 1), 'x': (2, 7), 'y': (3, 1), + 'z': (1, 4), + # Digits + '0': (4, 3), '1': (7, 0), '2': (7, 3), '3': (1, 0), '4': (1, 3), + '5': (2, 0), '6': (2, 3), '7': (3, 0), '8': (3, 3), '9': (4, 0), + # Special characters + ' ': (7, 4), # SPACE + '\n': (0, 1), # RETURN + '\r': (0, 1), # RETURN + '+': (5, 0), + '-': (5, 3), + '*': (6, 1), + '/': (6, 7), + '=': (6, 5), + '.': (5, 4), + ',': (5, 7), + ':': (5, 5), + ';': (6, 2), + '@': (5, 6), + '#': None, # Requires SHIFT+3 (handled specially) + '$': None, # Requires SHIFT+4 (handled specially) + '%': None, # Requires SHIFT+5 (handled specially) + '(': None, # Requires SHIFT+8 (handled specially) + ')': None, # Requires SHIFT+9 (handled specially) + '"': None, # Requires SHIFT+2 (handled specially) + '!': None, # Requires SHIFT+1 (handled specially) + '?': None, # Requires SHIFT+/ (handled specially) + '<': None, # Requires SHIFT+, (handled specially) + '>': None, # Requires SHIFT+. (handled specially) + } + + # Characters that require SHIFT: (shift_row, shift_col, key_row, key_col) + ASCII_SHIFTED = { + '!': (1, 7, 7, 0), # SHIFT + 1 + '"': (1, 7, 7, 3), # SHIFT + 2 + '#': (1, 7, 1, 0), # SHIFT + 3 + '$': (1, 7, 1, 3), # SHIFT + 4 + '%': (1, 7, 2, 0), # SHIFT + 5 + '&': (1, 7, 2, 3), # SHIFT + 6 + "'": (1, 7, 7, 3), # SHIFT + 2 - produces " (double quote) for BASIC strings + '(': (1, 7, 3, 3), # SHIFT + 8 + ')': (1, 7, 4, 0), # SHIFT + 9 + '?': (1, 7, 6, 7), # SHIFT + / + '<': (1, 7, 5, 7), # SHIFT + , + '>': (1, 7, 5, 4), # SHIFT + . + } diff --git a/systems/c64/mixins/drive.py b/systems/c64/mixins/drive.py new file mode 100644 index 0000000..37792d9 --- /dev/null +++ b/systems/c64/mixins/drive.py @@ -0,0 +1,382 @@ +"""C64 Drive Mixin. + +Mixin for 1541 disk drive operations. +""" + +from mos6502.compat import logging, Path, Optional +from mos6502 import CPU, CPUVariant + +# Drive module imports are conditional (not available on Pico) +_DRIVE_AVAILABLE = False +try: + from c64.drive import ( + Drive1541, + IECBus, + ThreadedDrive1541, + ThreadedIECBus, + MultiprocessDrive1541, + MultiprocessIECBus, + SharedIECState, + _THREADED_AVAILABLE, + _MULTIPROCESS_AVAILABLE, + ) + _DRIVE_AVAILABLE = True +except ImportError: + Drive1541 = None + IECBus = None + ThreadedDrive1541 = None + ThreadedIECBus = None + MultiprocessDrive1541 = None + MultiprocessIECBus = None + SharedIECState = None + _THREADED_AVAILABLE = False + _MULTIPROCESS_AVAILABLE = False + +log = logging.getLogger("c64") + + +class C64DriveMixin: + """Mixin for 1541 disk drive operations.""" + + def attach_drive(self, drive_rom_path: Optional[Path] = None, disk_path: Optional[Path] = None, + runner: str = "synchronous") -> bool: + """Attach a 1541 disk drive to the IEC bus. + + Supports multiple ROM formats: + - Single 16KB ROM (1541-II style): 1541.rom, 1541-II.251968-03.bin, etc. + - Two 8KB ROMs (original 1541): 1541-c000.bin + 1541-e000.bin + + Args: + drive_rom_path: Path to 1541 DOS ROM (default: auto-detect in rom_dir) + disk_path: Optional D64 disk image to insert + runner: Drive execution mode: + - "synchronous" (default): Cycle-accurate emulation + - "threaded": Uses ThreadedIECBus for atomic state + - "multiprocess": Drive runs in separate process (bypasses GIL) + + Returns: + True if drive attached successfully, False otherwise + """ + # Drive module is optional (not available on Pico) + if not _DRIVE_AVAILABLE: + log.warning("Drive module not available - disk drive support disabled") + return False + rom_path = drive_rom_path + rom_path_e000 = None + + if rom_path is None: + # Try to find 1541 ROM(s) in rom_dir + # Priority 1: Single 16KB ROM files (most common) + rom_16k_names = [ + "1541.rom", + "1541-II.rom", + "1541-II.251968-03.bin", # Most compatible 1541-II ROM + "1541C.251968-01.bin", + "1541C.251968-02.bin", + "1541-II.355640-01.bin", + "dos1541", + "dos1541.rom", + ] + for name in rom_16k_names: + candidate = self.rom_dir / name + if candidate.exists(): + rom_path = candidate + break + + # Priority 2: Two 8KB ROM files (original 1541 style) + if rom_path is None: + # Look for C000 ROM + c000_names = [ + "1541-c000.325302-01.bin", + "1541-c000.bin", + "1540-c000.325302-01.bin", + ] + # Look for E000 ROM (multiple revisions available) + e000_names = [ + "1541-e000.901229-05.bin", # Short-board, most common + "1541-e000.901229-03.bin", # Long-board with autoboot + "1541-e000.901229-02.bin", + "1541-e000.901229-01.bin", + "1541-e000.bin", + ] + + for c000_name in c000_names: + c000_candidate = self.rom_dir / c000_name + if c000_candidate.exists(): + # Found C000 ROM, now look for E000 ROM + for e000_name in e000_names: + e000_candidate = self.rom_dir / e000_name + if e000_candidate.exists(): + rom_path = c000_candidate + rom_path_e000 = e000_candidate + break + if rom_path_e000 is not None: + break + + if rom_path is None or not rom_path.exists(): + log.warning(f"1541 ROM not found in {self.rom_dir}. Drive disabled.") + log.warning(" Expected: 1541.rom (16KB) or 1541-c000.bin + 1541-e000.bin (8KB each)") + return False + + # Track the runner mode + self.drive_runner = runner + + # Validate runner mode is available (threaded/multiprocess not available on MicroPython) + if runner == "multiprocess" and not _MULTIPROCESS_AVAILABLE: + log.warning("Multiprocess drive mode not available, falling back to synchronous") + runner = "synchronous" + self.drive_runner = runner + elif runner == "threaded" and not _THREADED_AVAILABLE: + log.warning("Threaded drive mode not available, falling back to synchronous") + runner = "synchronous" + self.drive_runner = runner + + if runner == "multiprocess": + # Multiprocess mode: Drive runs in separate process (bypasses GIL) + import os + import time + + # Create shared memory for IEC state (use PID + time for uniqueness) + unique_id = f"{os.getpid()}_{int(time.time() * 1000) % 1000000}" + self._iec_shared_state = SharedIECState( + name=f"iec_bus_{unique_id}", + create=True + ) + + # Create multiprocess IEC bus + self.iec_bus = MultiprocessIECBus(self._iec_shared_state) + self.iec_bus.connect_c64(self.cia2) + self.cia2.set_iec_bus(self.iec_bus) + + # Create and start drive subprocess + self.drive8 = MultiprocessDrive1541(8) + self.drive8.start_process( + rom_path=rom_path, + rom_path_e000=rom_path_e000, + disk_path=disk_path, + shared_state=self._iec_shared_state, + ) + + # Wire up tick synchronization Events + tick_request, tick_done = self.drive8.get_tick_events() + self.iec_bus.set_tick_events(tick_request, tick_done) + + self.drive_enabled = True + + # Set up synchronous tick-based execution + # Similar to threaded mode but with batching to reduce IPC overhead + self._mp_accumulated_cycles = 0 + self._mp_batch_size = 100 # Cycles per IPC call (balance timing vs overhead) + + def sync_multiprocess(cpu, cycles): + """Accumulate cycles and sync with drive subprocess.""" + if self.drive8 and self._iec_shared_state: + # Update IEC bus state + self.iec_bus.update() + + # Accumulate cycles + self._mp_accumulated_cycles += cycles + + # When batch is full, sync with drive + if self._mp_accumulated_cycles >= self._mp_batch_size: + batch = self._mp_accumulated_cycles + self._mp_accumulated_cycles = 0 + + # Update C64 cycle counter + self._iec_shared_state.set_c64_cycles(cpu.cycles_executed) + + # Wait for drive to catch up to our cycle count + tick_request, tick_done = self.drive8.get_tick_events() + target_cycles = cpu.cycles_executed + + # Signal drive and wait for it to process + import time + max_wait = 0.1 # seconds + start = time.time() + while time.time() - start < max_wait: + tick_request.set() + drive_cycles = self._iec_shared_state.get_drive_cycles() + if drive_cycles >= target_cycles - 50: + break + time.sleep(0.00001) # 10us + + # Read bus state after drive processed + self.iec_bus.atn, self.iec_bus.clk, self.iec_bus.data = \ + self._iec_shared_state.get_bus_state(False) + + self.cpu.post_tick_callback = sync_multiprocess + log.info(f"1541 drive 8 attached in MULTIPROCESS mode (ROM: {rom_path.name})") + return True + + elif runner == "threaded": + # Threaded mode: Uses ThreadedIECBus for atomic state updates + self.iec_bus = ThreadedIECBus() + self.iec_bus.connect_c64(self.cia2) + self.cia2.set_iec_bus(self.iec_bus) + + # Create threaded drive 8 + self.drive8 = ThreadedDrive1541(8) + else: + # Synchronous mode: Cycle-accurate but slower + self.iec_bus = IECBus() + self.iec_bus.connect_c64(self.cia2) + self.cia2.set_iec_bus(self.iec_bus) + + # Create standard drive 8 + self.drive8 = Drive1541(8) + + # Create a separate CPU for the drive (for threaded/synchronous modes) + # The 1541 uses a full 6502 (not 6510) + drive_cpu = CPU(CPUVariant.NMOS_6502, False) + self.drive8.cpu = drive_cpu + + # Set up drive CPU memory handler + drive_cpu.ram.memory_handler = self.drive8.memory + + # Load 1541 ROM (supports both 16KB single file and 8KB+8KB split) + try: + self.drive8.load_rom(rom_path, rom_path_e000) + except Exception as e: + log.error(f"Failed to load 1541 ROM: {e}") + self.drive8 = None + self.iec_bus = None + return False + + # Connect drive to IEC bus + if runner == "threaded": + self.drive8.connect_to_threaded_bus(self.iec_bus) + else: + self.iec_bus.connect_drive(self.drive8) + + # Insert disk if provided + if disk_path is not None: + try: + self.drive8.insert_disk(disk_path) + except Exception as e: + log.error(f"Failed to insert disk: {e}") + + # Reset drive to initialize + self.drive8.reset() + + self.drive_enabled = True + + if runner == "threaded": + # Threaded mode: Uses ThreadedIECBus for thread-safe state + # but still runs drive in lockstep via post_tick_callback + # The threading benefit is that bus state updates are atomic + + def sync_drive_on_tick_threaded(cpu, cycles): + """Sync drive CPU and IEC bus after each C64 cycle consumption.""" + if self.drive8 and self.drive8.cpu: + # Update IEC bus state so drive sees current C64 outputs + self.iec_bus.update() + + # Run drive for the same number of cycles (1:1 sync) + # Call the base class tick() directly since ThreadedDrive1541.tick() is a no-op + Drive1541.tick(self.drive8, cycles) + + # Update IEC bus again so C64 sees drive's response + self.iec_bus.update() + + self.cpu.post_tick_callback = sync_drive_on_tick_threaded + # Note: Not starting drive thread - running synchronously with ThreadedIECBus + log.info(f"1541 drive 8 attached with ThreadedIECBus (ROM: {rom_path.name})") + else: + # Synchronous mode: Set up cycle-accurate IEC synchronization + # using post_tick_callback. The tick() function is called every + # time the CPU spends cycles, which is the natural place to + # synchronize connected hardware. + # + # The IEC serial bus is bit-banged and requires tight timing between + # the C64 and 1541 CPUs. By hooking into tick(), we ensure the drive + # runs in lockstep with every cycle the C64 spends. + + def sync_drive_on_tick(cpu, cycles): + """Sync drive CPU and IEC bus after each C64 cycle consumption.""" + if self.drive8 and self.drive8.cpu: + # Update IEC bus state so drive sees current C64 outputs + self.iec_bus.update() + + # Run drive for the same number of cycles + # The drive's tick() method handles its own cycle budget + self.drive8.tick(cycles) + + # Update IEC bus again so C64 sees drive's response + self.iec_bus.update() + + self.cpu.post_tick_callback = sync_drive_on_tick + log.info(f"1541 drive 8 attached in SYNCHRONOUS mode (ROM: {rom_path.name})") + + return True + + def insert_disk(self, disk_path: Path) -> bool: + """Insert a D64 disk image into drive 8. + + Args: + disk_path: Path to D64 disk image + + Returns: + True if disk inserted successfully + """ + if not self.drive_enabled or self.drive8 is None: + log.error("No drive attached. Use --disk or attach_drive() first.") + return False + + try: + self.drive8.insert_disk(disk_path) + return True + except Exception as e: + log.error(f"Failed to insert disk: {e}") + return False + + def eject_disk(self) -> None: + """Eject the disk from drive 8.""" + if self.drive8 is not None: + self.drive8.eject_disk() + + def cleanup(self) -> None: + """Clean up resources (drive subprocess, shared memory, etc.).""" + # Stop multiprocess drive if running + if self.drive8 is not None: + # Check class types carefully - they may be None if drive module unavailable + if MultiprocessDrive1541 is not None and isinstance(self.drive8, MultiprocessDrive1541): + self.drive8.stop_process() + elif ThreadedDrive1541 is not None and isinstance(self.drive8, ThreadedDrive1541): + self.drive8.stop_thread() + + # Clean up shared memory + if hasattr(self, '_iec_shared_state') and self._iec_shared_state is not None: + try: + self._iec_shared_state.close() + self._iec_shared_state.unlink() + except Exception: + pass + self._iec_shared_state = None + + def get_drive_pc_region(self) -> str: + """Determine which memory region the drive's PC is currently in. + + 1541 memory map: + - $0000-$07FF: RAM (2KB) + - $1800-$1BFF: VIA1 + - $1C00-$1FFF: VIA2 + - $C000-$FFFF: ROM (DOS) + + Returns: + String describing the region: "RAM", "VIA1", "VIA2", "DOS", or "???" + """ + if not self.drive8 or not self.drive8.cpu: + return "???" + + pc = self.drive8.cpu.PC + + if pc < 0x0800: + return "RAM" + elif 0x1800 <= pc < 0x1C00: + return "VIA1" + elif 0x1C00 <= pc < 0x2000: + return "VIA2" + elif pc >= 0xC000: + return "DOS" + else: + return "???" diff --git a/systems/c64/mixins/input_devices.py b/systems/c64/mixins/input_devices.py new file mode 100644 index 0000000..93a2346 --- /dev/null +++ b/systems/c64/mixins/input_devices.py @@ -0,0 +1,348 @@ +"""C64 Input Devices Mixin. + +Mixin for input devices (mouse, paddle, lightpen, joystick). +""" + +from mos6502.compat import logging, Union +from c64.cia1 import ( + PADDLE_1_FIRE, + PADDLE_2_FIRE, + MOUSE_LEFT_BUTTON, + MOUSE_RIGHT_BUTTON, + LIGHTPEN_BUTTON, + JOYSTICK_UP, + JOYSTICK_DOWN, + JOYSTICK_LEFT, + JOYSTICK_RIGHT, + JOYSTICK_FIRE, +) + +log = logging.getLogger("c64") + + +class C64InputDevicesMixin: + """Mixin for input devices (mouse, paddle, lightpen, joystick).""" + + def enable_mouse(self, enabled: bool = True, port: int = 1, sensitivity: float = 1.0) -> None: + """Enable or disable mouse input. + + Args: + enabled: Whether mouse input is enabled + port: Which joystick port (1 or 2) the mouse is connected to + sensitivity: Scale factor for mouse motion (1.0 = 1:1 mapping) + """ + self._mouse_enabled = enabled + self._mouse_port = port + self._mouse_sensitivity = sensitivity + self.sid.mouse_enabled = enabled + log.info(f"Mouse input {'enabled' if enabled else 'disabled'} on port {port}, sensitivity={sensitivity}") + + def update_mouse_motion(self, delta_x: int, delta_y: int) -> None: + """Update mouse position from motion delta. + + The 1351 mouse sends relative motion that wraps around 0-255. + This method should be called with pygame MOUSEMOTION event rel values. + + Args: + delta_x: Horizontal motion in pixels (positive = right) + delta_y: Vertical motion in pixels (positive = down) + """ + if not self._mouse_enabled: + return + + # Apply sensitivity scaling + scaled_x = int(delta_x * self._mouse_sensitivity) + scaled_y = int(delta_y * self._mouse_sensitivity) + + # Update SID POT registers + self.sid.update_mouse(scaled_x, scaled_y) + + def set_mouse_button(self, button: int, pressed: bool) -> None: + """Set mouse button state. + + Args: + button: Button number (1 = left, 3 = right for pygame) + pressed: True if button is pressed, False if released + """ + if not self._mouse_enabled: + return + + # Get the joystick register for the configured port + if self._mouse_port == 1: + joystick = self.cia1.joystick_1 + else: + joystick = self.cia1.joystick_2 + + # Mouse buttons use active-low logic (0 = pressed, 1 = released) + # Left button (1) = Fire (bit 4) + # Right button (3) = Up (bit 0) - common mapping for 1351 + if button == 1: # Left button + if pressed: + joystick &= ~MOUSE_LEFT_BUTTON # Clear bit (pressed) + else: + joystick |= MOUSE_LEFT_BUTTON # Set bit (released) + elif button == 3: # Right button + if pressed: + joystick &= ~MOUSE_RIGHT_BUTTON # Clear bit (pressed) + else: + joystick |= MOUSE_RIGHT_BUTTON # Set bit (released) + + # Update the joystick state + if self._mouse_port == 1: + self.cia1.joystick_1 = joystick + else: + self.cia1.joystick_2 = joystick + + # ========================================================================= + # Paddle Input Support + # ========================================================================= + # Paddles use the same SID POT registers as the 1351 mouse, but with + # absolute positioning instead of relative motion. + # - Two paddles per port (directly reading the same POT registers) + # - Each paddle has a fire button (directly triggers joystick port bits) + # - Paddle 1: POTX ($D419), fire on bit 2 of joystick port + # - Paddle 2: POTY ($D41A), fire on bit 3 of joystick port + # Fire buttons active-low on the joystick port. + + _paddle_enabled: bool = False + _paddle_port: int = 1 # Which joystick port (1 or 2) + + def enable_paddle(self, enabled: bool = True, port: int = 1) -> None: + """Enable or disable paddle input. + + Args: + enabled: Whether paddle input is enabled + port: Which joystick port (1 or 2) the paddles are connected to + """ + self._paddle_enabled = enabled + self._paddle_port = port + log.info(f"Paddle input {'enabled' if enabled else 'disabled'} on port {port}") + + def update_paddle_position(self, mouse_x: int, mouse_y: int, window_width: int, window_height: int) -> None: + """Update paddle position from absolute mouse position. + + Args: + mouse_x: Mouse X position in window (pixels) + mouse_y: Mouse Y position in window (pixels) + window_width: Window width (pixels) + window_height: Window height (pixels) + """ + if not self._paddle_enabled: + return + + # Scale mouse position to paddle range (0-255) + # Clamp to valid range in case mouse is outside window + paddle_x = max(0, min(255, int((mouse_x / max(1, window_width)) * 255))) + paddle_y = max(0, min(255, int((mouse_y / max(1, window_height)) * 255))) + + # Update SID POT registers + self.sid.set_paddle(paddle_x, paddle_y) + + def set_paddle_button(self, button: int, pressed: bool) -> None: + """Set paddle button state. + + C64 paddle fire buttons use different CIA bits than joystick fire: + - Paddle 1 (X-axis paddle) fire: Bit 2 of CIA port + - Paddle 2 (Y-axis paddle) fire: Bit 3 of CIA port + + Port 1 paddles read from $DC01 (CIA1 Port B) + Port 2 paddles read from $DC00 (CIA1 Port A) + + Reference: https://www.c64-wiki.com/wiki/Paddle + + Args: + button: Mouse button (1 = left → paddle 1 fire, 3 = right → paddle 2 fire) + pressed: True if button is pressed, False if released + """ + if not self._paddle_enabled: + return + + # Get the joystick register for the configured port + if self._paddle_port == 1: + joystick = self.cia1.joystick_1 + else: + joystick = self.cia1.joystick_2 + + # Paddle buttons use active-low logic (0 = pressed, 1 = released) + # Real C64 paddle fire buttons: + # - Paddle 1 (X) fire = Bit 2 (directly wired to control port pin 3) + # - Paddle 2 (Y) fire = Bit 3 (directly wired to control port pin 4) + if button == 1: # Left mouse button = Paddle 1 fire + if pressed: + joystick &= ~PADDLE_1_FIRE # Clear bit 2 (pressed) + else: + joystick |= PADDLE_1_FIRE # Set bit 2 (released) + elif button == 3: # Right mouse button = Paddle 2 fire + if pressed: + joystick &= ~PADDLE_2_FIRE # Clear bit 3 (pressed) + else: + joystick |= PADDLE_2_FIRE # Set bit 3 (released) + + # Update the joystick state + if self._paddle_port == 1: + self.cia1.joystick_1 = joystick + else: + self.cia1.joystick_2 = joystick + + # ========================================================================= + # Lightpen Input Support + # ========================================================================= + # Lightpen uses VIC-II registers for position (not SID POT like mouse/paddle): + # - $D013 (LPX): X coordinate divided by 2 (multiply by 2 to get actual X) + # - $D014 (LPY): Y coordinate (same as sprite Y coordinates) + # - Button triggers joystick fire on port 1 only (bit 4 of $DC01) + # - Can also trigger VIC IRQ bit 3 when position is latched + # + # Lightpen only works on control port 1 (directly wired to VIC). + # Reference: https://www.c64-wiki.com/wiki/Light_pen + + _lightpen_enabled: bool = False + + def enable_lightpen(self, enabled: bool = True) -> None: + """Enable or disable lightpen input. + + Note: Lightpen only works on control port 1 (hardware limitation). + + Args: + enabled: Whether lightpen input is enabled + """ + self._lightpen_enabled = enabled + log.info(f"Lightpen input {'enabled' if enabled else 'disabled'}") + + def update_lightpen_position(self, mouse_x: int, mouse_y: int, + window_width: int, window_height: int) -> None: + """Update lightpen position from mouse position. + + Maps mouse window coordinates to VIC-II lightpen registers. + The VIC stores X/2 in $D013 and Y in $D014, using sprite coordinate space. + + Args: + mouse_x: Mouse X position in window (pixels) + mouse_y: Mouse Y position in window (pixels) + window_width: Window width (pixels) + window_height: Window height (pixels) + """ + if not self._lightpen_enabled: + return + + # VIC-II visible area in sprite coordinates: + # PAL: X = 24-343 (320 pixels), Y = 50-249 (200 pixels) + # We map the window to the visible screen area + + # X coordinate: map window X to sprite X range (24-343), then divide by 2 + # The visible screen is 320 pixels wide, starting at sprite X=24 + sprite_x = 24 + int((mouse_x / max(1, window_width)) * 320) + sprite_x = max(0, min(511, sprite_x)) # Clamp to 9-bit range + lpx = sprite_x // 2 # VIC stores X/2 + + # Y coordinate: map window Y to sprite Y range (50-249) + # The visible screen is 200 pixels tall, starting at sprite Y=50 + sprite_y = 50 + int((mouse_y / max(1, window_height)) * 200) + sprite_y = max(0, min(255, sprite_y)) # Clamp to 8-bit range + + # Update VIC lightpen registers + self.vic.regs[0x13] = lpx & 0xFF + self.vic.regs[0x14] = sprite_y & 0xFF + + def set_lightpen_button(self, button: int, pressed: bool) -> None: + """Set lightpen button state. + + Lightpen button is directly wired to joystick fire on port 1. + Only left mouse button is used (lightpens have one button). + + Reference: https://www.c64-wiki.com/wiki/Light_pen + + Args: + button: Mouse button (1 = left/lightpen button) + pressed: True if button is pressed, False if released + """ + if not self._lightpen_enabled: + return + + # Lightpen only uses port 1 (hardware constraint) + joystick = self.cia1.joystick_1 + + # Lightpen button uses active-low logic (0 = pressed, 1 = released) + # Only left button (1) triggers the lightpen + if button == 1: + if pressed: + joystick &= ~LIGHTPEN_BUTTON # Clear bit (pressed) + else: + joystick |= LIGHTPEN_BUTTON # Set bit (released) + + self.cia1.joystick_1 = joystick + + # ========================================================================= + # Keyboard Joystick Emulation + # ========================================================================= + # Maps keyboard keys to joystick directions and fire button. + # Supports both numpad (8/2/4/6/0) and WASD+Space layouts. + # Default port is 2 since most C64 games expect joystick in port 2. + # + # C64 joystick uses active-low logic (0 = pressed, 1 = released): + # - Bit 0: Up + # - Bit 1: Down + # - Bit 2: Left + # - Bit 3: Right + # - Bit 4: Fire + # Reference: https://www.c64-wiki.com/wiki/Joystick + + _joystick_enabled: bool = False + _joystick_port: int = 2 # Default to port 2 (most common for C64 games) + + def enable_joystick(self, enabled: bool = True, port: int = 2) -> None: + """Enable or disable keyboard joystick emulation. + + When enabled, keyboard keys are mapped to joystick directions: + - Numpad: 8=Up, 2=Down, 4=Left, 6=Right, 0=Fire + - WASD: W=Up, S=Down, A=Left, D=Right, Space=Fire + + Args: + enabled: Whether joystick emulation is enabled + port: Which joystick port to emulate (1 or 2, default: 2) + """ + self._joystick_enabled = enabled + self._joystick_port = port + log.info(f"Keyboard joystick {'enabled' if enabled else 'disabled'} on port {port}") + + def set_joystick_direction(self, direction: int, pressed: bool) -> None: + """Set a joystick direction or fire button state. + + Args: + direction: Direction bit (JOYSTICK_UP, JOYSTICK_DOWN, etc.) + pressed: True if pressed, False if released + """ + if not self._joystick_enabled: + return + + if self._joystick_port == 1: + joystick = self.cia1.joystick_1 + else: + joystick = self.cia1.joystick_2 + + # Active-low logic: 0 = pressed, 1 = released + if pressed: + joystick &= ~direction # Clear bit (pressed) + else: + joystick |= direction # Set bit (released) + + if self._joystick_port == 1: + self.cia1.joystick_1 = joystick + else: + self.cia1.joystick_2 = joystick + + # Pending key releases for non-blocking terminal input + # Each entry is (release_cycles, row, col) - uses CPU cycles, not wall-clock time + _pending_key_releases: list = [] + + # Pygame keyboard buffer for type-ahead + # Stores (row, col, needs_shift) tuples for keys waiting to be injected + _pygame_key_buffer: list = [] + _pygame_keys_currently_pressed: set = set() # Track physical key state + _pygame_current_injection: Union[tuple, None]= None # (row, col, needs_shift, start_cycles, released) + + # Timing in CPU cycles (not wall-clock) so keys inject correctly at any emulator speed + # KERNAL scans keyboard once per frame (~17000-20000 cycles). We need to span one scan. + # At ~1MHz: 20000 cycles = ~20ms (one full frame), 2000 cycles = ~2ms + _key_hold_cycles: int = 20000 # Hold key for one full frame, guarantees KERNAL sees it + _key_gap_cycles: int = 2000 # Gap between keys diff --git a/systems/c64/mixins/keyboard.py b/systems/c64/mixins/keyboard.py new file mode 100644 index 0000000..530bf0c --- /dev/null +++ b/systems/c64/mixins/keyboard.py @@ -0,0 +1,670 @@ +"""C64 Keyboard Mixin. + +Mixin for keyboard input handling. +""" + +from mos6502.compat import logging, Tuple + +log = logging.getLogger("c64") + + +class C64KeyboardMixin: + """Mixin for keyboard input handling.""" + + def inject_keyboard_buffer(self, text: str) -> None: + """Inject a string into the KERNAL keyboard buffer. + + This writes directly to the keyboard buffer at $0277-$0280 and sets + the buffer count at $00C6. The KERNAL will process these characters + as if they were typed. Maximum 10 characters. + + Arguments: + text: String to inject (max 10 chars, typically ending with \\r for RETURN) + """ + # Convert to PETSCII (for simple ASCII chars, it's mostly the same) + # RETURN key is 0x0D in PETSCII + petscii_text = text.replace('\r', '\x0d').replace('\n', '\x0d') + + # Limit to 10 characters (keyboard buffer size) + if len(petscii_text) > 10: + log.warning(f"Keyboard buffer overflow: truncating '{text}' to 10 chars") + petscii_text = petscii_text[:10] + + # Write characters to buffer at $0277 + for i, char in enumerate(petscii_text): + self.cpu.ram[self.KEYBOARD_BUFFER + i] = ord(char) + + # Set buffer count at $00C6 + self.cpu.ram[self.KEYBOARD_BUFFER_SIZE] = len(petscii_text) + + log.info(f"Injected '{text.strip()}' into keyboard buffer ({len(petscii_text)} chars)") + + def inject_keyboard_string(self, text: str, cycles_per_chunk: int = 100_000) -> None: + """Inject a string into the keyboard buffer, handling strings longer than 10 chars. + + For strings longer than 10 characters, this method injects them in chunks + of 10 characters each, running the CPU between chunks to allow KERNAL + to process the buffer. + + Arguments: + text: String to inject (any length, typically ending with \\r for RETURN) + cycles_per_chunk: CPU cycles to run between chunks (default 100,000) + """ + from mos6502.errors import CPUCycleExhaustionError + + # Convert to PETSCII + petscii_text = text.replace('\r', '\x0d').replace('\n', '\x0d') + + # Process in chunks of 10 characters + chunk_size = 10 + position = 0 + + while position < len(petscii_text): + # Wait for keyboard buffer to be empty + max_wait_cycles = 1_000_000 + waited = 0 + while int(self.cpu.ram[self.KEYBOARD_BUFFER_SIZE]) > 0 and waited < max_wait_cycles: + try: + self.cpu.execute(10_000) + except CPUCycleExhaustionError: + pass + waited += 10_000 + + if waited >= max_wait_cycles: + log.warning("Timeout waiting for keyboard buffer to empty") + break + + # Inject next chunk + chunk = petscii_text[position:position + chunk_size] + for i, char in enumerate(chunk): + self.cpu.ram[self.KEYBOARD_BUFFER + i] = ord(char) + self.cpu.ram[self.KEYBOARD_BUFFER_SIZE] = len(chunk) + + log.debug(f"Injected chunk {position//chunk_size + 1}: {len(chunk)} chars") + position += chunk_size + + # Run CPU to process this chunk + if position < len(petscii_text): + try: + self.cpu.execute(cycles_per_chunk) + except CPUCycleExhaustionError: + pass + + log.info(f"Injected '{text.strip()}' into keyboard buffer ({len(petscii_text)} chars total)") + + def _handle_pygame_keyboard(self, event, pygame) -> None: + """Handle pygame keyboard events and update CIA1 keyboard matrix. + + Args: + event: Pygame event + pygame: Pygame module + """ + # C64 keyboard matrix mapping: (row, col) + # Reference: https://www.c64-wiki.com/wiki/Keyboard + key_map = { + # Row 0 + pygame.K_BACKSPACE: (0, 0), # DEL/INST + pygame.K_RETURN: (0, 1), # RETURN + pygame.K_KP_ENTER: (0, 1), # RETURN (numeric keypad) + pygame.K_RIGHT: (0, 2), # CRSR → + pygame.K_F7: (0, 3), # F7 + pygame.K_F1: (0, 4), # F1 + pygame.K_F3: (0, 5), # F3 + pygame.K_F5: (0, 6), # F5 + pygame.K_DOWN: (0, 7), # CRSR ↓ + + # Row 1 + pygame.K_3: (1, 0), + pygame.K_w: (1, 1), + pygame.K_a: (1, 2), + pygame.K_4: (1, 3), + pygame.K_z: (1, 4), + pygame.K_s: (1, 5), + pygame.K_e: (1, 6), + pygame.K_LSHIFT: (1, 7), # Left SHIFT + + # Row 2 + pygame.K_5: (2, 0), + pygame.K_r: (2, 1), + pygame.K_d: (2, 2), + pygame.K_6: (2, 3), + pygame.K_c: (2, 4), + pygame.K_f: (2, 5), + pygame.K_t: (2, 6), + pygame.K_x: (2, 7), + + # Row 3 + pygame.K_7: (3, 0), + pygame.K_y: (3, 1), + pygame.K_g: (3, 2), + pygame.K_8: (3, 3), + pygame.K_b: (3, 4), + pygame.K_h: (3, 5), + pygame.K_u: (3, 6), + pygame.K_v: (3, 7), + + # Row 4 + pygame.K_9: (4, 0), + pygame.K_i: (4, 1), + pygame.K_j: (4, 2), + pygame.K_0: (4, 3), + pygame.K_m: (4, 4), + pygame.K_k: (4, 5), + pygame.K_o: (4, 6), + pygame.K_n: (4, 7), + + # Row 5 + pygame.K_PLUS: (5, 0), # + + pygame.K_p: (5, 1), + pygame.K_l: (5, 2), + pygame.K_MINUS: (5, 3), # - + pygame.K_PERIOD: (5, 4), # . + pygame.K_COLON: (5, 5), # : + pygame.K_AT: (5, 6), # @ + pygame.K_COMMA: (5, 7), # , + + # Row 6 + # pygame.K_POUND: (6, 0), # £ (pound symbol on C64, no direct US keyboard equivalent) + pygame.K_QUOTE: (7, 3), # Map to '2' key - SHIFT+2 produces " (double quote) for BASIC strings + pygame.K_ASTERISK: (6, 1), # * + pygame.K_KP_MULTIPLY: (6, 1), # * on numpad + pygame.K_SEMICOLON: (6, 2), # ; + pygame.K_HOME: (6, 3), # HOME/CLR + # pygame.K_CLR: (6, 4), # CLR (combined with HOME) + pygame.K_EQUALS: (6, 5), # = + pygame.K_UP: (6, 6), # ↑ (up arrow, mapped to up key) + pygame.K_SLASH: (6, 7), # / + # US keyboard Shift+number symbols mapped to C64 equivalents + # On US keyboard: Shift+8=*, Shift+9=(, Shift+0=) + # On C64: Shift+8=(, Shift+9=), * is separate key + pygame.K_LEFTPAREN: (3, 3), # ( -> maps to '8' key (C64: Shift+8 = '(') + pygame.K_RIGHTPAREN: (4, 0), # ) -> maps to '9' key (C64: Shift+9 = ')') + + # Row 7 + pygame.K_1: (7, 0), + pygame.K_LEFT: (7, 1), # ← (CRSR left, using arrow key) + pygame.K_LCTRL: (7, 2), # CTRL + pygame.K_2: (7, 3), + pygame.K_QUOTEDBL: (7, 3), # " (double quote) - same as '2' key (SHIFT+2 on C64) + pygame.K_SPACE: (7, 4), # SPACE + # pygame.K_COMMODORE: (7, 5), # C= (no pygame equivalent) + pygame.K_q: (7, 6), + pygame.K_ESCAPE: (7, 7), # RUN/STOP (mapped to ESC) + } + + # Keys that should be held directly (not buffered) - modifiers and control keys + # These affect the state of other keys and need real-time response + direct_keys = { + pygame.K_LSHIFT, pygame.K_RSHIFT, # SHIFT modifiers + pygame.K_LCTRL, pygame.K_RCTRL, # CTRL + pygame.K_ESCAPE, # RUN/STOP + } + + # Keyboard-to-joystick mappings when joystick emulation is enabled + # Numpad: 8=Up, 2=Down, 4=Left, 6=Right, 0=Fire, 5=Fire (alt) + # Cursor keys: Up/Down/Left/Right arrows, Right Ctrl=Fire + joystick_key_map = { + # Numpad + pygame.K_KP8: JOYSTICK_UP, + pygame.K_KP2: JOYSTICK_DOWN, + pygame.K_KP4: JOYSTICK_LEFT, + pygame.K_KP6: JOYSTICK_RIGHT, + pygame.K_KP0: JOYSTICK_FIRE, + pygame.K_KP5: JOYSTICK_FIRE, # Alt fire on numpad center + # Cursor keys (when joystick enabled, these become joystick instead of C64 cursor) + pygame.K_UP: JOYSTICK_UP, + pygame.K_DOWN: JOYSTICK_DOWN, + pygame.K_LEFT: JOYSTICK_LEFT, + pygame.K_RIGHT: JOYSTICK_RIGHT, + pygame.K_RCTRL: JOYSTICK_FIRE, # Right Ctrl = Fire + pygame.K_KP_ENTER: JOYSTICK_FIRE, # Numpad Enter = Fire + } + + if event.type == pygame.KEYDOWN: + # Log all key presses with key code and name + key_name = pygame.key.name(event.key) if hasattr(pygame.key, 'name') else str(event.key) + + # Try to get ASCII representation + try: + ascii_char = chr(event.key) if 32 <= event.key < 127 else f"<{event.key}>" + ascii_code = event.key + except (ValueError, OverflowError): + ascii_char = f"" + ascii_code = event.key + + # Handle Ctrl+V for paste from system clipboard + ctrl_held = bool(event.mod & (pygame.KMOD_LCTRL | pygame.KMOD_RCTRL)) + if ctrl_held and event.key == pygame.K_v: + try: + clipboard_text = pygame.scrap.get(pygame.SCRAP_TEXT) + if clipboard_text: + # Decode bytes to string if needed + if isinstance(clipboard_text, bytes): + clipboard_text = clipboard_text.decode('utf-8', errors='ignore') + # Remove null terminator if present + clipboard_text = clipboard_text.rstrip('\x00') + if clipboard_text: + self._paste_text(clipboard_text) + log.info(f"Pasted {len(clipboard_text)} characters from clipboard") + except Exception as e: + log.warning(f"Paste failed: {e}") + return + + # Handle joystick keys first (if joystick enabled) + if self._joystick_enabled and event.key in joystick_key_map: + direction = joystick_key_map[event.key] + self.set_joystick_direction(direction, True) + if DEBUG_KEYBOARD: + log.info(f"*** JOYSTICK KEYDOWN: key={key_name}, direction=0x{direction:02X} ***") + return # Don't process as keyboard key + + # US keyboard to C64 symbol remapping when SHIFT is held + # US keyboard: Shift+8=*, Shift+9=(, Shift+0=) + # C64 keyboard: Shift+8=(, Shift+9=), * is separate key + # Remap these to produce expected characters on C64 + shift_held = bool(event.mod & (pygame.KMOD_LSHIFT | pygame.KMOD_RSHIFT)) + remapped_key = event.key + remap_needs_shift = False + remap_suppress_shift = False + if shift_held: + if event.key == pygame.K_8: + # US Shift+8 = * → C64 * key (must suppress shift!) + remapped_key = pygame.K_ASTERISK + remap_needs_shift = False + remap_suppress_shift = True # User is holding shift, but we want unshifted * + elif event.key == pygame.K_9: + # US Shift+9 = ( → C64 Shift+8 + remapped_key = pygame.K_8 + remap_needs_shift = True + elif event.key == pygame.K_0: + # US Shift+0 = ) → C64 Shift+9 + remapped_key = pygame.K_9 + remap_needs_shift = True + + if remapped_key in key_map: + row, col = key_map[remapped_key] + + # Track physical key state + self._pygame_keys_currently_pressed.add(event.key) + + # Direct keys (modifiers) are pressed immediately for real-time feel + if event.key in direct_keys: + self.cia1.press_key(row, col) + else: + # Buffer the key for injection with proper timing + # Some keys need SHIFT to produce the expected character on C64: + # - Quote/double-quote: SHIFT+2 on C64 + # - Parentheses: SHIFT+8 for '(', SHIFT+9 for ')' on C64 + # - Remapped shifted number keys (set above) + needs_shift = (remap_needs_shift or + event.key == pygame.K_QUOTE or + event.key == pygame.K_QUOTEDBL or + event.key == pygame.K_LEFTPAREN or + event.key == pygame.K_RIGHTPAREN) + self._buffer_pygame_key(row, col, needs_shift, remap_suppress_shift) + + if DEBUG_KEYBOARD: + petscii_key = self.cia1._get_key_name(row, col) + buffered = "BUFFERED" if event.key not in direct_keys else "DIRECT" + log.info(f"*** KEYDOWN [{buffered}]: pygame='{key_name}' (code={event.key}), ASCII='{ascii_char}' (0x{ascii_code:02X}), matrix=({row},{col}), PETSCII={petscii_key}, buffer_len={len(self._pygame_key_buffer)} ***") + else: + if DEBUG_KEYBOARD: + log.info(f"*** UNMAPPED KEYDOWN: pygame='{key_name}' (code={event.key}), ASCII='{ascii_char}' ***") + + elif event.type == pygame.KEYUP: + # Handle joystick keys first (if joystick enabled) + if self._joystick_enabled and event.key in joystick_key_map: + direction = joystick_key_map[event.key] + self.set_joystick_direction(direction, False) + if DEBUG_KEYBOARD: + key_name = pygame.key.name(event.key) if hasattr(pygame.key, 'name') else str(event.key) + log.info(f"*** JOYSTICK KEYUP: key={key_name}, direction=0x{direction:02X} ***") + return # Don't process as keyboard key + + if event.key in key_map: + row, col = key_map[event.key] + + # Track physical key state + self._pygame_keys_currently_pressed.discard(event.key) + + # Direct keys are released immediately + if event.key in direct_keys: + self.cia1.release_key(row, col) + + # Buffered keys don't need explicit release - buffer handles timing + + if DEBUG_KEYBOARD: + log.info(f"*** KEYUP: pygame key {event.key}, row={row}, col={col} ***") + + def ascii_to_key_press(self, char: str) -> tuple: + """Convert ASCII character to C64 key press. + + Arguments: + char: Single ASCII character + + Returns: + Tuple of (needs_shift, row, col) or None if not mappable + """ + if char in self.ASCII_SHIFTED: + shift_row, shift_col, key_row, key_col = self.ASCII_SHIFTED[char] + return (True, key_row, key_col) + elif char in self.ASCII_TO_MATRIX: + pos = self.ASCII_TO_MATRIX[char] + if pos is not None: + return (False, pos[0], pos[1]) + return None + + def type_character(self, char: str, hold_cycles: int = 5000) -> None: + """Simulate typing a character on the C64 keyboard. + + Arguments: + char: Single ASCII character to type + hold_cycles: Number of CPU cycles to hold the key down + """ + key_info = self.ascii_to_key_press(char) + if key_info is None: + log.warning(f"Cannot type character: {repr(char)}") + return + + needs_shift, row, col = key_info + + # Press SHIFT if needed + if needs_shift: + self.cia1.press_key(1, 7) # Left SHIFT + + # Press the key + self.cia1.press_key(row, col) + + # Run CPU for some cycles to let the KERNAL process the keypress + try: + self.cpu.execute(hold_cycles) + except errors.CPUCycleExhaustionError: + pass + + # Release the key + self.cia1.release_key(row, col) + + # Release SHIFT if it was pressed + if needs_shift: + self.cia1.release_key(1, 7) + + # Run a bit more to ensure key release is processed + try: + self.cpu.execute(hold_cycles // 2) + except errors.CPUCycleExhaustionError: + pass + + def type_string(self, text: str, hold_cycles: int = 5000) -> None: + """Type a string of characters on the C64 keyboard. + + Arguments: + text: String to type + hold_cycles: Number of CPU cycles to hold each key down + """ + for char in text: + self.type_character(char, hold_cycles) + + # ------------------------------------------------------------------------- + # Mouse Input (1351 proportional mouse emulation) + # ------------------------------------------------------------------------- + # The Commodore 1351 mouse uses: + # - SID POT registers ($D419/$D41A) for position (relative motion, wraps 0-255) + # - Joystick port for buttons (active low): + # - Left button = Fire (bit 4) + # - Right button = Up (bit 0) in some implementations + # Mouse is typically plugged into Port 1 (joystick_1) + + _mouse_enabled: bool = False + _mouse_port: int = 1 # Which joystick port (1 or 2) + _mouse_sensitivity: float = 1.0 # Scale factor for mouse motion + + def _queue_key_release(self, row: int, col: int) -> bool: + """Queue a key for release after a delay (cycle-based, not wall-clock). + + Uses CPU cycles for timing so key handling works correctly at any + emulator speed (throttled or unthrottled). + + Implements debouncing: if this key already has a pending release, + the keypress is skipped entirely to prevent key repeat issues. + + Args: + row: Keyboard matrix row + col: Keyboard matrix column + + Returns: + True if key was queued, False if skipped due to debouncing + """ + # Check if this key already has a pending release (debounce) + for _, pending_row, pending_col in self._pending_key_releases: + if pending_row == row and pending_col == col: + # Key already pending - skip this press (debounce) + return False + + release_cycles = self.cpu.cycles_executed + self._key_hold_cycles + self._pending_key_releases.append((release_cycles, row, col)) + return True + + def _process_pending_key_releases(self) -> None: + """Process any pending key releases (call from main loop). + + Uses CPU cycles for timing, which scales correctly with emulator speed. + """ + if not self._pending_key_releases: + return + + current_cycles = self.cpu.cycles_executed + still_pending = [] + for release_cycles, row, col in self._pending_key_releases: + if current_cycles >= release_cycles: + self.cia1.release_key(row, col) + else: + still_pending.append((release_cycles, row, col)) + self._pending_key_releases = still_pending + + def _buffer_pygame_key(self, row: int, col: int, needs_shift: bool = False, + suppress_shift: bool = False) -> None: + """Buffer a keypress from pygame for injection into CIA. + + Keys are buffered and injected one at a time with proper timing + to ensure the KERNAL sees every keypress. + + Args: + row: C64 keyboard matrix row + col: C64 keyboard matrix column + needs_shift: If True, press SHIFT along with this key + suppress_shift: If True, release SHIFT while pressing this key + (for when user is holding shift but we want unshifted char) + """ + self._pygame_key_buffer.append((row, col, needs_shift, suppress_shift)) + + def _paste_text(self, text: str) -> None: + """Paste text by buffering keystrokes for each character. + + Converts ASCII/Unicode text to C64 key matrix positions and buffers + them for injection via the pygame key buffer system. + + Args: + text: Text to paste (ASCII characters) + """ + # ASCII character to C64 keyboard matrix mapping: (row, col, needs_shift) + # Based on the C64 keyboard matrix + ascii_to_matrix = { + # Letters (unshifted = uppercase on C64) + 'A': (1, 2, False), 'B': (3, 4, False), 'C': (2, 4, False), + 'D': (2, 2, False), 'E': (1, 6, False), 'F': (2, 5, False), + 'G': (3, 2, False), 'H': (3, 5, False), 'I': (4, 1, False), + 'J': (4, 2, False), 'K': (4, 5, False), 'L': (5, 2, False), + 'M': (4, 4, False), 'N': (4, 7, False), 'O': (4, 6, False), + 'P': (5, 1, False), 'Q': (7, 6, False), 'R': (2, 1, False), + 'S': (1, 5, False), 'T': (2, 6, False), 'U': (3, 6, False), + 'V': (3, 7, False), 'W': (1, 1, False), 'X': (2, 7, False), + 'Y': (3, 1, False), 'Z': (1, 4, False), + # Lowercase -> same as uppercase (C64 types uppercase by default) + 'a': (1, 2, False), 'b': (3, 4, False), 'c': (2, 4, False), + 'd': (2, 2, False), 'e': (1, 6, False), 'f': (2, 5, False), + 'g': (3, 2, False), 'h': (3, 5, False), 'i': (4, 1, False), + 'j': (4, 2, False), 'k': (4, 5, False), 'l': (5, 2, False), + 'm': (4, 4, False), 'n': (4, 7, False), 'o': (4, 6, False), + 'p': (5, 1, False), 'q': (7, 6, False), 'r': (2, 1, False), + 's': (1, 5, False), 't': (2, 6, False), 'u': (3, 6, False), + 'v': (3, 7, False), 'w': (1, 1, False), 'x': (2, 7, False), + 'y': (3, 1, False), 'z': (1, 4, False), + # Numbers + '1': (7, 0, False), '2': (7, 3, False), '3': (1, 0, False), + '4': (1, 3, False), '5': (2, 0, False), '6': (2, 3, False), + '7': (3, 0, False), '8': (3, 3, False), '9': (4, 0, False), + '0': (4, 3, False), + # Symbols (unshifted) + ' ': (7, 4, False), # SPACE + '\r': (0, 1, False), # RETURN + '\n': (0, 1, False), # RETURN (newline) + ',': (5, 7, False), # COMMA + '.': (5, 4, False), # PERIOD + ':': (5, 5, False), # COLON + ';': (6, 2, False), # SEMICOLON + '/': (6, 7, False), # SLASH + '=': (6, 5, False), # EQUALS + '+': (5, 0, False), # PLUS + '-': (5, 3, False), # MINUS + '*': (6, 1, False), # ASTERISK + '@': (5, 6, False), # AT + # Shifted symbols + '!': (7, 0, True), # SHIFT+1 + '"': (7, 3, True), # SHIFT+2 (quote) + '#': (1, 0, True), # SHIFT+3 + '$': (1, 3, True), # SHIFT+4 + '%': (2, 0, True), # SHIFT+5 + '&': (2, 3, True), # SHIFT+6 + "'": (3, 0, True), # SHIFT+7 (apostrophe) + '(': (3, 3, True), # SHIFT+8 + ')': (4, 0, True), # SHIFT+9 + '<': (5, 7, True), # SHIFT+COMMA + '>': (5, 4, True), # SHIFT+PERIOD + '?': (6, 7, True), # SHIFT+SLASH + } + + for char in text: + if char in ascii_to_matrix: + row, col, needs_shift = ascii_to_matrix[char] + self._buffer_pygame_key(row, col, needs_shift, False) + else: + # Skip unmapped characters + log.debug(f"Paste: skipping unmapped character '{char}' (0x{ord(char):02X})") + + def _process_pygame_key_buffer(self) -> None: + """Process the pygame key buffer, injecting keys into CIA. + + Called from main loop. Handles timing for key injection using CPU cycles + so keys inject faster when emulator runs faster than real-time. + - Press key, hold for ~20000 cycles (~20ms at 1MHz) + - Release key, wait ~5000 cycles gap + - Repeat for next key + """ + current_cycles = self.cpu.cycles_executed + + # If we have a current injection in progress, check timing + if self._pygame_current_injection is not None: + row, col, needs_shift, suppress_shift, start_cycles, released = self._pygame_current_injection + + if not released: + # Key is being held - check if hold cycles elapsed + if current_cycles - start_cycles >= self._key_hold_cycles: + # Release the key + self.cia1.release_key(row, col) + if needs_shift: + self.cia1.release_key(1, 7) # Release SHIFT + if suppress_shift: + # Re-press shift if user is still holding it physically + import pygame + if pygame.K_LSHIFT in self._pygame_keys_currently_pressed or \ + pygame.K_RSHIFT in self._pygame_keys_currently_pressed: + self.cia1.press_key(1, 7) + # Mark as released, record release cycle + self._pygame_current_injection = (row, col, needs_shift, suppress_shift, current_cycles, True) + else: + # Key is released - check if gap cycles elapsed + if current_cycles - start_cycles >= self._key_gap_cycles: + # Done with this key, clear injection + self._pygame_current_injection = None + + # If no current injection and buffer has keys, start next one + if self._pygame_current_injection is None and self._pygame_key_buffer: + row, col, needs_shift, suppress_shift = self._pygame_key_buffer.pop(0) + # Press the key + if needs_shift: + self.cia1.press_key(1, 7) # Press SHIFT + if suppress_shift: + self.cia1.release_key(1, 7) # Release SHIFT even if physically held + self.cia1.press_key(row, col) + # Record injection start (not released yet) + self._pygame_current_injection = (row, col, needs_shift, suppress_shift, current_cycles, False) + + def _handle_terminal_input(self, char: str) -> bool: + """Handle a single character of terminal input. + + Converts ASCII input to C64 key presses. Handles escape sequences + for arrow keys and other special keys. Uses non-blocking key release + queue to avoid stalling the main loop. + + Arguments: + char: Single character from terminal input + + Returns: + True if Ctrl+C was pressed (should exit), False otherwise + """ + import sys as _sys + + # Handle special keys + if char == '\x03': # Ctrl+C + return True + elif char == '\x1b': # Escape sequence + # Read the rest of the escape sequence + try: + import select + if select.select([_sys.stdin], [], [], 0.1)[0]: + seq = _sys.stdin.read(2) + if seq == '[A': # Up arrow -> CRSR UP (SHIFT + CRSR DOWN) + self.cia1.press_key(1, 7) # SHIFT + self.cia1.press_key(0, 7) # CRSR DOWN + self._queue_key_release(0, 7) + self._queue_key_release(1, 7) + elif seq == '[B': # Down arrow -> CRSR DOWN + self.cia1.press_key(0, 7) + self._queue_key_release(0, 7) + elif seq == '[C': # Right arrow -> CRSR RIGHT + self.cia1.press_key(0, 2) + self._queue_key_release(0, 2) + elif seq == '[D': # Left arrow -> CRSR LEFT (SHIFT + CRSR RIGHT) + self.cia1.press_key(1, 7) # SHIFT + self.cia1.press_key(0, 2) # CRSR RIGHT + self._queue_key_release(0, 2) + self._queue_key_release(1, 7) + elif seq == '[3': # Delete key (followed by ~) + if select.select([_sys.stdin], [], [], 0.1)[0]: + _sys.stdin.read(1) # Consume the ~ + self.cia1.press_key(0, 0) # DEL + self._queue_key_release(0, 0) + except ImportError: + pass # select not available + elif char == '\x7f': # Backspace + # Check if key is already pending (debounce) + already_pending = any(r == 0 and c == 0 for _, r, c in self._pending_key_releases) + if not already_pending: + self.cia1.press_key(0, 0) # DEL + self._queue_key_release(0, 0) + else: + # Regular character - press key and queue release + key_info = self.ascii_to_key_press(char) + if key_info: + needs_shift, row, col = key_info + # Check if key is already pending (debounce) + already_pending = any(r == row and c == col for _, r, c in self._pending_key_releases) + if not already_pending: + if needs_shift: + self.cia1.press_key(1, 7) # SHIFT + self.cia1.press_key(row, col) + self._queue_key_release(row, col) + if needs_shift: + self._queue_key_release(1, 7) + + return False diff --git a/systems/c64/mixins/program.py b/systems/c64/mixins/program.py new file mode 100644 index 0000000..b691dee --- /dev/null +++ b/systems/c64/mixins/program.py @@ -0,0 +1,91 @@ +"""C64 Program Mixin. + +Mixin for program loading. +""" + +from mos6502.compat import logging, Path, Optional, Tuple +from c64.memory import BASIC_PROGRAM_START + +log = logging.getLogger("c64") + + +class C64ProgramMixin: + """Mixin for program loading.""" + + def load_program( + self, program_path: Path, load_address: Optional[int] = None + ) -> Tuple[int, int]: + """Load a program into memory. + + Arguments: + program_path: Path to the program file + load_address: Address to load program at (default: $0801 for BASIC programs) + If None and file has a 2-byte header, use header address + + Returns: + Tuple of (load_address, end_address) - the address range used + """ + program_path = Path(program_path) + + if not program_path.exists(): + raise FileNotFoundError(f"Program not found: {program_path}") + + program_data = program_path.read_bytes() + + # Check if program has a load address header (common for .prg files) + if load_address is None and len(program_data) >= 2: + # First two bytes might be load address (little-endian) + header_addr = program_data[0] | (program_data[1] << 8) + + # If it looks like a reasonable address, use it + if 0x0000 <= header_addr <= 0xFFFF: + load_address = header_addr + program_data = program_data[2:] # Skip header + log.info(f"Using load address from file header: ${load_address:04X}") + + # Default to BASIC program start + if load_address is None: + load_address = BASIC_PROGRAM_START + + # Write program to memory + for offset, byte_value in enumerate(program_data): + self.cpu.ram[load_address + offset] = byte_value + + end_address = load_address + len(program_data) + + log.info( + f"Loaded program: {program_path.name} " + f"at ${load_address:04X}-${end_address - 1:04X} " + f"({len(program_data)} bytes)" + ) + + return load_address, end_address + + def update_basic_pointers(self, program_end: int) -> None: + """Update BASIC memory pointers after loading a program. + + When loading a BASIC program at $0801, BASIC's internal pointers + must be updated so that RUN knows where the program ends. + + Arguments: + program_end: Address immediately after the last byte of the program + """ + # VARTAB, ARYTAB, and STREND should all point to the end of the program + # (They get properly set up when BASIC parses the program, but for + # directly loaded programs we need to set them manually) + lo = program_end & 0xFF + hi = (program_end >> 8) & 0xFF + + # Set VARTAB (start of variables = end of program) + self.cpu.ram[self.VARTAB] = lo + self.cpu.ram[self.VARTAB + 1] = hi + + # Set ARYTAB (start of arrays = end of variables) + self.cpu.ram[self.ARYTAB] = lo + self.cpu.ram[self.ARYTAB + 1] = hi + + # Set STREND (end of arrays = bottom of strings) + self.cpu.ram[self.STREND] = lo + self.cpu.ram[self.STREND + 1] = hi + + log.info(f"Updated BASIC pointers: VARTAB/ARYTAB/STREND = ${program_end:04X}") diff --git a/systems/c64/mixins/runner.py b/systems/c64/mixins/runner.py new file mode 100644 index 0000000..ae86018 --- /dev/null +++ b/systems/c64/mixins/runner.py @@ -0,0 +1,448 @@ +"""C64 Runner Mixin. + +Mixin for execution (run, run_repl). +""" + +from mos6502.compat import logging +from mos6502.core import INFINITE_CYCLES +from mos6502.memory import Byte, Word +from c64.memory import ( + BASIC_ROM_START, + BASIC_ROM_END, + KERNAL_ROM_START, + KERNAL_ROM_END, +) + +log = logging.getLogger("c64") + + +class C64RunnerMixin: + """Mixin for execution (run, run_repl).""" + + def basic_ready(self) -> bool: + """Return True if BASIC input loop has been reached.""" + return self._basic_ready + + @property + + def kernal_waiting_for_input(self) -> bool: + """Return True if KERNAL is waiting for keyboard input. + + The KERNAL keyboard input loop is at $E5CF-$E5D6. + When PC is in this range, the system is waiting for user input. + """ + return self._kernal_waiting_for_input + + def _pc_callback(self, new_pc: int) -> None: + """PC change callback - detects when BASIC is ready or KERNAL is waiting for input. + + This is called by the CPU every time PC changes. + """ + # Check if PC is in BASIC ROM range + if BASIC_ROM_START <= new_pc <= BASIC_ROM_END: + self._basic_ready = True + if self._stop_on_basic: + raise StopIteration("BASIC is ready") + + # Check if PC is in KERNAL keyboard input loop ($E5CF-$E5D6) + # This is the GETIN routine that waits for keyboard input + if 0xE5CF <= new_pc <= 0xE5D6: + self._kernal_waiting_for_input = True + if self._stop_on_kernal_input: + raise StopIteration("KERNAL waiting for input") + else: + # Reset when PC leaves the input loop + self._kernal_waiting_for_input = False + + def _setup_pc_callback( + self, stop_on_basic: bool = False, stop_on_kernal_input: bool = False + ) -> None: + """Set up the PC callback for BASIC/KERNAL detection. + + Arguments: + stop_on_basic: If True, raise StopIteration when BASIC is ready + stop_on_kernal_input: If True, raise StopIteration when KERNAL is waiting for input + """ + self._stop_on_basic = stop_on_basic + self._stop_on_kernal_input = stop_on_kernal_input + self.cpu.pc_callback = self._pc_callback + + def _clear_pc_callback(self) -> None: + """Remove the PC callback and reset detection flags.""" + self.cpu.pc_callback = None + self._stop_on_basic = False + self._stop_on_kernal_input = False + + def _check_pc_region(self) -> None: + """Monitor PC and enable detailed logging when entering BASIC or KERNAL ROM.""" + region = self.get_pc_region() + + # Track important PC locations + pc = self.cpu.PC + + # Log when we enter BASIC ROM + if BASIC_ROM_START <= pc <= BASIC_ROM_END and not hasattr(self, '_logged_basic_entry'): + self._logged_basic_entry = True + self._basic_ready = True + log.info(f"*** ENTERED BASIC ROM at ${pc:04X} ***") + + # Log when stuck at KERNAL idle loop ($E5CF-$E5D2) + if 0xE5CF <= pc <= 0xE5D2: + if not hasattr(self, '_e5cf_count'): + self._e5cf_count = 0 + self._e5cf_count += 1 + if self._e5cf_count % 10000 == 0: + log.info(f"*** Still in KERNAL idle loop at ${pc:04X} (count={self._e5cf_count}) ***") + + # Enable logging when entering KERNAL for the first time (for debugging) + if DEBUG_KERNAL and region == "KERNAL" and self.last_pc_region != "KERNAL": + logging.getLogger("mos6502").setLevel(logging.DEBUG) + logging.getLogger("mos6502.cpu").setLevel(logging.DEBUG) + logging.getLogger("mos6502.cpu.flags").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory.RAM").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory.Byte").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory.Word").setLevel(logging.DEBUG) + log.info(f"*** ENTERING KERNAL ROM at ${self.cpu.PC:04X} - Enabling detailed CPU logging ***") + + # Enable logging when entering BASIC for the first time + if DEBUG_BASIC and region == "BASIC" and not self.basic_logging_enabled: + self.basic_logging_enabled = True + logging.getLogger("mos6502").setLevel(logging.DEBUG) + logging.getLogger("mos6502.cpu").setLevel(logging.DEBUG) + logging.getLogger("mos6502.cpu.flags").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory.RAM").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory.Byte").setLevel(logging.DEBUG) + logging.getLogger("mos6502.memory.Word").setLevel(logging.DEBUG) + log.info(f"*** ENTERING BASIC ROM at ${self.cpu.PC:04X} - Enabling detailed CPU logging ***") + + self.last_pc_region = region + + def run( + self, + max_cycles: int = INFINITE_CYCLES, + stop_on_basic: bool = False, + stop_on_kernal_input: bool = False, + throttle: bool = True, + stop_on_illegal_instruction: bool = False, + ) -> None: + """Run the C64 emulator. + + Arguments: + max_cycles: Maximum number of CPU cycles to execute + (default: INFINITE_CYCLES for continuous execution) + stop_on_basic: If True, stop execution when BASIC prompt is ready + stop_on_kernal_input: If True, stop execution when KERNAL is waiting for keyboard input + throttle: If True, throttle emulation to real-time speed (default: True) + Use --no-throttle for benchmarks to run at maximum speed + stop_on_illegal_instruction: If True, dump crash report on illegal instruction + """ + # Store for use in error handler + self._stop_on_illegal_instruction = stop_on_illegal_instruction + log.info(f"Starting execution at PC=${self.cpu.PC:04X}") + log.info("Press Ctrl+C to stop") + + # Set up PC callback for BASIC/KERNAL detection if requested + if stop_on_basic or stop_on_kernal_input: + # Use positional args - kwargs don't work in MicroPython frozen modules + self._setup_pc_callback(stop_on_basic, stop_on_kernal_input) + + try: + # Execute with cycle counter display + # Use threading for concurrent execution - multiprocessing has pickling issues + # with closures and the GIL doesn't affect us since we're I/O bound on display + # The frame_complete uses multiprocessing.Event for cross-process safety + import threading + import time + import sys as _sys + from mos6502.timing import FrameGovernor + + # Record execution start time for speedup calculation + self._execution_start_time = time.perf_counter() + + # Check if we're in a TTY (terminal) for interactive display + is_tty = _sys.stdout.isatty() + + # All modes: CPU in background thread, display + input in main thread + # This ensures responsive input handling regardless of display mode + pygame_mode = self.display_mode == "pygame" and self.pygame_available + + cpu_done = threading.Event() + cpu_error = None + stop_cpu = threading.Event() + + # Create frame governor for real-time throttling + # Use positional args - kwargs don't work in MicroPython frozen modules + governor = FrameGovernor(self.video_timing.refresh_hz, throttle) + cycles_per_frame = self.video_timing.cycles_per_frame + + def cpu_thread() -> None: + nonlocal cpu_error + try: + # Execute frame-by-frame with optional throttling + # This allows the governor to maintain real-time speed + cycles_remaining = max_cycles + while cycles_remaining > 0 and not stop_cpu.is_set(): + # Execute one frame's worth of cycles + cycles_this_frame = min(cycles_per_frame, cycles_remaining) + try: + self.cpu.execute(cycles_this_frame) + except errors.CPUCycleExhaustionError: + pass # Normal - frame completed + cycles_remaining -= cycles_this_frame + + # Throttle to real-time (governor.throttle() returns + # immediately if throttling is disabled) + governor.throttle() + except Exception as e: + cpu_error = e + finally: + cpu_done.set() + + # Start CPU thread + cpu_thread_obj = threading.Thread(target=cpu_thread, daemon=True) + cpu_thread_obj.start() + + # Set up terminal input (works for all modes) + terminal_input_available = False + old_settings = None + try: + import select + import termios + import tty + if _sys.stdin.isatty(): + old_settings = termios.tcgetattr(_sys.stdin) + tty.setcbreak(_sys.stdin.fileno()) + terminal_input_available = True + except ImportError: + pass # Terminal input not available (Windows, etc.) + + # Main thread handles display + input + try: + last_terminal_render = time.perf_counter() + TERMINAL_RENDER_INTERVAL = 0.1 # 100ms between terminal renders + + while not cpu_done.is_set(): + # Check stop conditions + if stop_on_basic and self.basic_ready: + break + if stop_on_kernal_input and self.kernal_waiting_for_input: + break + + # Handle terminal keyboard input (all modes) + if terminal_input_available: + import select + if select.select([_sys.stdin], [], [], 0)[0]: + char = _sys.stdin.read(1) + if self._handle_terminal_input(char): + break # Ctrl+C pressed + + # Process any pending key releases (non-blocking) + self._process_pending_key_releases() + + # Pump pygame events every iteration (outside of draw loop) + if pygame_mode: + self._pump_pygame_events() + self._process_pygame_key_buffer() + + # Mode-specific rendering + # Use half frame time as timeout - ensures we check twice per frame + # even if CPU is slow, while not wasting CPU on excessive polling + frame_timeout = 0.5 / self.video_timing.refresh_hz # ~10ms PAL, ~8ms NTSC + + if pygame_mode: + # Render when VIC has a new frame ready (both pygame and terminal) + if self.vic.frame_complete.is_set(): + self._render_pygame() + else: + # Tiny sleep to prevent busy-spinning when no frame ready + time.sleep(0.001) # 1ms + else: + # Terminal/headless: render when VIC has a new frame ready + if self.vic.frame_complete.is_set(): + self.vic.frame_complete.clear() + self._check_pc_region() + self._render_terminal() + else: + time.sleep(0.001) # 1ms + + finally: + # Signal CPU thread to stop and wait for it + stop_cpu.set() + cpu_done.set() + cpu_thread_obj.join(timeout=0.5) + + # Stop drive thread if running in threaded mode + if self.drive_enabled and getattr(self, 'drive_threaded', False): + if self.drive8 is not None and hasattr(self.drive8, 'stop_thread'): + self.drive8.stop_thread() + + # Restore terminal settings if we changed them + if old_settings is not None: + import termios + termios.tcsetattr(_sys.stdin, termios.TCSADRAIN, old_settings) + + # Cleanup pygame if used + if pygame_mode: + try: + import pygame + pygame.quit() + except Exception: + pass + + # Log governor stats if throttling was enabled + if throttle: + stats = governor.stats() + log.info(f"Governor stats: {stats['frame_count']} frames, " + f"avg sleep {stats['avg_sleep_per_frame']*1000:.1f}ms/frame, " + f"dropped {stats['frames_dropped']}") + + # Re-raise CPU thread exception if any + if cpu_error: + raise cpu_error + + except StopIteration as e: + # PC callback requested stop (e.g., BASIC is ready or KERNAL waiting for input) + log.info(f"Execution stopped at PC=${self.cpu.PC:04X} ({e})") + except errors.CPUCycleExhaustionError as e: + log.info(f"CPU execution completed: {e}") + except errors.CPUBreakError as e: + log.info(f"Program terminated (BRK at PC=${self.cpu.PC:04X})") + except (KeyboardInterrupt, errors.QuitRequestError) as e: + if isinstance(e, errors.QuitRequestError): + log.info(f"\nExecution stopped: {e}") + else: + log.info("\nExecution interrupted by user") + log.info(f"PC=${self.cpu.PC:04X}, Cycles={self.cpu.cycles_executed}") + except (errors.IllegalCPUInstructionError, RuntimeError) as e: + # Check if we should dump crash report and stop (vs raising) + if getattr(self, '_stop_on_illegal_instruction', False) and isinstance(e, errors.IllegalCPUInstructionError): + self.dump_crash_report(e) + # Don't re-raise - just stop execution cleanly + else: + log.exception(f"Execution error at PC=${self.cpu.PC:04X}") + # Show context around error + try: + pc_val = int(self.cpu.PC) + self.show_disassembly(max(0, pc_val - 10), 20) + self.dump_memory(max(0, pc_val - 16), min(0xFFFF, pc_val + 16)) + except (IndexError, KeyError, ValueError): + log.exception("Could not display context") + raise + finally: + # Record execution end time for speedup calculation + import time + self._execution_end_time = time.perf_counter() + # Clean up PC callback + self._clear_pc_callback() + # Show screen buffer on termination + self.show_screen() + # Clean up drive subprocess if running + self.cleanup() + + def run_repl(self, max_cycles: int = INFINITE_CYCLES) -> None: + """Run the C64 in REPL mode with terminal input. + + This mode renders the C64 screen to the terminal and accepts + keyboard input, converting ASCII to C64 key presses. + + Note: REPL mode requires Unix-like terminal support (termios, tty). + It is not available on Windows. + + Arguments: + max_cycles: Maximum cycles to run (default: infinite) + """ + import sys as _sys + import threading + import time + + try: + import select + import termios + import tty + except ImportError: + log.error("REPL mode requires Unix-like terminal support (termios, tty).") + log.error("This mode is not available on Windows. Use --display terminal instead.") + return + + if not _sys.stdin.isatty(): + log.error("REPL mode requires an interactive terminal (stdin must be a TTY).") + return + + # Save terminal settings + old_settings = termios.tcgetattr(_sys.stdin) + + # Shared state between threads + stop_event = threading.Event() + cpu_error = None + + def cpu_thread(): + """Run CPU in background thread.""" + nonlocal cpu_error + try: + self.cpu.execute(max_cycles) + except errors.CPUCycleExhaustionError: + pass # Normal termination when max_cycles reached + except Exception as e: + cpu_error = e + log.error(f"CPU thread error: {e}") + finally: + stop_event.set() + + try: + # Put terminal in cbreak mode (character-at-a-time, no echo) + tty.setcbreak(_sys.stdin.fileno()) + + # Record execution start time for speedup calculation + self._execution_start_time = time.perf_counter() + + # Start CPU in background thread + cpu_thread_obj = threading.Thread(target=cpu_thread, daemon=True) + cpu_thread_obj.start() + + # Main loop: handle input and render + # Limit render rate to match video timing (PAL ~50Hz, NTSC ~60Hz) + last_render = 0 + render_interval = self.video_timing.render_interval + + while not stop_event.is_set(): + # Check for input (non-blocking with short timeout) + if select.select([_sys.stdin], [], [], 0.01)[0]: + char = _sys.stdin.read(1) + if self._handle_terminal_input(char): + break # Ctrl+C pressed + + # Process any pending key releases + self._process_pending_key_releases() + + # Render at limited rate to avoid terminal buffer overflow + now = time.time() + if now - last_render >= render_interval: + self.dirty_tracker.force_redraw() + self._render_terminal_repl() + last_render = now + + except (KeyboardInterrupt, errors.QuitRequestError): + pass + finally: + # Record execution end time for speedup calculation + self._execution_end_time = time.perf_counter() + + stop_event.set() + # Wait for CPU thread to finish + cpu_thread_obj.join(timeout=0.5) + + # Restore terminal settings + termios.tcsetattr(_sys.stdin, termios.TCSADRAIN, old_settings) + + # Clear screen and show final state + _sys.stdout.write("\033[2J\033[H") + self.show_screen() + + # Clean up drive subprocess if running + self.cleanup() + + # Re-raise CPU error if any + if cpu_error: + raise cpu_error diff --git a/systems/c64/sid.py b/systems/c64/sid.py index 0ff1b25..8d837d0 100644 --- a/systems/c64/sid.py +++ b/systems/c64/sid.py @@ -23,9 +23,8 @@ value written to ANY SID register (due to data bus capacitance). """ -from __future__ import annotations -import logging +from mos6502.compat import logging class SID: diff --git a/systems/c64/vic.py b/systems/c64/vic.py index c9be34d..64f53d2 100644 --- a/systems/c64/vic.py +++ b/systems/c64/vic.py @@ -8,11 +8,36 @@ - Color constants and ANSI conversion utilities """ -from __future__ import annotations -import logging -import multiprocessing -from typing import TYPE_CHECKING, NamedTuple +from mos6502.compat import logging +# Multiprocessing is optional - only needed for cross-process frame sync +try: + import multiprocessing + _multiprocessing_available = True +except ImportError: + _multiprocessing_available = False + +# Simple Event class for when multiprocessing is not available +class _SimpleEvent: + """Simple Event replacement when multiprocessing is not available.""" + def __init__(self): + self._flag = False + def set(self): + self._flag = True + def clear(self): + self._flag = False + def is_set(self): + return self._flag + def wait(self, timeout=None): + return self._flag + +def _create_event(): + """Create an Event - uses multiprocessing if available, otherwise simple.""" + if _multiprocessing_available: + return multiprocessing.Event() + return _SimpleEvent() + +from mos6502.compat import TYPE_CHECKING, NamedTuple if TYPE_CHECKING: from c64.cia2 import CIA2 @@ -25,33 +50,48 @@ # Video Timing Constants # ============================================================================= -class VideoTiming(NamedTuple): - """Video system timing parameters for VIC-II chip variants. - - VIC-II Chip Variants: - 6569 (PAL) - Europe, Australia (312 lines, 63 cycles/line, ~50Hz) - 6567R8 (NTSC) - North America 1984+ (263 lines, 65 cycles/line, ~60Hz) - 6567R56A (NTSC) - North America 1982-1984 (262 lines, 64 cycles/line, ~60Hz) - - Access standard timing configurations via class attributes: - VideoTiming.VIC_6569 - PAL chip - VideoTiming.VIC_6567R8 - New NTSC chip (default for NTSC) - VideoTiming.VIC_6567R56A - Old NTSC chip - VideoTiming.PAL - Alias for VIC_6569 - VideoTiming.NTSC - Alias for VIC_6567R8 - """ - chip_name: str # VIC-II chip identifier (6569, 6567R8, 6567R56A) - cpu_freq: int # CPU frequency in Hz - refresh_hz: float # Screen refresh rate in Hz - cycles_per_frame: int # CPU cycles per video frame - cycles_per_line: int # CPU cycles per raster line - raster_lines: int # Total raster lines per frame - render_interval: float # Seconds between frame renders (1/refresh_hz) +# VideoTiming - try to use NamedTuple, fall back to collections.namedtuple for MicroPython +try: + # CPython - use class syntax with NamedTuple + class VideoTiming(NamedTuple): + """Video system timing parameters for VIC-II chip variants. + + VIC-II Chip Variants: + 6569 (PAL) - Europe, Australia (312 lines, 63 cycles/line, ~50Hz) + 6567R8 (NTSC) - North America 1984+ (263 lines, 65 cycles/line, ~60Hz) + 6567R56A (NTSC) - North America 1982-1984 (262 lines, 64 cycles/line, ~60Hz) + + Access standard timing configurations via class attributes: + VideoTiming.VIC_6569 - PAL chip + VideoTiming.VIC_6567R8 - New NTSC chip (default for NTSC) + VideoTiming.VIC_6567R56A - Old NTSC chip + VideoTiming.PAL - Alias for VIC_6569 + VideoTiming.NTSC - Alias for VIC_6567R8 + """ + chip_name: str # VIC-II chip identifier (6569, 6567R8, 6567R56A) + cpu_freq: int # CPU frequency in Hz + refresh_hz: float # Screen refresh rate in Hz + cycles_per_frame: int # CPU cycles per video frame + cycles_per_line: int # CPU cycles per raster line + raster_lines: int # Total raster lines per frame + render_interval: float # Seconds between frame renders (1/refresh_hz) + # Test that it works (will fail on MicroPython) + _test = VideoTiming(chip_name="test", cpu_freq=0, refresh_hz=0.0, + cycles_per_frame=0, cycles_per_line=0, raster_lines=0, + render_interval=0.0) + del _test +except (TypeError, AttributeError): + # MicroPython - use collections.namedtuple + from collections import namedtuple + VideoTiming = namedtuple('VideoTiming', [ + 'chip_name', 'cpu_freq', 'refresh_hz', 'cycles_per_frame', + 'cycles_per_line', 'raster_lines', 'render_interval' + ]) # VIC-II 6569 (PAL) - Europe, Australia # More compatible with demos/games, slightly slower CPU -VideoTiming.VIC_6569 = VideoTiming( +VIC_6569 = VideoTiming( chip_name="6569", cpu_freq=985248, # ~0.985 MHz refresh_hz=50.125, # ~50 Hz @@ -63,7 +103,7 @@ class VideoTiming(NamedTuple): # VIC-II 6567R8 (NTSC) - North America, Japan (1984 onwards) # "New" NTSC chip, most common in later C64s -VideoTiming.VIC_6567R8 = VideoTiming( +VIC_6567R8 = VideoTiming( chip_name="6567R8", cpu_freq=1022727, # ~1.023 MHz refresh_hz=59.826, # ~60 Hz @@ -75,7 +115,7 @@ class VideoTiming(NamedTuple): # VIC-II 6567R56A (NTSC) - North America (1982-1984) # "Old" NTSC chip, found in early C64s -VideoTiming.VIC_6567R56A = VideoTiming( +VIC_6567R56A = VideoTiming( chip_name="6567R56A", cpu_freq=1022727, # ~1.023 MHz refresh_hz=59.826, # ~60 Hz (slightly different in reality) @@ -85,13 +125,19 @@ class VideoTiming(NamedTuple): render_interval=1.0 / 59.826, ) -# Backwards compatibility aliases -VideoTiming.PAL = VideoTiming.VIC_6569 -VideoTiming.NTSC = VideoTiming.VIC_6567R8 # Default to new NTSC chip +# Module-level aliases +PAL = VIC_6569 +NTSC = VIC_6567R8 # Default to new NTSC chip -# Module-level constants for backwards compatibility -PAL = VideoTiming.PAL -NTSC = VideoTiming.NTSC +# Try to set class attributes for backwards compatibility (may fail on MicroPython) +try: + VideoTiming.VIC_6569 = VIC_6569 + VideoTiming.VIC_6567R8 = VIC_6567R8 + VideoTiming.VIC_6567R56A = VIC_6567R56A + VideoTiming.PAL = PAL + VideoTiming.NTSC = NTSC +except (TypeError, AttributeError): + pass # MicroPython doesn't allow setting attributes on namedtuple # ============================================================================= @@ -258,7 +304,7 @@ class C64VIC: - Light pen registers """ - def __init__(self, char_rom, cpu: MOS6502CPU, cia2: CIA2 = None, video_timing: VideoTiming = None) -> None: + def __init__(self, char_rom, cpu: "MOS6502CPU", cia2: "CIA2" = None, video_timing: VideoTiming = None) -> None: self.log = logging.getLogger("c64.vic") self.regs = [0] * 0x40 self.char_rom = char_rom @@ -384,8 +430,8 @@ def __init__(self, char_rom, cpu: MOS6502CPU, cia2: CIA2 = None, video_timing: V # VIC sets this when a frame completes (raster wraps to 0) # Pygame checks it, grabs a snapshot, and clears it # No blocking - VIC runs at full speed, pygame renders what it catches - # Using multiprocessing.Event for proper cross-process visibility - self.frame_complete = multiprocessing.Event() + # Using Event for frame sync (multiprocessing.Event if available) + self.frame_complete = _create_event() # RAM snapshots taken at VBlank for consistent rendering # Only the 16KB VIC bank is snapshotted (not full 64KB) for performance @@ -407,7 +453,7 @@ def __init__(self, char_rom, cpu: MOS6502CPU, cia2: CIA2 = None, video_timing: V self.cycles_per_frame, ) - def set_cia2(self, cia2: CIA2) -> None: + def set_cia2(self, cia2: "CIA2") -> None: """Set reference to CIA2 for VIC bank selection.""" self.cia2 = cia2 diff --git a/systems/destest-full.rom b/systems/destest-full.rom new file mode 100644 index 0000000..d23ca27 Binary files /dev/null and b/systems/destest-full.rom differ diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_01_action_replay.bin b/systems/ec.bak/error_cart_type_01_action_replay.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_01_action_replay.bin rename to systems/ec.bak/error_cart_type_01_action_replay.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_02_kcs_power_cartridge.bin b/systems/ec.bak/error_cart_type_02_kcs_power_cartridge.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_02_kcs_power_cartridge.bin rename to systems/ec.bak/error_cart_type_02_kcs_power_cartridge.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_03_final_cartridge_iii.bin b/systems/ec.bak/error_cart_type_03_final_cartridge_iii.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_03_final_cartridge_iii.bin rename to systems/ec.bak/error_cart_type_03_final_cartridge_iii.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_04_simons_basic.bin b/systems/ec.bak/error_cart_type_04_simons_basic.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_04_simons_basic.bin rename to systems/ec.bak/error_cart_type_04_simons_basic.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_05_ocean_type_1.bin b/systems/ec.bak/error_cart_type_05_ocean_type_1.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_05_ocean_type_1.bin rename to systems/ec.bak/error_cart_type_05_ocean_type_1.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_06_expert_cartridge.bin b/systems/ec.bak/error_cart_type_06_expert_cartridge.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_06_expert_cartridge.bin rename to systems/ec.bak/error_cart_type_06_expert_cartridge.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_07_fun_play_power_play.bin b/systems/ec.bak/error_cart_type_07_fun_play_power_play.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_07_fun_play_power_play.bin rename to systems/ec.bak/error_cart_type_07_fun_play_power_play.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_08_super_games.bin b/systems/ec.bak/error_cart_type_08_super_games.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_08_super_games.bin rename to systems/ec.bak/error_cart_type_08_super_games.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_09_atomic_power.bin b/systems/ec.bak/error_cart_type_09_atomic_power.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_09_atomic_power.bin rename to systems/ec.bak/error_cart_type_09_atomic_power.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_10_epyx_fastload.bin b/systems/ec.bak/error_cart_type_10_epyx_fastload.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_10_epyx_fastload.bin rename to systems/ec.bak/error_cart_type_10_epyx_fastload.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_11_westermann_learning.bin b/systems/ec.bak/error_cart_type_11_westermann_learning.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_11_westermann_learning.bin rename to systems/ec.bak/error_cart_type_11_westermann_learning.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_12_rex_utility.bin b/systems/ec.bak/error_cart_type_12_rex_utility.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_12_rex_utility.bin rename to systems/ec.bak/error_cart_type_12_rex_utility.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_13_final_cartridge_i.bin b/systems/ec.bak/error_cart_type_13_final_cartridge_i.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_13_final_cartridge_i.bin rename to systems/ec.bak/error_cart_type_13_final_cartridge_i.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_14_magic_formel.bin b/systems/ec.bak/error_cart_type_14_magic_formel.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_14_magic_formel.bin rename to systems/ec.bak/error_cart_type_14_magic_formel.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_15_c64_game_system_system_3.bin b/systems/ec.bak/error_cart_type_15_c64_game_system_system_3.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_15_c64_game_system_system_3.bin rename to systems/ec.bak/error_cart_type_15_c64_game_system_system_3.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_16_warpspeed.bin b/systems/ec.bak/error_cart_type_16_warpspeed.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_16_warpspeed.bin rename to systems/ec.bak/error_cart_type_16_warpspeed.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_17_dinamic.bin b/systems/ec.bak/error_cart_type_17_dinamic.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_17_dinamic.bin rename to systems/ec.bak/error_cart_type_17_dinamic.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_18_zaxxon_super_zaxxon_sega.bin b/systems/ec.bak/error_cart_type_18_zaxxon_super_zaxxon_sega.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_18_zaxxon_super_zaxxon_sega.bin rename to systems/ec.bak/error_cart_type_18_zaxxon_super_zaxxon_sega.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_19_magic_desk_domark_hes_australia.bin b/systems/ec.bak/error_cart_type_19_magic_desk_domark_hes_australia.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_19_magic_desk_domark_hes_australia.bin rename to systems/ec.bak/error_cart_type_19_magic_desk_domark_hes_australia.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_20_super_snapshot_v5.bin b/systems/ec.bak/error_cart_type_20_super_snapshot_v5.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_20_super_snapshot_v5.bin rename to systems/ec.bak/error_cart_type_20_super_snapshot_v5.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_21_comal-80.bin b/systems/ec.bak/error_cart_type_21_comal-80.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_21_comal-80.bin rename to systems/ec.bak/error_cart_type_21_comal-80.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_22_structured_basic.bin b/systems/ec.bak/error_cart_type_22_structured_basic.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_22_structured_basic.bin rename to systems/ec.bak/error_cart_type_22_structured_basic.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_23_ross.bin b/systems/ec.bak/error_cart_type_23_ross.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_23_ross.bin rename to systems/ec.bak/error_cart_type_23_ross.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_24_dela_ep64.bin b/systems/ec.bak/error_cart_type_24_dela_ep64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_24_dela_ep64.bin rename to systems/ec.bak/error_cart_type_24_dela_ep64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_25_dela_ep7x8.bin b/systems/ec.bak/error_cart_type_25_dela_ep7x8.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_25_dela_ep7x8.bin rename to systems/ec.bak/error_cart_type_25_dela_ep7x8.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_26_dela_ep256.bin b/systems/ec.bak/error_cart_type_26_dela_ep256.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_26_dela_ep256.bin rename to systems/ec.bak/error_cart_type_26_dela_ep256.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_27_rex_ep256.bin b/systems/ec.bak/error_cart_type_27_rex_ep256.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_27_rex_ep256.bin rename to systems/ec.bak/error_cart_type_27_rex_ep256.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_28_mikro_assembler.bin b/systems/ec.bak/error_cart_type_28_mikro_assembler.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_28_mikro_assembler.bin rename to systems/ec.bak/error_cart_type_28_mikro_assembler.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_29_final_cartridge_plus.bin b/systems/ec.bak/error_cart_type_29_final_cartridge_plus.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_29_final_cartridge_plus.bin rename to systems/ec.bak/error_cart_type_29_final_cartridge_plus.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_30_action_replay_4.bin b/systems/ec.bak/error_cart_type_30_action_replay_4.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_30_action_replay_4.bin rename to systems/ec.bak/error_cart_type_30_action_replay_4.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_31_stardos.bin b/systems/ec.bak/error_cart_type_31_stardos.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_31_stardos.bin rename to systems/ec.bak/error_cart_type_31_stardos.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_32_easyflash.bin b/systems/ec.bak/error_cart_type_32_easyflash.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_32_easyflash.bin rename to systems/ec.bak/error_cart_type_32_easyflash.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_33_easyflash_x-bank.bin b/systems/ec.bak/error_cart_type_33_easyflash_x-bank.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_33_easyflash_x-bank.bin rename to systems/ec.bak/error_cart_type_33_easyflash_x-bank.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_34_capture.bin b/systems/ec.bak/error_cart_type_34_capture.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_34_capture.bin rename to systems/ec.bak/error_cart_type_34_capture.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_35_action_replay_3.bin b/systems/ec.bak/error_cart_type_35_action_replay_3.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_35_action_replay_3.bin rename to systems/ec.bak/error_cart_type_35_action_replay_3.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_36_retro_replay_nordic_replay.bin b/systems/ec.bak/error_cart_type_36_retro_replay_nordic_replay.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_36_retro_replay_nordic_replay.bin rename to systems/ec.bak/error_cart_type_36_retro_replay_nordic_replay.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_37_mmc64.bin b/systems/ec.bak/error_cart_type_37_mmc64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_37_mmc64.bin rename to systems/ec.bak/error_cart_type_37_mmc64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_38_mmc_replay.bin b/systems/ec.bak/error_cart_type_38_mmc_replay.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_38_mmc_replay.bin rename to systems/ec.bak/error_cart_type_38_mmc_replay.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_39_ide64.bin b/systems/ec.bak/error_cart_type_39_ide64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_39_ide64.bin rename to systems/ec.bak/error_cart_type_39_ide64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_40_super_snapshot_v4.bin b/systems/ec.bak/error_cart_type_40_super_snapshot_v4.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_40_super_snapshot_v4.bin rename to systems/ec.bak/error_cart_type_40_super_snapshot_v4.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_41_ieee488.bin b/systems/ec.bak/error_cart_type_41_ieee488.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_41_ieee488.bin rename to systems/ec.bak/error_cart_type_41_ieee488.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_42_game_killer.bin b/systems/ec.bak/error_cart_type_42_game_killer.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_42_game_killer.bin rename to systems/ec.bak/error_cart_type_42_game_killer.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_43_prophet_64.bin b/systems/ec.bak/error_cart_type_43_prophet_64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_43_prophet_64.bin rename to systems/ec.bak/error_cart_type_43_prophet_64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_44_exos.bin b/systems/ec.bak/error_cart_type_44_exos.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_44_exos.bin rename to systems/ec.bak/error_cart_type_44_exos.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_45_freeze_frame.bin b/systems/ec.bak/error_cart_type_45_freeze_frame.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_45_freeze_frame.bin rename to systems/ec.bak/error_cart_type_45_freeze_frame.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_46_freeze_machine.bin b/systems/ec.bak/error_cart_type_46_freeze_machine.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_46_freeze_machine.bin rename to systems/ec.bak/error_cart_type_46_freeze_machine.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_47_snapshot64.bin b/systems/ec.bak/error_cart_type_47_snapshot64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_47_snapshot64.bin rename to systems/ec.bak/error_cart_type_47_snapshot64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_48_super_explode_v5.bin b/systems/ec.bak/error_cart_type_48_super_explode_v5.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_48_super_explode_v5.bin rename to systems/ec.bak/error_cart_type_48_super_explode_v5.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_49_magic_voice.bin b/systems/ec.bak/error_cart_type_49_magic_voice.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_49_magic_voice.bin rename to systems/ec.bak/error_cart_type_49_magic_voice.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_50_action_replay_2.bin b/systems/ec.bak/error_cart_type_50_action_replay_2.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_50_action_replay_2.bin rename to systems/ec.bak/error_cart_type_50_action_replay_2.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_51_mach_5.bin b/systems/ec.bak/error_cart_type_51_mach_5.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_51_mach_5.bin rename to systems/ec.bak/error_cart_type_51_mach_5.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_52_diashow_maker.bin b/systems/ec.bak/error_cart_type_52_diashow_maker.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_52_diashow_maker.bin rename to systems/ec.bak/error_cart_type_52_diashow_maker.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_53_pagefox.bin b/systems/ec.bak/error_cart_type_53_pagefox.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_53_pagefox.bin rename to systems/ec.bak/error_cart_type_53_pagefox.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_54_kingsoft_business_basic.bin b/systems/ec.bak/error_cart_type_54_kingsoft_business_basic.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_54_kingsoft_business_basic.bin rename to systems/ec.bak/error_cart_type_54_kingsoft_business_basic.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_55_silver_rock_128.bin b/systems/ec.bak/error_cart_type_55_silver_rock_128.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_55_silver_rock_128.bin rename to systems/ec.bak/error_cart_type_55_silver_rock_128.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_56_formel_64.bin b/systems/ec.bak/error_cart_type_56_formel_64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_56_formel_64.bin rename to systems/ec.bak/error_cart_type_56_formel_64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_57_rgcd.bin b/systems/ec.bak/error_cart_type_57_rgcd.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_57_rgcd.bin rename to systems/ec.bak/error_cart_type_57_rgcd.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_58_rr-net_mk3.bin b/systems/ec.bak/error_cart_type_58_rr-net_mk3.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_58_rr-net_mk3.bin rename to systems/ec.bak/error_cart_type_58_rr-net_mk3.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_59_easy_calc_result.bin b/systems/ec.bak/error_cart_type_59_easy_calc_result.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_59_easy_calc_result.bin rename to systems/ec.bak/error_cart_type_59_easy_calc_result.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_60_gmod2.bin b/systems/ec.bak/error_cart_type_60_gmod2.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_60_gmod2.bin rename to systems/ec.bak/error_cart_type_60_gmod2.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_61_max_basic.bin b/systems/ec.bak/error_cart_type_61_max_basic.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_61_max_basic.bin rename to systems/ec.bak/error_cart_type_61_max_basic.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_62_gmod3.bin b/systems/ec.bak/error_cart_type_62_gmod3.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_62_gmod3.bin rename to systems/ec.bak/error_cart_type_62_gmod3.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_63_zipp-code_48.bin b/systems/ec.bak/error_cart_type_63_zipp-code_48.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_63_zipp-code_48.bin rename to systems/ec.bak/error_cart_type_63_zipp-code_48.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_64_blackbox_v8.bin b/systems/ec.bak/error_cart_type_64_blackbox_v8.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_64_blackbox_v8.bin rename to systems/ec.bak/error_cart_type_64_blackbox_v8.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_65_blackbox_v3.bin b/systems/ec.bak/error_cart_type_65_blackbox_v3.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_65_blackbox_v3.bin rename to systems/ec.bak/error_cart_type_65_blackbox_v3.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_66_blackbox_v4.bin b/systems/ec.bak/error_cart_type_66_blackbox_v4.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_66_blackbox_v4.bin rename to systems/ec.bak/error_cart_type_66_blackbox_v4.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_67_rex_ram_floppy.bin b/systems/ec.bak/error_cart_type_67_rex_ram_floppy.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_67_rex_ram_floppy.bin rename to systems/ec.bak/error_cart_type_67_rex_ram_floppy.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_68_bis_plus.bin b/systems/ec.bak/error_cart_type_68_bis_plus.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_68_bis_plus.bin rename to systems/ec.bak/error_cart_type_68_bis_plus.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_69_sd_box.bin b/systems/ec.bak/error_cart_type_69_sd_box.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_69_sd_box.bin rename to systems/ec.bak/error_cart_type_69_sd_box.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_70_multimax.bin b/systems/ec.bak/error_cart_type_70_multimax.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_70_multimax.bin rename to systems/ec.bak/error_cart_type_70_multimax.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_71_blackbox_v9.bin b/systems/ec.bak/error_cart_type_71_blackbox_v9.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_71_blackbox_v9.bin rename to systems/ec.bak/error_cart_type_71_blackbox_v9.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_72_lt_kernal.bin b/systems/ec.bak/error_cart_type_72_lt_kernal.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_72_lt_kernal.bin rename to systems/ec.bak/error_cart_type_72_lt_kernal.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_73_cmd_ramlink.bin b/systems/ec.bak/error_cart_type_73_cmd_ramlink.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_73_cmd_ramlink.bin rename to systems/ec.bak/error_cart_type_73_cmd_ramlink.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_74_drean_hero_bootleg.bin b/systems/ec.bak/error_cart_type_74_drean_hero_bootleg.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_74_drean_hero_bootleg.bin rename to systems/ec.bak/error_cart_type_74_drean_hero_bootleg.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_75_ieee_flash_64.bin b/systems/ec.bak/error_cart_type_75_ieee_flash_64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_75_ieee_flash_64.bin rename to systems/ec.bak/error_cart_type_75_ieee_flash_64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_76_turtle_graphics_ii.bin b/systems/ec.bak/error_cart_type_76_turtle_graphics_ii.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_76_turtle_graphics_ii.bin rename to systems/ec.bak/error_cart_type_76_turtle_graphics_ii.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_77_freeze_frame_mk2.bin b/systems/ec.bak/error_cart_type_77_freeze_frame_mk2.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_77_freeze_frame_mk2.bin rename to systems/ec.bak/error_cart_type_77_freeze_frame_mk2.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_78_partner_64.bin b/systems/ec.bak/error_cart_type_78_partner_64.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_78_partner_64.bin rename to systems/ec.bak/error_cart_type_78_partner_64.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_79_hyper-basic_mk2.bin b/systems/ec.bak/error_cart_type_79_hyper-basic_mk2.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_79_hyper-basic_mk2.bin rename to systems/ec.bak/error_cart_type_79_hyper-basic_mk2.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_80_universal_cartridge_1.bin b/systems/ec.bak/error_cart_type_80_universal_cartridge_1.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_80_universal_cartridge_1.bin rename to systems/ec.bak/error_cart_type_80_universal_cartridge_1.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_81_universal_cartridge_15.bin b/systems/ec.bak/error_cart_type_81_universal_cartridge_15.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_81_universal_cartridge_15.bin rename to systems/ec.bak/error_cart_type_81_universal_cartridge_15.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_82_universal_cartridge_2.bin b/systems/ec.bak/error_cart_type_82_universal_cartridge_2.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_82_universal_cartridge_2.bin rename to systems/ec.bak/error_cart_type_82_universal_cartridge_2.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_83_bmp_data_turbo_2000.bin b/systems/ec.bak/error_cart_type_83_bmp_data_turbo_2000.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_83_bmp_data_turbo_2000.bin rename to systems/ec.bak/error_cart_type_83_bmp_data_turbo_2000.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_84_profi-dos.bin b/systems/ec.bak/error_cart_type_84_profi-dos.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_84_profi-dos.bin rename to systems/ec.bak/error_cart_type_84_profi-dos.bin diff --git a/systems/c64/cartridges/error_cartridges/error_cart_type_85_magic_desk_16.bin b/systems/ec.bak/error_cart_type_85_magic_desk_16.bin similarity index 100% rename from systems/c64/cartridges/error_cartridges/error_cart_type_85_magic_desk_16.bin rename to systems/ec.bak/error_cart_type_85_magic_desk_16.bin diff --git a/systems/error_cartridges/error_cart_type_01_action_replay.bin b/systems/error_cartridges/error_cart_type_01_action_replay.bin new file mode 100644 index 0000000..d466528 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_01_action_replay.bin differ diff --git a/systems/error_cartridges/error_cart_type_02_kcs_power_cartridge.bin b/systems/error_cartridges/error_cart_type_02_kcs_power_cartridge.bin new file mode 100644 index 0000000..6e36a1d Binary files /dev/null and b/systems/error_cartridges/error_cart_type_02_kcs_power_cartridge.bin differ diff --git a/systems/error_cartridges/error_cart_type_03_final_cartridge_iii.bin b/systems/error_cartridges/error_cart_type_03_final_cartridge_iii.bin new file mode 100644 index 0000000..8c688d7 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_03_final_cartridge_iii.bin differ diff --git a/systems/error_cartridges/error_cart_type_04_simons_basic.bin b/systems/error_cartridges/error_cart_type_04_simons_basic.bin new file mode 100644 index 0000000..cd39ef0 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_04_simons_basic.bin differ diff --git a/systems/error_cartridges/error_cart_type_05_ocean_type_1.bin b/systems/error_cartridges/error_cart_type_05_ocean_type_1.bin new file mode 100644 index 0000000..700052c Binary files /dev/null and b/systems/error_cartridges/error_cart_type_05_ocean_type_1.bin differ diff --git a/systems/error_cartridges/error_cart_type_06_expert_cartridge.bin b/systems/error_cartridges/error_cart_type_06_expert_cartridge.bin new file mode 100644 index 0000000..87ad16d Binary files /dev/null and b/systems/error_cartridges/error_cart_type_06_expert_cartridge.bin differ diff --git a/systems/error_cartridges/error_cart_type_07_fun_play_power_play.bin b/systems/error_cartridges/error_cart_type_07_fun_play_power_play.bin new file mode 100644 index 0000000..db6d80b Binary files /dev/null and b/systems/error_cartridges/error_cart_type_07_fun_play_power_play.bin differ diff --git a/systems/error_cartridges/error_cart_type_08_super_games.bin b/systems/error_cartridges/error_cart_type_08_super_games.bin new file mode 100644 index 0000000..804b964 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_08_super_games.bin differ diff --git a/systems/error_cartridges/error_cart_type_09_atomic_power.bin b/systems/error_cartridges/error_cart_type_09_atomic_power.bin new file mode 100644 index 0000000..f25f200 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_09_atomic_power.bin differ diff --git a/systems/error_cartridges/error_cart_type_10_epyx_fastload.bin b/systems/error_cartridges/error_cart_type_10_epyx_fastload.bin new file mode 100644 index 0000000..41e825a Binary files /dev/null and b/systems/error_cartridges/error_cart_type_10_epyx_fastload.bin differ diff --git a/systems/error_cartridges/error_cart_type_11_westermann_learning.bin b/systems/error_cartridges/error_cart_type_11_westermann_learning.bin new file mode 100644 index 0000000..5f37d0d Binary files /dev/null and b/systems/error_cartridges/error_cart_type_11_westermann_learning.bin differ diff --git a/systems/error_cartridges/error_cart_type_12_rex_utility.bin b/systems/error_cartridges/error_cart_type_12_rex_utility.bin new file mode 100644 index 0000000..9a3eba0 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_12_rex_utility.bin differ diff --git a/systems/error_cartridges/error_cart_type_13_final_cartridge_i.bin b/systems/error_cartridges/error_cart_type_13_final_cartridge_i.bin new file mode 100644 index 0000000..7705400 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_13_final_cartridge_i.bin differ diff --git a/systems/error_cartridges/error_cart_type_14_magic_formel.bin b/systems/error_cartridges/error_cart_type_14_magic_formel.bin new file mode 100644 index 0000000..cf84e43 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_14_magic_formel.bin differ diff --git a/systems/error_cartridges/error_cart_type_15_c64_game_system_system_3.bin b/systems/error_cartridges/error_cart_type_15_c64_game_system_system_3.bin new file mode 100644 index 0000000..1ac903e Binary files /dev/null and b/systems/error_cartridges/error_cart_type_15_c64_game_system_system_3.bin differ diff --git a/systems/error_cartridges/error_cart_type_16_warpspeed.bin b/systems/error_cartridges/error_cart_type_16_warpspeed.bin new file mode 100644 index 0000000..82327a9 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_16_warpspeed.bin differ diff --git a/systems/error_cartridges/error_cart_type_17_dinamic.bin b/systems/error_cartridges/error_cart_type_17_dinamic.bin new file mode 100644 index 0000000..fbb1da2 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_17_dinamic.bin differ diff --git a/systems/error_cartridges/error_cart_type_18_zaxxon_super_zaxxon_sega.bin b/systems/error_cartridges/error_cart_type_18_zaxxon_super_zaxxon_sega.bin new file mode 100644 index 0000000..417b8c8 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_18_zaxxon_super_zaxxon_sega.bin differ diff --git a/systems/error_cartridges/error_cart_type_19_magic_desk_domark_hes_australia.bin b/systems/error_cartridges/error_cart_type_19_magic_desk_domark_hes_australia.bin new file mode 100644 index 0000000..c5aadc7 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_19_magic_desk_domark_hes_australia.bin differ diff --git a/systems/error_cartridges/error_cart_type_20_super_snapshot_v5.bin b/systems/error_cartridges/error_cart_type_20_super_snapshot_v5.bin new file mode 100644 index 0000000..508b5bd Binary files /dev/null and b/systems/error_cartridges/error_cart_type_20_super_snapshot_v5.bin differ diff --git a/systems/error_cartridges/error_cart_type_21_comal-80.bin b/systems/error_cartridges/error_cart_type_21_comal-80.bin new file mode 100644 index 0000000..5505893 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_21_comal-80.bin differ diff --git a/systems/error_cartridges/error_cart_type_22_structured_basic.bin b/systems/error_cartridges/error_cart_type_22_structured_basic.bin new file mode 100644 index 0000000..a33ae6c Binary files /dev/null and b/systems/error_cartridges/error_cart_type_22_structured_basic.bin differ diff --git a/systems/error_cartridges/error_cart_type_23_ross.bin b/systems/error_cartridges/error_cart_type_23_ross.bin new file mode 100644 index 0000000..39af4c7 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_23_ross.bin differ diff --git a/systems/error_cartridges/error_cart_type_24_dela_ep64.bin b/systems/error_cartridges/error_cart_type_24_dela_ep64.bin new file mode 100644 index 0000000..0660d94 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_24_dela_ep64.bin differ diff --git a/systems/error_cartridges/error_cart_type_25_dela_ep7x8.bin b/systems/error_cartridges/error_cart_type_25_dela_ep7x8.bin new file mode 100644 index 0000000..422092a Binary files /dev/null and b/systems/error_cartridges/error_cart_type_25_dela_ep7x8.bin differ diff --git a/systems/error_cartridges/error_cart_type_26_dela_ep256.bin b/systems/error_cartridges/error_cart_type_26_dela_ep256.bin new file mode 100644 index 0000000..a145ddc Binary files /dev/null and b/systems/error_cartridges/error_cart_type_26_dela_ep256.bin differ diff --git a/systems/error_cartridges/error_cart_type_27_rex_ep256.bin b/systems/error_cartridges/error_cart_type_27_rex_ep256.bin new file mode 100644 index 0000000..29b61ba Binary files /dev/null and b/systems/error_cartridges/error_cart_type_27_rex_ep256.bin differ diff --git a/systems/error_cartridges/error_cart_type_28_mikro_assembler.bin b/systems/error_cartridges/error_cart_type_28_mikro_assembler.bin new file mode 100644 index 0000000..9cfccb9 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_28_mikro_assembler.bin differ diff --git a/systems/error_cartridges/error_cart_type_29_final_cartridge_plus.bin b/systems/error_cartridges/error_cart_type_29_final_cartridge_plus.bin new file mode 100644 index 0000000..953391c Binary files /dev/null and b/systems/error_cartridges/error_cart_type_29_final_cartridge_plus.bin differ diff --git a/systems/error_cartridges/error_cart_type_30_action_replay_4.bin b/systems/error_cartridges/error_cart_type_30_action_replay_4.bin new file mode 100644 index 0000000..26c6870 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_30_action_replay_4.bin differ diff --git a/systems/error_cartridges/error_cart_type_31_stardos.bin b/systems/error_cartridges/error_cart_type_31_stardos.bin new file mode 100644 index 0000000..3931126 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_31_stardos.bin differ diff --git a/systems/error_cartridges/error_cart_type_32_easyflash.bin b/systems/error_cartridges/error_cart_type_32_easyflash.bin new file mode 100644 index 0000000..dfa6b0f Binary files /dev/null and b/systems/error_cartridges/error_cart_type_32_easyflash.bin differ diff --git a/systems/error_cartridges/error_cart_type_33_easyflash_x-bank.bin b/systems/error_cartridges/error_cart_type_33_easyflash_x-bank.bin new file mode 100644 index 0000000..a3cf988 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_33_easyflash_x-bank.bin differ diff --git a/systems/error_cartridges/error_cart_type_34_capture.bin b/systems/error_cartridges/error_cart_type_34_capture.bin new file mode 100644 index 0000000..8b20907 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_34_capture.bin differ diff --git a/systems/error_cartridges/error_cart_type_35_action_replay_3.bin b/systems/error_cartridges/error_cart_type_35_action_replay_3.bin new file mode 100644 index 0000000..d12448b Binary files /dev/null and b/systems/error_cartridges/error_cart_type_35_action_replay_3.bin differ diff --git a/systems/error_cartridges/error_cart_type_36_retro_replay_nordic_replay.bin b/systems/error_cartridges/error_cart_type_36_retro_replay_nordic_replay.bin new file mode 100644 index 0000000..753e578 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_36_retro_replay_nordic_replay.bin differ diff --git a/systems/error_cartridges/error_cart_type_37_mmc64.bin b/systems/error_cartridges/error_cart_type_37_mmc64.bin new file mode 100644 index 0000000..ec19cdf Binary files /dev/null and b/systems/error_cartridges/error_cart_type_37_mmc64.bin differ diff --git a/systems/error_cartridges/error_cart_type_38_mmc_replay.bin b/systems/error_cartridges/error_cart_type_38_mmc_replay.bin new file mode 100644 index 0000000..d067e16 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_38_mmc_replay.bin differ diff --git a/systems/error_cartridges/error_cart_type_39_ide64.bin b/systems/error_cartridges/error_cart_type_39_ide64.bin new file mode 100644 index 0000000..28d7dd6 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_39_ide64.bin differ diff --git a/systems/error_cartridges/error_cart_type_40_super_snapshot_v4.bin b/systems/error_cartridges/error_cart_type_40_super_snapshot_v4.bin new file mode 100644 index 0000000..75336ea Binary files /dev/null and b/systems/error_cartridges/error_cart_type_40_super_snapshot_v4.bin differ diff --git a/systems/error_cartridges/error_cart_type_41_ieee488.bin b/systems/error_cartridges/error_cart_type_41_ieee488.bin new file mode 100644 index 0000000..73e3984 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_41_ieee488.bin differ diff --git a/systems/error_cartridges/error_cart_type_42_game_killer.bin b/systems/error_cartridges/error_cart_type_42_game_killer.bin new file mode 100644 index 0000000..6d6a136 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_42_game_killer.bin differ diff --git a/systems/error_cartridges/error_cart_type_43_prophet_64.bin b/systems/error_cartridges/error_cart_type_43_prophet_64.bin new file mode 100644 index 0000000..4b5495e Binary files /dev/null and b/systems/error_cartridges/error_cart_type_43_prophet_64.bin differ diff --git a/systems/error_cartridges/error_cart_type_44_exos.bin b/systems/error_cartridges/error_cart_type_44_exos.bin new file mode 100644 index 0000000..653cdb3 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_44_exos.bin differ diff --git a/systems/error_cartridges/error_cart_type_45_freeze_frame.bin b/systems/error_cartridges/error_cart_type_45_freeze_frame.bin new file mode 100644 index 0000000..a9dd3e4 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_45_freeze_frame.bin differ diff --git a/systems/error_cartridges/error_cart_type_46_freeze_machine.bin b/systems/error_cartridges/error_cart_type_46_freeze_machine.bin new file mode 100644 index 0000000..4eb45eb Binary files /dev/null and b/systems/error_cartridges/error_cart_type_46_freeze_machine.bin differ diff --git a/systems/error_cartridges/error_cart_type_47_snapshot64.bin b/systems/error_cartridges/error_cart_type_47_snapshot64.bin new file mode 100644 index 0000000..334132b Binary files /dev/null and b/systems/error_cartridges/error_cart_type_47_snapshot64.bin differ diff --git a/systems/error_cartridges/error_cart_type_48_super_explode_v5.bin b/systems/error_cartridges/error_cart_type_48_super_explode_v5.bin new file mode 100644 index 0000000..c10fb0a Binary files /dev/null and b/systems/error_cartridges/error_cart_type_48_super_explode_v5.bin differ diff --git a/systems/error_cartridges/error_cart_type_49_magic_voice.bin b/systems/error_cartridges/error_cart_type_49_magic_voice.bin new file mode 100644 index 0000000..2090094 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_49_magic_voice.bin differ diff --git a/systems/error_cartridges/error_cart_type_50_action_replay_2.bin b/systems/error_cartridges/error_cart_type_50_action_replay_2.bin new file mode 100644 index 0000000..aa25e8b Binary files /dev/null and b/systems/error_cartridges/error_cart_type_50_action_replay_2.bin differ diff --git a/systems/error_cartridges/error_cart_type_51_mach_5.bin b/systems/error_cartridges/error_cart_type_51_mach_5.bin new file mode 100644 index 0000000..2b7ebbc Binary files /dev/null and b/systems/error_cartridges/error_cart_type_51_mach_5.bin differ diff --git a/systems/error_cartridges/error_cart_type_52_diashow_maker.bin b/systems/error_cartridges/error_cart_type_52_diashow_maker.bin new file mode 100644 index 0000000..152386a Binary files /dev/null and b/systems/error_cartridges/error_cart_type_52_diashow_maker.bin differ diff --git a/systems/error_cartridges/error_cart_type_53_pagefox.bin b/systems/error_cartridges/error_cart_type_53_pagefox.bin new file mode 100644 index 0000000..cd23b37 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_53_pagefox.bin differ diff --git a/systems/error_cartridges/error_cart_type_54_kingsoft_business_basic.bin b/systems/error_cartridges/error_cart_type_54_kingsoft_business_basic.bin new file mode 100644 index 0000000..0e47d68 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_54_kingsoft_business_basic.bin differ diff --git a/systems/error_cartridges/error_cart_type_55_silver_rock_128.bin b/systems/error_cartridges/error_cart_type_55_silver_rock_128.bin new file mode 100644 index 0000000..e81b4fb Binary files /dev/null and b/systems/error_cartridges/error_cart_type_55_silver_rock_128.bin differ diff --git a/systems/error_cartridges/error_cart_type_56_formel_64.bin b/systems/error_cartridges/error_cart_type_56_formel_64.bin new file mode 100644 index 0000000..ac01074 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_56_formel_64.bin differ diff --git a/systems/error_cartridges/error_cart_type_57_rgcd.bin b/systems/error_cartridges/error_cart_type_57_rgcd.bin new file mode 100644 index 0000000..320235e Binary files /dev/null and b/systems/error_cartridges/error_cart_type_57_rgcd.bin differ diff --git a/systems/error_cartridges/error_cart_type_58_rr-net_mk3.bin b/systems/error_cartridges/error_cart_type_58_rr-net_mk3.bin new file mode 100644 index 0000000..032a950 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_58_rr-net_mk3.bin differ diff --git a/systems/error_cartridges/error_cart_type_59_easy_calc_result.bin b/systems/error_cartridges/error_cart_type_59_easy_calc_result.bin new file mode 100644 index 0000000..7a438e3 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_59_easy_calc_result.bin differ diff --git a/systems/error_cartridges/error_cart_type_60_gmod2.bin b/systems/error_cartridges/error_cart_type_60_gmod2.bin new file mode 100644 index 0000000..e5421c5 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_60_gmod2.bin differ diff --git a/systems/error_cartridges/error_cart_type_61_max_basic.bin b/systems/error_cartridges/error_cart_type_61_max_basic.bin new file mode 100644 index 0000000..2061f6c Binary files /dev/null and b/systems/error_cartridges/error_cart_type_61_max_basic.bin differ diff --git a/systems/error_cartridges/error_cart_type_62_gmod3.bin b/systems/error_cartridges/error_cart_type_62_gmod3.bin new file mode 100644 index 0000000..7975d46 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_62_gmod3.bin differ diff --git a/systems/error_cartridges/error_cart_type_63_zipp-code_48.bin b/systems/error_cartridges/error_cart_type_63_zipp-code_48.bin new file mode 100644 index 0000000..d07dea4 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_63_zipp-code_48.bin differ diff --git a/systems/error_cartridges/error_cart_type_64_blackbox_v8.bin b/systems/error_cartridges/error_cart_type_64_blackbox_v8.bin new file mode 100644 index 0000000..07beaef Binary files /dev/null and b/systems/error_cartridges/error_cart_type_64_blackbox_v8.bin differ diff --git a/systems/error_cartridges/error_cart_type_65_blackbox_v3.bin b/systems/error_cartridges/error_cart_type_65_blackbox_v3.bin new file mode 100644 index 0000000..7265f51 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_65_blackbox_v3.bin differ diff --git a/systems/error_cartridges/error_cart_type_66_blackbox_v4.bin b/systems/error_cartridges/error_cart_type_66_blackbox_v4.bin new file mode 100644 index 0000000..f2863cc Binary files /dev/null and b/systems/error_cartridges/error_cart_type_66_blackbox_v4.bin differ diff --git a/systems/error_cartridges/error_cart_type_67_rex_ram_floppy.bin b/systems/error_cartridges/error_cart_type_67_rex_ram_floppy.bin new file mode 100644 index 0000000..8f2eba3 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_67_rex_ram_floppy.bin differ diff --git a/systems/error_cartridges/error_cart_type_68_bis_plus.bin b/systems/error_cartridges/error_cart_type_68_bis_plus.bin new file mode 100644 index 0000000..e923296 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_68_bis_plus.bin differ diff --git a/systems/error_cartridges/error_cart_type_69_sd_box.bin b/systems/error_cartridges/error_cart_type_69_sd_box.bin new file mode 100644 index 0000000..a7755c8 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_69_sd_box.bin differ diff --git a/systems/error_cartridges/error_cart_type_70_multimax.bin b/systems/error_cartridges/error_cart_type_70_multimax.bin new file mode 100644 index 0000000..ab1f4b9 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_70_multimax.bin differ diff --git a/systems/error_cartridges/error_cart_type_71_blackbox_v9.bin b/systems/error_cartridges/error_cart_type_71_blackbox_v9.bin new file mode 100644 index 0000000..9172c45 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_71_blackbox_v9.bin differ diff --git a/systems/error_cartridges/error_cart_type_72_lt_kernal.bin b/systems/error_cartridges/error_cart_type_72_lt_kernal.bin new file mode 100644 index 0000000..bbe0ccc Binary files /dev/null and b/systems/error_cartridges/error_cart_type_72_lt_kernal.bin differ diff --git a/systems/error_cartridges/error_cart_type_73_cmd_ramlink.bin b/systems/error_cartridges/error_cart_type_73_cmd_ramlink.bin new file mode 100644 index 0000000..b6cfb4e Binary files /dev/null and b/systems/error_cartridges/error_cart_type_73_cmd_ramlink.bin differ diff --git a/systems/error_cartridges/error_cart_type_74_drean_hero_bootleg.bin b/systems/error_cartridges/error_cart_type_74_drean_hero_bootleg.bin new file mode 100644 index 0000000..6b2767b Binary files /dev/null and b/systems/error_cartridges/error_cart_type_74_drean_hero_bootleg.bin differ diff --git a/systems/error_cartridges/error_cart_type_75_ieee_flash_64.bin b/systems/error_cartridges/error_cart_type_75_ieee_flash_64.bin new file mode 100644 index 0000000..21994b6 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_75_ieee_flash_64.bin differ diff --git a/systems/error_cartridges/error_cart_type_76_turtle_graphics_ii.bin b/systems/error_cartridges/error_cart_type_76_turtle_graphics_ii.bin new file mode 100644 index 0000000..6e36dd6 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_76_turtle_graphics_ii.bin differ diff --git a/systems/error_cartridges/error_cart_type_77_freeze_frame_mk2.bin b/systems/error_cartridges/error_cart_type_77_freeze_frame_mk2.bin new file mode 100644 index 0000000..01269cf Binary files /dev/null and b/systems/error_cartridges/error_cart_type_77_freeze_frame_mk2.bin differ diff --git a/systems/error_cartridges/error_cart_type_78_partner_64.bin b/systems/error_cartridges/error_cart_type_78_partner_64.bin new file mode 100644 index 0000000..d46256d Binary files /dev/null and b/systems/error_cartridges/error_cart_type_78_partner_64.bin differ diff --git a/systems/error_cartridges/error_cart_type_79_hyper-basic_mk2.bin b/systems/error_cartridges/error_cart_type_79_hyper-basic_mk2.bin new file mode 100644 index 0000000..7f56224 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_79_hyper-basic_mk2.bin differ diff --git a/systems/error_cartridges/error_cart_type_80_universal_cartridge_1.bin b/systems/error_cartridges/error_cart_type_80_universal_cartridge_1.bin new file mode 100644 index 0000000..7da8cb6 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_80_universal_cartridge_1.bin differ diff --git a/systems/error_cartridges/error_cart_type_81_universal_cartridge_15.bin b/systems/error_cartridges/error_cart_type_81_universal_cartridge_15.bin new file mode 100644 index 0000000..990ae0c Binary files /dev/null and b/systems/error_cartridges/error_cart_type_81_universal_cartridge_15.bin differ diff --git a/systems/error_cartridges/error_cart_type_82_universal_cartridge_2.bin b/systems/error_cartridges/error_cart_type_82_universal_cartridge_2.bin new file mode 100644 index 0000000..74edfb4 Binary files /dev/null and b/systems/error_cartridges/error_cart_type_82_universal_cartridge_2.bin differ diff --git a/systems/error_cartridges/error_cart_type_83_bmp_data_turbo_2000.bin b/systems/error_cartridges/error_cart_type_83_bmp_data_turbo_2000.bin new file mode 100644 index 0000000..755eafc Binary files /dev/null and b/systems/error_cartridges/error_cart_type_83_bmp_data_turbo_2000.bin differ diff --git a/systems/error_cartridges/error_cart_type_84_profi-dos.bin b/systems/error_cartridges/error_cart_type_84_profi-dos.bin new file mode 100644 index 0000000..25910af Binary files /dev/null and b/systems/error_cartridges/error_cart_type_84_profi-dos.bin differ diff --git a/systems/error_cartridges/error_cart_type_85_magic_desk_16.bin b/systems/error_cartridges/error_cart_type_85_magic_desk_16.bin new file mode 100644 index 0000000..24d87ca Binary files /dev/null and b/systems/error_cartridges/error_cart_type_85_magic_desk_16.bin differ diff --git a/systems/files/zif-diagnostic-cartridge-bottom.stl b/systems/files/zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..d2abdba --- /dev/null +++ b/systems/files/zif-diagnostic-cartridge-bottom.stl @@ -0,0 +1,19770 @@ +solid OpenSCAD_Model + facet normal 0 -1 0 + outer loop + vertex -31.79 8 2 + vertex 31.79 8 10 + vertex -31.79 8 10 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 31.79 8 10 + vertex -31.79 8 2 + vertex 31.79 8 2 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex 0 8 51.035 + vertex 31.79 8 74.72 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -1.33275 8 50.9184 + vertex 0 8 51.035 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -2.625 8 50.5721 + vertex -1.33275 8 50.9184 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -3.8375 8 50.0067 + vertex -2.625 8 50.5721 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -4.93339 8 49.2394 + vertex -3.8375 8 50.0067 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -5.87939 8 48.2934 + vertex -4.93339 8 49.2394 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -6.64674 8 47.1975 + vertex -5.87939 8 48.2934 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -7.21214 8 45.985 + vertex -6.64674 8 47.1975 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -7.5584 8 44.6927 + vertex -7.21214 8 45.985 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 74.72 + vertex -7.675 8 43.36 + vertex -7.5584 8 44.6927 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -31.79 8 11.6 + vertex -7.675 8 43.36 + vertex -31.79 8 74.72 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -7.675 8 43.36 + vertex -31.79 8 11.6 + vertex -7.5584 8 42.0273 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -7.5584 8 42.0273 + vertex -31.79 8 11.6 + vertex -7.21214 8 40.735 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -7.21214 8 40.735 + vertex -31.79 8 11.6 + vertex -6.64674 8 39.5225 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -1.33275 8 35.8016 + vertex -31.79 8 11.6 + vertex 0 8 35.685 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -2.625 8 36.1479 + vertex -31.79 8 11.6 + vertex -1.33275 8 35.8016 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -3.8375 8 36.7133 + vertex -31.79 8 11.6 + vertex -2.625 8 36.1479 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -4.93339 8 37.4806 + vertex -31.79 8 11.6 + vertex -3.8375 8 36.7133 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -5.87939 8 38.4266 + vertex -31.79 8 11.6 + vertex -4.93339 8 37.4806 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -6.64674 8 39.5225 + vertex -31.79 8 11.6 + vertex -5.87939 8 38.4266 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.33275 8 50.9184 + vertex 31.79 8 74.72 + vertex 0 8 51.035 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.625 8 50.5721 + vertex 31.79 8 74.72 + vertex 1.33275 8 50.9184 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 3.8375 8 50.0067 + vertex 31.79 8 74.72 + vertex 2.625 8 50.5721 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 4.93339 8 49.2394 + vertex 31.79 8 74.72 + vertex 3.8375 8 50.0067 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 5.87939 8 48.2934 + vertex 31.79 8 74.72 + vertex 4.93339 8 49.2394 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 6.64674 8 47.1975 + vertex 31.79 8 74.72 + vertex 5.87939 8 48.2934 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 7.21214 8 45.985 + vertex 31.79 8 74.72 + vertex 6.64674 8 47.1975 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 7.5584 8 44.6927 + vertex 31.79 8 74.72 + vertex 7.21214 8 45.985 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 7.675 8 43.36 + vertex 31.79 8 74.72 + vertex 7.5584 8 44.6927 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 8 11.6 + vertex 7.675 8 43.36 + vertex 7.5584 8 42.0273 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 8 11.6 + vertex 7.5584 8 42.0273 + vertex 7.21214 8 40.735 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 8 11.6 + vertex 7.21214 8 40.735 + vertex 6.64674 8 39.5225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 8 11.6 + vertex 6.64674 8 39.5225 + vertex 5.87939 8 38.4266 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 8 11.6 + vertex 0 8 35.685 + vertex -31.79 8 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 8 35.685 + vertex 31.79 8 11.6 + vertex 1.33275 8 35.8016 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.33275 8 35.8016 + vertex 31.79 8 11.6 + vertex 2.625 8 36.1479 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.625 8 36.1479 + vertex 31.79 8 11.6 + vertex 3.8375 8 36.7133 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 7.675 8 43.36 + vertex 31.79 8 11.6 + vertex 31.79 8 74.72 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 4.93339 8 37.4806 + vertex 31.79 8 11.6 + vertex 5.87939 8 38.4266 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 3.8375 8 36.7133 + vertex 31.79 8 11.6 + vertex 4.93339 8 37.4806 + endloop + endfacet + facet normal -0.992434 -0.0868235 0.0868161 + outer loop + vertex -2.90314 4.03351 43.8719 + vertex -2.94792 4.03351 43.36 + vertex -2.875 3.2 43.36 + endloop + endfacet + facet normal -0.992431 -0.0868366 0.0868377 + outer loop + vertex -2.83132 3.2 43.8592 + vertex -2.90314 4.03351 43.8719 + vertex -2.875 3.2 43.36 + endloop + endfacet + facet normal -0.962497 -0.257896 0.0841988 + outer loop + vertex -3.1164 4.8417 43.9095 + vertex -3.16447 4.8417 43.36 + vertex -2.94792 4.03351 43.36 + endloop + endfacet + facet normal -0.962497 -0.257895 0.0841974 + outer loop + vertex -2.90314 4.03351 43.8719 + vertex -3.1164 4.8417 43.9095 + vertex -2.94792 4.03351 43.36 + endloop + endfacet + facet normal -0.903468 -0.421305 0.0790479 + outer loop + vertex -3.46463 5.6 43.9709 + vertex -3.51808 5.6 43.36 + vertex -3.16447 4.8417 43.36 + endloop + endfacet + facet normal -0.903473 -0.421296 0.0790354 + outer loop + vertex -3.1164 4.8417 43.9095 + vertex -3.46463 5.6 43.9709 + vertex -3.16447 4.8417 43.36 + endloop + endfacet + facet normal -0.817055 -0.57211 0.0714894 + outer loop + vertex -3.93725 6.28538 44.0542 + vertex -3.99799 6.28538 43.36 + vertex -3.51808 5.6 43.36 + endloop + endfacet + facet normal -0.817056 -0.572109 0.0714874 + outer loop + vertex -3.46463 5.6 43.9709 + vertex -3.93725 6.28538 44.0542 + vertex -3.51808 5.6 43.36 + endloop + endfacet + facet normal -0.705757 -0.705757 0.0617471 + outer loop + vertex -4.51989 6.87701 44.157 + vertex -4.58962 6.87701 43.36 + vertex -3.99799 6.28538 43.36 + endloop + endfacet + facet normal -0.705755 -0.70576 0.061751 + outer loop + vertex -3.93725 6.28538 44.0542 + vertex -4.51989 6.87701 44.157 + vertex -3.99799 6.28538 43.36 + endloop + endfacet + facet normal -0.572857 -0.818122 0.0501187 + outer loop + vertex -5.19486 7.35692 44.276 + vertex -5.275 7.35692 43.36 + vertex -4.58962 6.87701 43.36 + endloop + endfacet + facet normal -0.572856 -0.818122 0.0501195 + outer loop + vertex -4.51989 6.87701 44.157 + vertex -5.19486 7.35692 44.276 + vertex -4.58962 6.87701 43.36 + endloop + endfacet + facet normal -0.422338 -0.905685 0.0369491 + outer loop + vertex -5.94164 7.71053 44.4077 + vertex -6.0333 7.71053 43.36 + vertex -5.275 7.35692 43.36 + endloop + endfacet + facet normal -0.422337 -0.905685 0.0369499 + outer loop + vertex -5.19486 7.35692 44.276 + vertex -5.94164 7.71053 44.4077 + vertex -5.275 7.35692 43.36 + endloop + endfacet + facet normal -0.258748 -0.965679 0.0226383 + outer loop + vertex -6.73755 7.92708 44.548 + vertex -6.84149 7.92708 43.36 + vertex -6.0333 7.71053 43.36 + endloop + endfacet + facet normal -0.25875 -0.965679 0.0226372 + outer loop + vertex -5.94164 7.71053 44.4077 + vertex -6.73755 7.92708 44.548 + vertex -6.0333 7.71053 43.36 + endloop + endfacet + facet normal -0.08715 -0.996166 0.00762489 + outer loop + vertex -7.5584 8 44.6927 + vertex -7.675 8 43.36 + vertex -6.84149 7.92708 43.36 + endloop + endfacet + facet normal -0.08715 -0.996166 0.00762489 + outer loop + vertex -6.73755 7.92708 44.548 + vertex -7.5584 8 44.6927 + vertex -6.84149 7.92708 43.36 + endloop + endfacet + facet normal -0.962268 -0.0868437 0.257872 + outer loop + vertex -2.77014 4.03351 44.3682 + vertex -2.90314 4.03351 43.8719 + vertex -2.83132 3.2 43.8592 + endloop + endfacet + facet normal -0.962287 -0.0868082 0.257816 + outer loop + vertex -2.77014 4.03351 44.3682 + vertex -2.83132 3.2 43.8592 + vertex -2.70162 3.2 44.3433 + endloop + endfacet + facet normal -0.933249 -0.257894 0.250075 + outer loop + vertex -2.97363 4.8417 44.4423 + vertex -3.1164 4.8417 43.9095 + vertex -2.90314 4.03351 43.8719 + endloop + endfacet + facet normal -0.933241 -0.257906 0.250093 + outer loop + vertex -2.77014 4.03351 44.3682 + vertex -2.97363 4.8417 44.4423 + vertex -2.90314 4.03351 43.8719 + endloop + endfacet + facet normal -0.876025 -0.421297 0.234711 + outer loop + vertex -3.30591 5.6 44.5633 + vertex -3.46463 5.6 43.9709 + vertex -3.1164 4.8417 43.9095 + endloop + endfacet + facet normal -0.876009 -0.421315 0.234737 + outer loop + vertex -2.97363 4.8417 44.4423 + vertex -3.30591 5.6 44.5633 + vertex -3.1164 4.8417 43.9095 + endloop + endfacet + facet normal -0.792238 -0.572104 0.212264 + outer loop + vertex -3.75688 6.28538 44.7274 + vertex -3.93725 6.28538 44.0542 + vertex -3.46463 5.6 43.9709 + endloop + endfacet + facet normal -0.792239 -0.572103 0.212262 + outer loop + vertex -3.30591 5.6 44.5633 + vertex -3.75688 6.28538 44.7274 + vertex -3.46463 5.6 43.9709 + endloop + endfacet + facet normal -0.684302 -0.705766 0.183372 + outer loop + vertex -4.31283 6.87701 44.9297 + vertex -4.51989 6.87701 44.157 + vertex -3.93725 6.28538 44.0542 + endloop + endfacet + facet normal -0.684325 -0.705749 0.183351 + outer loop + vertex -3.75688 6.28538 44.7274 + vertex -4.31283 6.87701 44.9297 + vertex -3.93725 6.28538 44.0542 + endloop + endfacet + facet normal -0.555453 -0.818121 0.148825 + outer loop + vertex -4.95688 7.35692 45.1642 + vertex -5.19486 7.35692 44.276 + vertex -4.51989 6.87701 44.157 + endloop + endfacet + facet normal -0.555434 -0.818132 0.148839 + outer loop + vertex -4.31283 6.87701 44.9297 + vertex -4.95688 7.35692 45.1642 + vertex -4.51989 6.87701 44.157 + endloop + endfacet + facet normal -0.409503 -0.905686 0.109729 + outer loop + vertex -5.66945 7.71053 45.4235 + vertex -5.94164 7.71053 44.4077 + vertex -5.19486 7.35692 44.276 + endloop + endfacet + facet normal -0.409513 -0.905682 0.109723 + outer loop + vertex -4.95688 7.35692 45.1642 + vertex -5.66945 7.71053 45.4235 + vertex -5.19486 7.35692 44.276 + endloop + endfacet + facet normal -0.25089 -0.965678 0.0672256 + outer loop + vertex -6.4289 7.92708 45.6999 + vertex -6.73755 7.92708 44.548 + vertex -5.94164 7.71053 44.4077 + endloop + endfacet + facet normal -0.250887 -0.965679 0.0672268 + outer loop + vertex -5.66945 7.71053 45.4235 + vertex -6.4289 7.92708 45.6999 + vertex -5.94164 7.71053 44.4077 + endloop + endfacet + facet normal -0.0845028 -0.996166 0.0226418 + outer loop + vertex -7.21214 8 45.985 + vertex -7.5584 8 44.6927 + vertex -6.73755 7.92708 44.548 + endloop + endfacet + facet normal -0.0845018 -0.996166 0.0226421 + outer loop + vertex -6.4289 7.92708 45.6999 + vertex -7.21214 8 45.985 + vertex -6.73755 7.92708 44.548 + endloop + endfacet + facet normal -0.902921 -0.0868014 0.42095 + outer loop + vertex -2.55298 4.03351 44.834 + vertex -2.77014 4.03351 44.3682 + vertex -2.70162 3.2 44.3433 + endloop + endfacet + facet normal -0.90288 -0.0868547 0.421028 + outer loop + vertex -2.55298 4.03351 44.834 + vertex -2.70162 3.2 44.3433 + vertex -2.48982 3.20001 44.7975 + endloop + endfacet + facet normal -0.875644 -0.257912 0.408324 + outer loop + vertex -2.74052 4.8417 44.9422 + vertex -2.97363 4.8417 44.4423 + vertex -2.77014 4.03351 44.3682 + endloop + endfacet + facet normal -0.875692 -0.257861 0.408255 + outer loop + vertex -2.74052 4.8417 44.9422 + vertex -2.77014 4.03351 44.3682 + vertex -2.55298 4.03351 44.834 + endloop + endfacet + facet normal -0.821916 -0.421323 0.383329 + outer loop + vertex -3.04674 5.6 45.119 + vertex -3.30591 5.6 44.5633 + vertex -2.97363 4.8417 44.4423 + endloop + endfacet + facet normal -0.821953 -0.421289 0.383287 + outer loop + vertex -2.74052 4.8417 44.9422 + vertex -3.04674 5.6 45.119 + vertex -2.97363 4.8417 44.4423 + endloop + endfacet + facet normal -0.74334 -0.572099 0.346625 + outer loop + vertex -3.46236 6.28538 45.359 + vertex -3.75688 6.28538 44.7274 + vertex -3.30591 5.6 44.5633 + endloop + endfacet + facet normal -0.743297 -0.572132 0.346662 + outer loop + vertex -3.04674 5.6 45.119 + vertex -3.46236 6.28538 45.359 + vertex -3.30591 5.6 44.5633 + endloop + endfacet + facet normal -0.642094 -0.705746 0.299396 + outer loop + vertex -3.97473 6.87701 45.6548 + vertex -4.31283 6.87701 44.9297 + vertex -3.75688 6.28538 44.7274 + endloop + endfacet + facet normal -0.642079 -0.705756 0.299406 + outer loop + vertex -3.46236 6.28538 45.359 + vertex -3.97473 6.87701 45.6548 + vertex -3.75688 6.28538 44.7274 + endloop + endfacet + facet normal -0.521142 -0.818137 0.243029 + outer loop + vertex -4.56828 7.35692 45.9975 + vertex -4.95688 7.35692 45.1642 + vertex -4.31283 6.87701 44.9297 + endloop + endfacet + facet normal -0.521175 -0.81812 0.243014 + outer loop + vertex -3.97473 6.87701 45.6548 + vertex -4.56828 7.35692 45.9975 + vertex -4.31283 6.87701 44.9297 + endloop + endfacet + facet normal -0.384243 -0.90568 0.179166 + outer loop + vertex -5.22499 7.71053 46.3767 + vertex -5.66945 7.71053 45.4235 + vertex -4.95688 7.35692 45.1642 + endloop + endfacet + facet normal -0.384216 -0.90569 0.179175 + outer loop + vertex -4.56828 7.35692 45.9975 + vertex -5.22499 7.71053 46.3767 + vertex -4.95688 7.35692 45.1642 + endloop + endfacet + facet normal -0.235403 -0.965679 0.109773 + outer loop + vertex -5.9249 7.92708 46.7807 + vertex -6.4289 7.92708 45.6999 + vertex -5.66945 7.71053 45.4235 + endloop + endfacet + facet normal -0.235416 -0.965676 0.10977 + outer loop + vertex -5.22499 7.71053 46.3767 + vertex -5.9249 7.92708 46.7807 + vertex -5.66945 7.71053 45.4235 + endloop + endfacet + facet normal -0.0792858 -0.996166 0.0369717 + outer loop + vertex -6.64674 8 47.1975 + vertex -7.21214 8 45.985 + vertex -6.4289 7.92708 45.6999 + endloop + endfacet + facet normal -0.0792842 -0.996166 0.0369719 + outer loop + vertex -5.9249 7.92708 46.7807 + vertex -6.64674 8 47.1975 + vertex -6.4289 7.92708 45.6999 + endloop + endfacet + facet normal -0.816035 -0.0868606 0.571438 + outer loop + vertex -2.55298 4.03351 44.834 + vertex -2.48982 3.20001 44.7975 + vertex -2.25824 4.03351 45.2549 + endloop + endfacet + facet normal -0.816054 -0.0868425 0.571414 + outer loop + vertex -2.25824 4.03351 45.2549 + vertex -2.48982 3.20001 44.7975 + vertex -2.20238 3.2 45.208 + endloop + endfacet + facet normal -0.79148 -0.257851 0.554141 + outer loop + vertex -2.42413 4.8417 45.3941 + vertex -2.74052 4.8417 44.9422 + vertex -2.55298 4.03351 44.834 + endloop + endfacet + facet normal -0.791421 -0.257902 0.554202 + outer loop + vertex -2.42413 4.8417 45.3941 + vertex -2.55298 4.03351 44.834 + vertex -2.25824 4.03351 45.2549 + endloop + endfacet + facet normal -0.742941 -0.421291 0.520147 + outer loop + vertex -2.695 5.6 45.6214 + vertex -3.04674 5.6 45.119 + vertex -2.74052 4.8417 44.9422 + endloop + endfacet + facet normal -0.742934 -0.421297 0.520152 + outer loop + vertex -2.695 5.6 45.6214 + vertex -2.74052 4.8417 44.9422 + vertex -2.42413 4.8417 45.3941 + endloop + endfacet + facet normal -0.67185 -0.572136 0.470401 + outer loop + vertex -3.06264 6.28538 45.9299 + vertex -3.46236 6.28538 45.359 + vertex -3.04674 5.6 45.119 + endloop + endfacet + facet normal -0.67187 -0.572123 0.470389 + outer loop + vertex -2.695 5.6 45.6214 + vertex -3.06264 6.28538 45.9299 + vertex -3.04674 5.6 45.119 + endloop + endfacet + facet normal -0.580348 -0.705755 0.406332 + outer loop + vertex -3.51585 6.87701 46.3102 + vertex -3.97473 6.87701 45.6548 + vertex -3.46236 6.28538 45.359 + endloop + endfacet + facet normal -0.580345 -0.705757 0.406333 + outer loop + vertex -3.06264 6.28538 45.9299 + vertex -3.51585 6.87701 46.3102 + vertex -3.46236 6.28538 45.359 + endloop + endfacet + facet normal -0.471048 -0.818122 0.329834 + outer loop + vertex -4.04088 7.35692 46.7507 + vertex -4.56828 7.35692 45.9975 + vertex -3.97473 6.87701 45.6548 + endloop + endfacet + facet normal -0.471077 -0.818108 0.329826 + outer loop + vertex -3.51585 6.87701 46.3102 + vertex -4.04088 7.35692 46.7507 + vertex -3.97473 6.87701 45.6548 + endloop + endfacet + facet normal -0.34726 -0.905691 0.243175 + outer loop + vertex -4.62178 7.71053 47.2381 + vertex -5.22499 7.71053 46.3767 + vertex -4.56828 7.35692 45.9975 + endloop + endfacet + facet normal -0.347283 -0.905683 0.243172 + outer loop + vertex -4.04088 7.35692 46.7507 + vertex -4.62178 7.71053 47.2381 + vertex -4.56828 7.35692 45.9975 + endloop + endfacet + facet normal -0.212779 -0.965675 0.148987 + outer loop + vertex -5.24088 7.92708 47.7576 + vertex -5.9249 7.92708 46.7807 + vertex -5.22499 7.71053 46.3767 + endloop + endfacet + facet normal -0.212758 -0.965679 0.148988 + outer loop + vertex -4.62178 7.71053 47.2381 + vertex -5.24088 7.92708 47.7576 + vertex -5.22499 7.71053 46.3767 + endloop + endfacet + facet normal -0.0716599 -0.996166 0.0501763 + outer loop + vertex -5.87939 8 48.2934 + vertex -6.64674 8 47.1975 + vertex -5.9249 7.92708 46.7807 + endloop + endfacet + facet normal -0.0716605 -0.996166 0.0501763 + outer loop + vertex -5.87939 8 48.2934 + vertex -5.9249 7.92708 46.7807 + vertex -5.24088 7.92708 47.7576 + endloop + endfacet + facet normal -0.704465 -0.0868472 0.704405 + outer loop + vertex -1.84801 3.2 45.5624 + vertex -2.25824 4.03351 45.2549 + vertex -2.20238 3.2 45.208 + endloop + endfacet + facet normal -0.704391 -0.0867803 0.704488 + outer loop + vertex -2.25824 4.03351 45.2549 + vertex -1.84801 3.2 45.5624 + vertex -1.89489 4.03351 45.6182 + endloop + endfacet + facet normal -0.68314 -0.2579 0.683234 + outer loop + vertex -1.89489 4.03351 45.6182 + vertex -2.42413 4.8417 45.3941 + vertex -2.25824 4.03351 45.2549 + endloop + endfacet + facet normal -0.68315 -0.25791 0.68322 + outer loop + vertex -2.42413 4.8417 45.3941 + vertex -1.89489 4.03351 45.6182 + vertex -2.03409 4.8417 45.7841 + endloop + endfacet + facet normal -0.641258 -0.421298 0.641324 + outer loop + vertex -2.03409 4.8417 45.7841 + vertex -2.695 5.6 45.6214 + vertex -2.42413 4.8417 45.3941 + endloop + endfacet + facet normal -0.641271 -0.421314 0.6413 + outer loop + vertex -2.695 5.6 45.6214 + vertex -2.03409 4.8417 45.7841 + vertex -2.26138 5.6 46.055 + endloop + endfacet + facet normal -0.579948 -0.572093 0.579975 + outer loop + vertex -2.56986 6.28538 46.4226 + vertex -2.695 5.6 45.6214 + vertex -2.26138 5.6 46.055 + endloop + endfacet + facet normal -0.579899 -0.572123 0.579993 + outer loop + vertex -2.695 5.6 45.6214 + vertex -2.56986 6.28538 46.4226 + vertex -3.06264 6.28538 45.9299 + endloop + endfacet + facet normal -0.500927 -0.705793 0.500927 + outer loop + vertex -3.51585 6.87701 46.3102 + vertex -2.56986 6.28538 46.4226 + vertex -2.95015 6.87701 46.8759 + endloop + endfacet + facet normal -0.500913 -0.705756 0.500994 + outer loop + vertex -2.56986 6.28538 46.4226 + vertex -3.51585 6.87701 46.3102 + vertex -3.06264 6.28538 45.9299 + endloop + endfacet + facet normal -0.406635 -0.818111 0.406622 + outer loop + vertex -4.04088 7.35692 46.7507 + vertex -2.95015 6.87701 46.8759 + vertex -3.3907 7.35692 47.4009 + endloop + endfacet + facet normal -0.406634 -0.818106 0.406634 + outer loop + vertex -2.95015 6.87701 46.8759 + vertex -4.04088 7.35692 46.7507 + vertex -3.51585 6.87701 46.3102 + endloop + endfacet + facet normal -0.299787 -0.905686 0.299767 + outer loop + vertex -4.62178 7.71053 47.2381 + vertex -3.3907 7.35692 47.4009 + vertex -3.87813 7.71053 47.9818 + endloop + endfacet + facet normal -0.299787 -0.905682 0.299778 + outer loop + vertex -3.3907 7.35692 47.4009 + vertex -4.62178 7.71053 47.2381 + vertex -4.04088 7.35692 46.7507 + endloop + endfacet + facet normal -0.183668 -0.965679 0.18366 + outer loop + vertex -5.24088 7.92708 47.7576 + vertex -3.87813 7.71053 47.9818 + vertex -4.39762 7.92708 48.6009 + endloop + endfacet + facet normal -0.183668 -0.965679 0.183656 + outer loop + vertex -3.87813 7.71053 47.9818 + vertex -5.24088 7.92708 47.7576 + vertex -4.62178 7.71053 47.2381 + endloop + endfacet + facet normal -0.0618601 -0.996166 0.0618601 + outer loop + vertex -5.87939 8 48.2934 + vertex -4.39762 7.92708 48.6009 + vertex -4.93339 8 49.2394 + endloop + endfacet + facet normal -0.0618593 -0.996166 0.0618564 + outer loop + vertex -4.39762 7.92708 48.6009 + vertex -5.87939 8 48.2934 + vertex -5.24088 7.92708 47.7576 + endloop + endfacet + facet normal -0.571493 -0.0867713 0.816006 + outer loop + vertex -1.47396 4.03351 45.913 + vertex -1.89489 4.03351 45.6182 + vertex -1.84801 3.2 45.5624 + endloop + endfacet + facet normal -0.571352 -0.086872 0.816095 + outer loop + vertex -1.47396 4.03351 45.913 + vertex -1.84801 3.2 45.5624 + vertex -1.4375 3.2 45.8498 + endloop + endfacet + facet normal -0.554185 -0.257911 0.79143 + outer loop + vertex -1.58224 4.8417 46.1005 + vertex -2.03409 4.8417 45.7841 + vertex -1.89489 4.03351 45.6182 + endloop + endfacet + facet normal -0.554257 -0.257862 0.791395 + outer loop + vertex -1.58224 4.8417 46.1005 + vertex -1.89489 4.03351 45.6182 + vertex -1.47396 4.03351 45.913 + endloop + endfacet + facet normal -0.520143 -0.421315 0.74293 + outer loop + vertex -1.75904 5.6 46.4067 + vertex -2.26138 5.6 46.055 + vertex -2.03409 4.8417 45.7841 + endloop + endfacet + facet normal -0.520208 -0.421273 0.742908 + outer loop + vertex -1.75904 5.6 46.4067 + vertex -2.03409 4.8417 45.7841 + vertex -1.58224 4.8417 46.1005 + endloop + endfacet + facet normal -0.470499 -0.572092 0.67182 + outer loop + vertex -1.99899 6.28538 46.8224 + vertex -2.56986 6.28538 46.4226 + vertex -2.26138 5.6 46.055 + endloop + endfacet + facet normal -0.470374 -0.572167 0.671844 + outer loop + vertex -1.99899 6.28538 46.8224 + vertex -2.26138 5.6 46.055 + vertex -1.75904 5.6 46.4067 + endloop + endfacet + facet normal -0.406286 -0.705796 0.58033 + outer loop + vertex -2.29481 6.87701 47.3347 + vertex -2.95015 6.87701 46.8759 + vertex -2.56986 6.28538 46.4226 + endloop + endfacet + facet normal -0.406421 -0.705724 0.580324 + outer loop + vertex -2.29481 6.87701 47.3347 + vertex -2.56986 6.28538 46.4226 + vertex -1.99899 6.28538 46.8224 + endloop + endfacet + facet normal -0.329843 -0.818111 0.471061 + outer loop + vertex -2.6375 7.35692 47.9283 + vertex -3.3907 7.35692 47.4009 + vertex -2.95015 6.87701 46.8759 + endloop + endfacet + facet normal -0.329784 -0.818138 0.471056 + outer loop + vertex -2.6375 7.35692 47.9283 + vertex -2.95015 6.87701 46.8759 + vertex -2.29481 6.87701 47.3347 + endloop + endfacet + facet normal -0.243162 -0.905687 0.34728 + outer loop + vertex -3.01665 7.71053 48.585 + vertex -3.87813 7.71053 47.9818 + vertex -3.3907 7.35692 47.4009 + endloop + endfacet + facet normal -0.243171 -0.905684 0.347282 + outer loop + vertex -3.01665 7.71053 48.585 + vertex -3.3907 7.35692 47.4009 + vertex -2.6375 7.35692 47.9283 + endloop + endfacet + facet normal -0.148978 -0.965679 0.212769 + outer loop + vertex -3.42074 7.92708 49.2849 + vertex -4.39762 7.92708 48.6009 + vertex -3.87813 7.71053 47.9818 + endloop + endfacet + facet normal -0.148979 -0.965678 0.212769 + outer loop + vertex -3.42074 7.92708 49.2849 + vertex -3.87813 7.71053 47.9818 + vertex -3.01665 7.71053 48.585 + endloop + endfacet + facet normal -0.0501763 -0.996166 0.071664 + outer loop + vertex -3.8375 8 50.0067 + vertex -4.93339 8 49.2394 + vertex -4.39762 7.92708 48.6009 + endloop + endfacet + facet normal -0.050179 -0.996166 0.071665 + outer loop + vertex -3.8375 8 50.0067 + vertex -4.39762 7.92708 48.6009 + vertex -3.42074 7.92708 49.2849 + endloop + endfacet + facet normal -0.420918 -0.0868757 0.902929 + outer loop + vertex -1.00825 4.03351 46.1301 + vertex -1.47396 4.03351 45.913 + vertex -1.4375 3.2 45.8498 + endloop + endfacet + facet normal -0.421034 -0.0868002 0.902882 + outer loop + vertex -1.00825 4.03351 46.1301 + vertex -1.4375 3.2 45.8498 + vertex -0.983308 3.2 46.0616 + endloop + endfacet + facet normal -0.408296 -0.257859 0.875673 + outer loop + vertex -1.08231 4.8417 46.3336 + vertex -1.58224 4.8417 46.1005 + vertex -1.47396 4.03351 45.913 + endloop + endfacet + facet normal -0.408222 -0.257905 0.875694 + outer loop + vertex -1.08231 4.8417 46.3336 + vertex -1.47396 4.03351 45.913 + vertex -1.00825 4.03351 46.1301 + endloop + endfacet + facet normal -0.383324 -0.421273 0.821944 + outer loop + vertex -1.20325 5.6 46.6659 + vertex -1.75904 5.6 46.4067 + vertex -1.58224 4.8417 46.1005 + endloop + endfacet + facet normal -0.383249 -0.421319 0.821955 + outer loop + vertex -1.20325 5.6 46.6659 + vertex -1.58224 4.8417 46.1005 + vertex -1.08231 4.8417 46.3336 + endloop + endfacet + facet normal -0.346585 -0.572171 0.743303 + outer loop + vertex -1.36739 6.28538 47.1169 + vertex -1.99899 6.28538 46.8224 + vertex -1.75904 5.6 46.4067 + endloop + endfacet + facet normal -0.346649 -0.572133 0.743302 + outer loop + vertex -1.36739 6.28538 47.1169 + vertex -1.75904 5.6 46.4067 + vertex -1.20325 5.6 46.6659 + endloop + endfacet + facet normal -0.299416 -0.705722 0.642111 + outer loop + vertex -1.56974 6.87701 47.6728 + vertex -2.29481 6.87701 47.3347 + vertex -1.99899 6.28538 46.8224 + endloop + endfacet + facet normal -0.2994 -0.705731 0.642109 + outer loop + vertex -1.56974 6.87701 47.6728 + vertex -1.99899 6.28538 46.8224 + vertex -1.36739 6.28538 47.1169 + endloop + endfacet + facet normal -0.243019 -0.818137 0.521146 + outer loop + vertex -1.80416 7.35692 48.3169 + vertex -2.6375 7.35692 47.9283 + vertex -2.29481 6.87701 47.3347 + endloop + endfacet + facet normal -0.243009 -0.818142 0.521143 + outer loop + vertex -1.80416 7.35692 48.3169 + vertex -2.29481 6.87701 47.3347 + vertex -1.56974 6.87701 47.6728 + endloop + endfacet + facet normal -0.179153 -0.905683 0.384243 + outer loop + vertex -2.06351 7.71053 49.0294 + vertex -3.01665 7.71053 48.585 + vertex -2.6375 7.35692 47.9283 + endloop + endfacet + facet normal -0.179185 -0.905671 0.384256 + outer loop + vertex -2.06351 7.71053 49.0294 + vertex -2.6375 7.35692 47.9283 + vertex -1.80416 7.35692 48.3169 + endloop + endfacet + facet normal -0.109773 -0.965678 0.235404 + outer loop + vertex -2.33993 7.92708 49.7889 + vertex -3.42074 7.92708 49.2849 + vertex -3.01665 7.71053 48.585 + endloop + endfacet + facet normal -0.109752 -0.965683 0.235393 + outer loop + vertex -2.33993 7.92708 49.7889 + vertex -3.01665 7.71053 48.585 + vertex -2.06351 7.71053 49.0294 + endloop + endfacet + facet normal -0.0369735 -0.996166 0.0792897 + outer loop + vertex -2.625 8 50.5721 + vertex -3.8375 8 50.0067 + vertex -3.42074 7.92708 49.2849 + endloop + endfacet + facet normal -0.0369744 -0.996166 0.0792902 + outer loop + vertex -2.625 8 50.5721 + vertex -3.42074 7.92708 49.2849 + vertex -2.33993 7.92708 49.7889 + endloop + endfacet + facet normal -0.257849 -0.0867984 0.962279 + outer loop + vertex -0.511901 4.03351 46.2631 + vertex -1.00825 4.03351 46.1301 + vertex -0.983308 3.2 46.0616 + endloop + endfacet + facet normal -0.257831 -0.0868097 0.962282 + outer loop + vertex -0.511901 4.03351 46.2631 + vertex -0.983308 3.2 46.0616 + vertex -0.499238 3.2 46.1913 + endloop + endfacet + facet normal -0.250121 -0.257906 0.933233 + outer loop + vertex -0.549505 4.8417 46.4764 + vertex -1.08231 4.8417 46.3336 + vertex -1.00825 4.03351 46.1301 + endloop + endfacet + facet normal -0.250067 -0.257938 0.933238 + outer loop + vertex -0.549505 4.8417 46.4764 + vertex -1.00825 4.03351 46.1301 + vertex -0.511901 4.03351 46.2631 + endloop + endfacet + facet normal -0.234702 -0.421318 0.876017 + outer loop + vertex -0.610908 5.6 46.8246 + vertex -1.20325 5.6 46.6659 + vertex -1.08231 4.8417 46.3336 + endloop + endfacet + facet normal -0.234787 -0.421267 0.876019 + outer loop + vertex -0.610908 5.6 46.8246 + vertex -1.08231 4.8417 46.3336 + vertex -0.549505 4.8417 46.4764 + endloop + endfacet + facet normal -0.212197 -0.572132 0.792235 + outer loop + vertex -0.694242 6.28538 47.2972 + vertex -1.36739 6.28538 47.1169 + vertex -1.20325 5.6 46.6659 + endloop + endfacet + facet normal -0.212258 -0.572096 0.792245 + outer loop + vertex -0.694242 6.28538 47.2972 + vertex -1.20325 5.6 46.6659 + vertex -0.610908 5.6 46.8246 + endloop + endfacet + facet normal -0.183401 -0.70573 0.684331 + outer loop + vertex -0.796979 6.87701 47.8799 + vertex -1.56974 6.87701 47.6728 + vertex -1.36739 6.28538 47.1169 + endloop + endfacet + facet normal -0.183286 -0.705795 0.684296 + outer loop + vertex -0.796979 6.87701 47.8799 + vertex -1.36739 6.28538 47.1169 + vertex -0.694242 6.28538 47.2972 + endloop + endfacet + facet normal -0.148834 -0.818143 0.555419 + outer loop + vertex -0.915994 7.35692 48.5549 + vertex -1.80416 7.35692 48.3169 + vertex -1.56974 6.87701 47.6728 + endloop + endfacet + facet normal -0.148855 -0.818133 0.555428 + outer loop + vertex -0.915994 7.35692 48.5549 + vertex -1.56974 6.87701 47.6728 + vertex -0.796979 6.87701 47.8799 + endloop + endfacet + facet normal -0.109737 -0.90567 0.409535 + outer loop + vertex -1.04767 7.71053 49.3016 + vertex -2.06351 7.71053 49.0294 + vertex -1.80416 7.35692 48.3169 + endloop + endfacet + facet normal -0.109743 -0.905668 0.409539 + outer loop + vertex -1.04767 7.71053 49.3016 + vertex -1.80416 7.35692 48.3169 + vertex -0.915994 7.35692 48.5549 + endloop + endfacet + facet normal -0.0672099 -0.965683 0.250876 + outer loop + vertex -1.18801 7.92708 50.0975 + vertex -2.33993 7.92708 49.7889 + vertex -2.06351 7.71053 49.0294 + endloop + endfacet + facet normal -0.0672273 -0.965678 0.25089 + outer loop + vertex -1.18801 7.92708 50.0975 + vertex -2.06351 7.71053 49.0294 + vertex -1.04767 7.71053 49.3016 + endloop + endfacet + facet normal -0.022646 -0.996166 0.0845055 + outer loop + vertex -1.33275 8 50.9184 + vertex -2.625 8 50.5721 + vertex -2.33993 7.92708 49.7889 + endloop + endfacet + facet normal -0.0226369 -0.996167 0.0844975 + outer loop + vertex -1.33275 8 50.9184 + vertex -2.33993 7.92708 49.7889 + vertex -1.18801 7.92708 50.0975 + endloop + endfacet + facet normal -0.0868546 -0.0868093 0.992432 + outer loop + vertex 0 4.03351 46.3079 + vertex -0.511901 4.03351 46.2631 + vertex -0.499238 3.2 46.1913 + endloop + endfacet + facet normal -0.0868709 -0.0867995 0.992431 + outer loop + vertex 0 4.03351 46.3079 + vertex -0.499238 3.2 46.1913 + vertex 0 3.2 46.235 + endloop + endfacet + facet normal -0.0842491 -0.257941 0.96248 + outer loop + vertex 0 4.8417 46.5245 + vertex -0.549505 4.8417 46.4764 + vertex -0.511901 4.03351 46.2631 + endloop + endfacet + facet normal -0.0842332 -0.25795 0.962479 + outer loop + vertex 0 4.8417 46.5245 + vertex -0.511901 4.03351 46.2631 + vertex 0 4.03351 46.3079 + endloop + endfacet + facet normal -0.0791216 -0.42127 0.903477 + outer loop + vertex 0 5.6 46.8781 + vertex -0.610908 5.6 46.8246 + vertex -0.549505 4.8417 46.4764 + endloop + endfacet + facet normal -0.0790837 -0.421294 0.90347 + outer loop + vertex 0 5.6 46.8781 + vertex -0.549505 4.8417 46.4764 + vertex 0 4.8417 46.5245 + endloop + endfacet + facet normal -0.0715559 -0.572098 0.817058 + outer loop + vertex 0 6.28538 47.358 + vertex -0.694242 6.28538 47.2972 + vertex -0.610908 5.6 46.8246 + endloop + endfacet + facet normal -0.0715534 -0.5721 0.817057 + outer loop + vertex 0 6.28538 47.358 + vertex -0.610908 5.6 46.8246 + vertex 0 5.6 46.8781 + endloop + endfacet + facet normal -0.0617194 -0.705791 0.705726 + outer loop + vertex 0 6.87701 47.9496 + vertex -0.796979 6.87701 47.8799 + vertex -0.694242 6.28538 47.2972 + endloop + endfacet + facet normal -0.0618098 -0.705737 0.705773 + outer loop + vertex 0 6.87701 47.9496 + vertex -0.694242 6.28538 47.2972 + vertex 0 6.28538 47.358 + endloop + endfacet + facet normal -0.0500928 -0.818133 0.572842 + outer loop + vertex 0 7.35692 48.635 + vertex -0.915994 7.35692 48.5549 + vertex -0.796979 6.87701 47.8799 + endloop + endfacet + facet normal -0.0500984 -0.81813 0.572846 + outer loop + vertex 0 7.35692 48.635 + vertex -0.796979 6.87701 47.8799 + vertex 0 6.87701 47.9496 + endloop + endfacet + facet normal -0.0369692 -0.905668 0.422372 + outer loop + vertex 0 7.71053 49.3933 + vertex -1.04767 7.71053 49.3016 + vertex -0.915994 7.35692 48.5549 + endloop + endfacet + facet normal -0.0369318 -0.905685 0.422339 + outer loop + vertex 0 7.71053 49.3933 + vertex -0.915994 7.35692 48.5549 + vertex 0 7.35692 48.635 + endloop + endfacet + facet normal -0.0226513 -0.965679 0.25875 + outer loop + vertex 0 7.92708 50.2015 + vertex -1.18801 7.92708 50.0975 + vertex -1.04767 7.71053 49.3016 + endloop + endfacet + facet normal -0.0226474 -0.96568 0.258745 + outer loop + vertex 0 7.92708 50.2015 + vertex -1.04767 7.71053 49.3016 + vertex 0 7.71053 49.3933 + endloop + endfacet + facet normal -0.00762412 -0.996167 0.0871445 + outer loop + vertex 0 8 51.035 + vertex -1.33275 8 50.9184 + vertex -1.18801 7.92708 50.0975 + endloop + endfacet + facet normal -0.00762932 -0.996166 0.0871511 + outer loop + vertex 0 8 51.035 + vertex -1.18801 7.92708 50.0975 + vertex 0 7.92708 50.2015 + endloop + endfacet + facet normal 0.0868546 -0.0867996 0.992432 + outer loop + vertex 0 4.03351 46.3079 + vertex 0 3.2 46.235 + vertex 0.511901 4.03351 46.2631 + endloop + endfacet + facet normal 0.0868708 -0.0868094 0.99243 + outer loop + vertex 0.511901 4.03351 46.2631 + vertex 0 3.2 46.235 + vertex 0.499238 3.2 46.1913 + endloop + endfacet + facet normal 0.0842489 -0.25795 0.962478 + outer loop + vertex 0 4.8417 46.5245 + vertex 0 4.03351 46.3079 + vertex 0.549505 4.8417 46.4764 + endloop + endfacet + facet normal 0.0842335 -0.25794 0.962482 + outer loop + vertex 0.549505 4.8417 46.4764 + vertex 0 4.03351 46.3079 + vertex 0.511901 4.03351 46.2631 + endloop + endfacet + facet normal 0.0791207 -0.421292 0.903467 + outer loop + vertex 0 5.6 46.8781 + vertex 0 4.8417 46.5245 + vertex 0.610908 5.6 46.8246 + endloop + endfacet + facet normal 0.0790847 -0.421269 0.903481 + outer loop + vertex 0.610908 5.6 46.8246 + vertex 0 4.8417 46.5245 + vertex 0.549505 4.8417 46.4764 + endloop + endfacet + facet normal 0.0715558 -0.5721 0.817057 + outer loop + vertex 0 6.28538 47.358 + vertex 0 5.6 46.8781 + vertex 0.694242 6.28538 47.2972 + endloop + endfacet + facet normal 0.0715535 -0.572098 0.817058 + outer loop + vertex 0.694242 6.28538 47.2972 + vertex 0 5.6 46.8781 + vertex 0.610908 5.6 46.8246 + endloop + endfacet + facet normal 0.0617239 -0.705741 0.705776 + outer loop + vertex 0 6.87701 47.9496 + vertex 0 6.28538 47.358 + vertex 0.796979 6.87701 47.8799 + endloop + endfacet + facet normal 0.0618048 -0.705795 0.705715 + outer loop + vertex 0.796979 6.87701 47.8799 + vertex 0 6.28538 47.358 + vertex 0.694242 6.28538 47.2972 + endloop + endfacet + facet normal 0.0500931 -0.818131 0.572847 + outer loop + vertex 0 7.35692 48.635 + vertex 0 6.87701 47.9496 + vertex 0.915994 7.35692 48.5549 + endloop + endfacet + facet normal 0.050098 -0.818134 0.572842 + outer loop + vertex 0.915994 7.35692 48.5549 + vertex 0 6.87701 47.9496 + vertex 0.796979 6.87701 47.8799 + endloop + endfacet + facet normal 0.0369662 -0.905684 0.422338 + outer loop + vertex 0 7.71053 49.3933 + vertex 0 7.35692 48.635 + vertex 1.04767 7.71053 49.3016 + endloop + endfacet + facet normal 0.0369352 -0.905667 0.422378 + outer loop + vertex 1.04767 7.71053 49.3016 + vertex 0 7.35692 48.635 + vertex 0.915994 7.35692 48.5549 + endloop + endfacet + facet normal 0.0226509 -0.96568 0.258745 + outer loop + vertex 0 7.92708 50.2015 + vertex 0 7.71053 49.3933 + vertex 1.18801 7.92708 50.0975 + endloop + endfacet + facet normal 0.0226478 -0.965679 0.25875 + outer loop + vertex 1.18801 7.92708 50.0975 + vertex 0 7.71053 49.3933 + vertex 1.04767 7.71053 49.3016 + endloop + endfacet + facet normal 0.0076247 -0.996166 0.0871511 + outer loop + vertex 0 8 51.035 + vertex 0 7.92708 50.2015 + vertex 1.33275 8 50.9184 + endloop + endfacet + facet normal 0.00762868 -0.996167 0.0871437 + outer loop + vertex 1.33275 8 50.9184 + vertex 0 7.92708 50.2015 + vertex 1.18801 7.92708 50.0975 + endloop + endfacet + facet normal 0.257849 -0.0868096 0.962278 + outer loop + vertex 0.511901 4.03351 46.2631 + vertex 0.499238 3.2 46.1913 + vertex 1.00825 4.03351 46.1301 + endloop + endfacet + facet normal 0.257831 -0.0867983 0.962283 + outer loop + vertex 1.00825 4.03351 46.1301 + vertex 0.499238 3.2 46.1913 + vertex 0.983308 3.2 46.0616 + endloop + endfacet + facet normal 0.250119 -0.257937 0.933225 + outer loop + vertex 0.549505 4.8417 46.4764 + vertex 0.511901 4.03351 46.2631 + vertex 1.08231 4.8417 46.3336 + endloop + endfacet + facet normal 0.25007 -0.257905 0.933247 + outer loop + vertex 1.08231 4.8417 46.3336 + vertex 0.511901 4.03351 46.2631 + vertex 1.00825 4.03351 46.1301 + endloop + endfacet + facet normal 0.234708 -0.421269 0.876039 + outer loop + vertex 0.610908 5.6 46.8246 + vertex 0.549505 4.8417 46.4764 + vertex 1.20325 5.6 46.6659 + endloop + endfacet + facet normal 0.23478 -0.421321 0.875995 + outer loop + vertex 1.20325 5.6 46.6659 + vertex 0.549505 4.8417 46.4764 + vertex 1.08231 4.8417 46.3336 + endloop + endfacet + facet normal 0.212203 -0.572099 0.792258 + outer loop + vertex 0.694242 6.28538 47.2972 + vertex 0.610908 5.6 46.8246 + vertex 1.36739 6.28538 47.1169 + endloop + endfacet + facet normal 0.212251 -0.572135 0.792219 + outer loop + vertex 1.36739 6.28538 47.1169 + vertex 0.610908 5.6 46.8246 + vertex 1.20325 5.6 46.6659 + endloop + endfacet + facet normal 0.183385 -0.70579 0.684273 + outer loop + vertex 0.796979 6.87701 47.8799 + vertex 0.694242 6.28538 47.2972 + vertex 1.56974 6.87701 47.6728 + endloop + endfacet + facet normal 0.183304 -0.705725 0.684362 + outer loop + vertex 1.56974 6.87701 47.6728 + vertex 0.694242 6.28538 47.2972 + vertex 1.36739 6.28538 47.1169 + endloop + endfacet + facet normal 0.148838 -0.818134 0.555432 + outer loop + vertex 0.915994 7.35692 48.5549 + vertex 0.796979 6.87701 47.8799 + vertex 1.80416 7.35692 48.3169 + endloop + endfacet + facet normal 0.148851 -0.818144 0.555413 + outer loop + vertex 1.80416 7.35692 48.3169 + vertex 0.796979 6.87701 47.8799 + vertex 1.56974 6.87701 47.6728 + endloop + endfacet + facet normal 0.109738 -0.905668 0.40954 + outer loop + vertex 1.04767 7.71053 49.3016 + vertex 0.915994 7.35692 48.5549 + vertex 2.06351 7.71053 49.0294 + endloop + endfacet + facet normal 0.109742 -0.905671 0.409533 + outer loop + vertex 2.06351 7.71053 49.0294 + vertex 0.915994 7.35692 48.5549 + vertex 1.80416 7.35692 48.3169 + endloop + endfacet + facet normal 0.0672141 -0.965679 0.250892 + outer loop + vertex 1.18801 7.92708 50.0975 + vertex 1.04767 7.71053 49.3016 + vertex 2.33993 7.92708 49.7889 + endloop + endfacet + facet normal 0.0672225 -0.965683 0.250872 + outer loop + vertex 2.33993 7.92708 49.7889 + vertex 1.04767 7.71053 49.3016 + vertex 2.06351 7.71053 49.0294 + endloop + endfacet + facet normal 0.0226435 -0.996166 0.0844963 + outer loop + vertex 1.33275 8 50.9184 + vertex 1.18801 7.92708 50.0975 + vertex 2.625 8 50.5721 + endloop + endfacet + facet normal 0.0226397 -0.996166 0.0845078 + outer loop + vertex 2.625 8 50.5721 + vertex 1.18801 7.92708 50.0975 + vertex 2.33993 7.92708 49.7889 + endloop + endfacet + facet normal 0.420921 -0.0868012 0.902935 + outer loop + vertex 1.00825 4.03351 46.1301 + vertex 0.983308 3.2 46.0616 + vertex 1.47396 4.03351 45.913 + endloop + endfacet + facet normal 0.421033 -0.0868777 0.902875 + outer loop + vertex 1.47396 4.03351 45.913 + vertex 0.983308 3.2 46.0616 + vertex 1.4375 3.20001 45.8498 + endloop + endfacet + facet normal 0.408291 -0.257904 0.875662 + outer loop + vertex 1.08231 4.8417 46.3336 + vertex 1.00825 4.03351 46.1301 + vertex 1.58224 4.8417 46.1005 + endloop + endfacet + facet normal 0.408228 -0.257857 0.875705 + outer loop + vertex 1.58224 4.8417 46.1005 + vertex 1.00825 4.03351 46.1301 + vertex 1.47396 4.03351 45.913 + endloop + endfacet + facet normal 0.383316 -0.421316 0.821926 + outer loop + vertex 1.20325 5.6 46.6659 + vertex 1.08231 4.8417 46.3336 + vertex 1.75904 5.6 46.4067 + endloop + endfacet + facet normal 0.383259 -0.42127 0.821976 + outer loop + vertex 1.75904 5.6 46.4067 + vertex 1.08231 4.8417 46.3336 + vertex 1.58224 4.8417 46.1005 + endloop + endfacet + facet normal 0.346595 -0.572135 0.743326 + outer loop + vertex 1.36739 6.28538 47.1169 + vertex 1.20325 5.6 46.6659 + vertex 1.99899 6.28538 46.8224 + endloop + endfacet + facet normal 0.346637 -0.572173 0.743277 + outer loop + vertex 1.99899 6.28538 46.8224 + vertex 1.20325 5.6 46.6659 + vertex 1.75904 5.6 46.4067 + endloop + endfacet + facet normal 0.299413 -0.70573 0.642103 + outer loop + vertex 1.56974 6.87701 47.6728 + vertex 1.36739 6.28538 47.1169 + vertex 2.29481 6.87701 47.3347 + endloop + endfacet + facet normal 0.299404 -0.705722 0.642117 + outer loop + vertex 2.29481 6.87701 47.3347 + vertex 1.36739 6.28538 47.1169 + vertex 1.99899 6.28538 46.8224 + endloop + endfacet + facet normal 0.243016 -0.818142 0.52114 + outer loop + vertex 1.80416 7.35692 48.3169 + vertex 1.56974 6.87701 47.6728 + vertex 2.6375 7.35692 47.9283 + endloop + endfacet + facet normal 0.243012 -0.818137 0.52115 + outer loop + vertex 2.6375 7.35692 47.9283 + vertex 1.56974 6.87701 47.6728 + vertex 2.29481 6.87701 47.3347 + endloop + endfacet + facet normal 0.179163 -0.905672 0.384265 + outer loop + vertex 2.06351 7.71053 49.0294 + vertex 1.80416 7.35692 48.3169 + vertex 3.01665 7.71053 48.585 + endloop + endfacet + facet normal 0.179174 -0.905684 0.384232 + outer loop + vertex 3.01665 7.71053 48.585 + vertex 1.80416 7.35692 48.3169 + vertex 2.6375 7.35692 47.9283 + endloop + endfacet + facet normal 0.109765 -0.965683 0.235388 + outer loop + vertex 2.33993 7.92708 49.7889 + vertex 2.06351 7.71053 49.0294 + vertex 3.42074 7.92708 49.2849 + endloop + endfacet + facet normal 0.10976 -0.965678 0.235412 + outer loop + vertex 3.42074 7.92708 49.2849 + vertex 2.06351 7.71053 49.0294 + vertex 3.01665 7.71053 48.585 + endloop + endfacet + facet normal 0.0369739 -0.996166 0.0792904 + outer loop + vertex 2.625 8 50.5721 + vertex 2.33993 7.92708 49.7889 + vertex 3.8375 8 50.0067 + endloop + endfacet + facet normal 0.036974 -0.996166 0.0792894 + outer loop + vertex 3.8375 8 50.0067 + vertex 2.33993 7.92708 49.7889 + vertex 3.42074 7.92708 49.2849 + endloop + endfacet + facet normal 0.571488 -0.0868717 0.815999 + outer loop + vertex 1.47396 4.03351 45.913 + vertex 1.4375 3.20001 45.8498 + vertex 1.89489 4.03351 45.6182 + endloop + endfacet + facet normal 0.571355 -0.08677 0.816103 + outer loop + vertex 1.89489 4.03351 45.6182 + vertex 1.4375 3.20001 45.8498 + vertex 1.84801 3.2 45.5624 + endloop + endfacet + facet normal 0.554192 -0.257864 0.79144 + outer loop + vertex 1.58224 4.8417 46.1005 + vertex 1.47396 4.03351 45.913 + vertex 2.03409 4.8417 45.7841 + endloop + endfacet + facet normal 0.554249 -0.257912 0.791384 + outer loop + vertex 2.03409 4.8417 45.7841 + vertex 1.47396 4.03351 45.913 + vertex 1.89489 4.03351 45.6182 + endloop + endfacet + facet normal 0.520153 -0.421275 0.742945 + outer loop + vertex 1.75904 5.6 46.4067 + vertex 1.58224 4.8417 46.1005 + vertex 2.26138 5.6 46.055 + endloop + endfacet + facet normal 0.520197 -0.421317 0.742891 + outer loop + vertex 2.26138 5.6 46.055 + vertex 1.58224 4.8417 46.1005 + vertex 2.03409 4.8417 45.7841 + endloop + endfacet + facet normal 0.470471 -0.572162 0.67178 + outer loop + vertex 1.99899 6.28538 46.8224 + vertex 1.75904 5.6 46.4067 + vertex 2.56986 6.28538 46.4226 + endloop + endfacet + facet normal 0.470405 -0.572087 0.671889 + outer loop + vertex 2.56986 6.28538 46.4226 + vertex 1.75904 5.6 46.4067 + vertex 2.26138 5.6 46.055 + endloop + endfacet + facet normal 0.406325 -0.705729 0.580385 + outer loop + vertex 2.29481 6.87701 47.3347 + vertex 1.99899 6.28538 46.8224 + vertex 2.95015 6.87701 46.8759 + endloop + endfacet + facet normal 0.406377 -0.705801 0.580261 + outer loop + vertex 2.95015 6.87701 46.8759 + vertex 1.99899 6.28538 46.8224 + vertex 2.56986 6.28538 46.4226 + endloop + endfacet + facet normal 0.329822 -0.818136 0.471032 + outer loop + vertex 2.6375 7.35692 47.9283 + vertex 2.29481 6.87701 47.3347 + vertex 3.3907 7.35692 47.4009 + endloop + endfacet + facet normal 0.329807 -0.818109 0.471089 + outer loop + vertex 3.3907 7.35692 47.4009 + vertex 2.29481 6.87701 47.3347 + vertex 2.95015 6.87701 46.8759 + endloop + endfacet + facet normal 0.243166 -0.905684 0.347286 + outer loop + vertex 3.01665 7.71053 48.585 + vertex 2.6375 7.35692 47.9283 + vertex 3.87813 7.71053 47.9818 + endloop + endfacet + facet normal 0.243167 -0.905687 0.347276 + outer loop + vertex 3.87813 7.71053 47.9818 + vertex 2.6375 7.35692 47.9283 + vertex 3.3907 7.35692 47.4009 + endloop + endfacet + facet normal 0.148978 -0.965678 0.212769 + outer loop + vertex 3.42074 7.92708 49.2849 + vertex 3.01665 7.71053 48.585 + vertex 4.39762 7.92708 48.6009 + endloop + endfacet + facet normal 0.148978 -0.965679 0.212768 + outer loop + vertex 4.39762 7.92708 48.6009 + vertex 3.01665 7.71053 48.585 + vertex 3.87813 7.71053 47.9818 + endloop + endfacet + facet normal 0.0501776 -0.996166 0.0716658 + outer loop + vertex 3.8375 8 50.0067 + vertex 3.42074 7.92708 49.2849 + vertex 4.93339 8 49.2394 + endloop + endfacet + facet normal 0.0501776 -0.996166 0.0716629 + outer loop + vertex 4.93339 8 49.2394 + vertex 3.42074 7.92708 49.2849 + vertex 4.39762 7.92708 48.6009 + endloop + endfacet + facet normal 0.704391 -0.0867803 0.704488 + outer loop + vertex 1.84801 3.2 45.5624 + vertex 2.25824 4.03351 45.2549 + vertex 1.89489 4.03351 45.6182 + endloop + endfacet + facet normal 0.704465 -0.0868472 0.704405 + outer loop + vertex 2.25824 4.03351 45.2549 + vertex 1.84801 3.2 45.5624 + vertex 2.20238 3.2 45.208 + endloop + endfacet + facet normal 0.68315 -0.25791 0.68322 + outer loop + vertex 1.89489 4.03351 45.6182 + vertex 2.42413 4.8417 45.3941 + vertex 2.03409 4.8417 45.7841 + endloop + endfacet + facet normal 0.68314 -0.2579 0.683234 + outer loop + vertex 2.42413 4.8417 45.3941 + vertex 1.89489 4.03351 45.6182 + vertex 2.25824 4.03351 45.2549 + endloop + endfacet + facet normal 0.641271 -0.421314 0.6413 + outer loop + vertex 2.03409 4.8417 45.7841 + vertex 2.695 5.6 45.6214 + vertex 2.26138 5.6 46.055 + endloop + endfacet + facet normal 0.641258 -0.421298 0.641324 + outer loop + vertex 2.695 5.6 45.6214 + vertex 2.03409 4.8417 45.7841 + vertex 2.42413 4.8417 45.3941 + endloop + endfacet + facet normal 0.579899 -0.572123 0.579993 + outer loop + vertex 2.56986 6.28538 46.4226 + vertex 2.695 5.6 45.6214 + vertex 3.06264 6.28538 45.9299 + endloop + endfacet + facet normal 0.579948 -0.572093 0.579975 + outer loop + vertex 2.695 5.6 45.6214 + vertex 2.56986 6.28538 46.4226 + vertex 2.26138 5.6 46.055 + endloop + endfacet + facet normal 0.500927 -0.705793 0.500927 + outer loop + vertex 2.56986 6.28538 46.4226 + vertex 3.51585 6.87701 46.3102 + vertex 2.95015 6.87701 46.8759 + endloop + endfacet + facet normal 0.500913 -0.705756 0.500994 + outer loop + vertex 3.51585 6.87701 46.3102 + vertex 2.56986 6.28538 46.4226 + vertex 3.06264 6.28538 45.9299 + endloop + endfacet + facet normal 0.406635 -0.818111 0.406622 + outer loop + vertex 2.95015 6.87701 46.8759 + vertex 4.04088 7.35692 46.7507 + vertex 3.3907 7.35692 47.4009 + endloop + endfacet + facet normal 0.406634 -0.818106 0.406634 + outer loop + vertex 4.04088 7.35692 46.7507 + vertex 2.95015 6.87701 46.8759 + vertex 3.51585 6.87701 46.3102 + endloop + endfacet + facet normal 0.299787 -0.905686 0.299767 + outer loop + vertex 3.3907 7.35692 47.4009 + vertex 4.62178 7.71053 47.2381 + vertex 3.87813 7.71053 47.9818 + endloop + endfacet + facet normal 0.299787 -0.905682 0.299778 + outer loop + vertex 4.62178 7.71053 47.2381 + vertex 3.3907 7.35692 47.4009 + vertex 4.04088 7.35692 46.7507 + endloop + endfacet + facet normal 0.183668 -0.965679 0.18366 + outer loop + vertex 3.87813 7.71053 47.9818 + vertex 5.24088 7.92708 47.7576 + vertex 4.39762 7.92708 48.6009 + endloop + endfacet + facet normal 0.183668 -0.965679 0.183656 + outer loop + vertex 5.24088 7.92708 47.7576 + vertex 3.87813 7.71053 47.9818 + vertex 4.62178 7.71053 47.2381 + endloop + endfacet + facet normal 0.0618601 -0.996166 0.0618601 + outer loop + vertex 4.39762 7.92708 48.6009 + vertex 5.87939 8 48.2934 + vertex 4.93339 8 49.2394 + endloop + endfacet + facet normal 0.0618593 -0.996166 0.0618564 + outer loop + vertex 5.87939 8 48.2934 + vertex 4.39762 7.92708 48.6009 + vertex 5.24088 7.92708 47.7576 + endloop + endfacet + facet normal 0.816037 -0.0868427 0.571439 + outer loop + vertex 2.25824 4.03351 45.2549 + vertex 2.20238 3.2 45.208 + vertex 2.55298 4.03351 44.834 + endloop + endfacet + facet normal 0.816052 -0.0868597 0.571415 + outer loop + vertex 2.55298 4.03351 44.834 + vertex 2.20238 3.2 45.208 + vertex 2.48982 3.2 44.7975 + endloop + endfacet + facet normal 0.791469 -0.2579 0.554134 + outer loop + vertex 2.42413 4.8417 45.3941 + vertex 2.25824 4.03351 45.2549 + vertex 2.74052 4.8417 44.9422 + endloop + endfacet + facet normal 0.791433 -0.257849 0.55421 + outer loop + vertex 2.74052 4.8417 44.9422 + vertex 2.25824 4.03351 45.2549 + vertex 2.55298 4.03351 44.834 + endloop + endfacet + facet normal 0.742939 -0.421296 0.520146 + outer loop + vertex 2.695 5.6 45.6214 + vertex 2.42413 4.8417 45.3941 + vertex 3.04674 5.6 45.119 + endloop + endfacet + facet normal 0.742936 -0.421291 0.520154 + outer loop + vertex 3.04674 5.6 45.119 + vertex 2.42413 4.8417 45.3941 + vertex 2.74052 4.8417 44.9422 + endloop + endfacet + facet normal 0.671858 -0.572124 0.470406 + outer loop + vertex 3.06264 6.28538 45.9299 + vertex 2.695 5.6 45.6214 + vertex 3.46236 6.28538 45.359 + endloop + endfacet + facet normal 0.671862 -0.572137 0.470384 + outer loop + vertex 3.46236 6.28538 45.359 + vertex 2.695 5.6 45.6214 + vertex 3.04674 5.6 45.119 + endloop + endfacet + facet normal 0.580347 -0.705757 0.406331 + outer loop + vertex 3.51585 6.87701 46.3102 + vertex 3.06264 6.28538 45.9299 + vertex 3.97473 6.87701 45.6548 + endloop + endfacet + facet normal 0.580347 -0.705755 0.406334 + outer loop + vertex 3.97473 6.87701 45.6548 + vertex 3.06264 6.28538 45.9299 + vertex 3.46236 6.28538 45.359 + endloop + endfacet + facet normal 0.471063 -0.818109 0.329844 + outer loop + vertex 4.04088 7.35692 46.7507 + vertex 3.51585 6.87701 46.3102 + vertex 4.56828 7.35692 45.9975 + endloop + endfacet + facet normal 0.47106 -0.818123 0.329814 + outer loop + vertex 4.56828 7.35692 45.9975 + vertex 3.51585 6.87701 46.3102 + vertex 3.97473 6.87701 45.6548 + endloop + endfacet + facet normal 0.347273 -0.905684 0.243184 + outer loop + vertex 4.62178 7.71053 47.2381 + vertex 4.04088 7.35692 46.7507 + vertex 5.22499 7.71053 46.3767 + endloop + endfacet + facet normal 0.347268 -0.905691 0.243162 + outer loop + vertex 5.22499 7.71053 46.3767 + vertex 4.04088 7.35692 46.7507 + vertex 4.56828 7.35692 45.9975 + endloop + endfacet + facet normal 0.212767 -0.965679 0.148978 + outer loop + vertex 5.24088 7.92708 47.7576 + vertex 4.62178 7.71053 47.2381 + vertex 5.9249 7.92708 46.7807 + endloop + endfacet + facet normal 0.212773 -0.965675 0.148998 + outer loop + vertex 5.9249 7.92708 46.7807 + vertex 4.62178 7.71053 47.2381 + vertex 5.22499 7.71053 46.3767 + endloop + endfacet + facet normal 0.0716603 -0.996166 0.0501766 + outer loop + vertex 5.87939 8 48.2934 + vertex 5.24088 7.92708 47.7576 + vertex 6.64674 8 47.1975 + endloop + endfacet + facet normal 0.0716601 -0.996166 0.050176 + outer loop + vertex 6.64674 8 47.1975 + vertex 5.24088 7.92708 47.7576 + vertex 5.9249 7.92708 46.7807 + endloop + endfacet + facet normal 0.902917 -0.086853 0.420948 + outer loop + vertex 2.55298 4.03351 44.834 + vertex 2.48982 3.2 44.7975 + vertex 2.77014 4.03351 44.3682 + endloop + endfacet + facet normal 0.902885 -0.0868007 0.421028 + outer loop + vertex 2.77014 4.03351 44.3682 + vertex 2.48982 3.2 44.7975 + vertex 2.70162 3.2 44.3433 + endloop + endfacet + facet normal 0.875656 -0.257862 0.40833 + outer loop + vertex 2.74052 4.8417 44.9422 + vertex 2.55298 4.03351 44.834 + vertex 2.97363 4.8417 44.4423 + endloop + endfacet + facet normal 0.875679 -0.257914 0.408249 + outer loop + vertex 2.97363 4.8417 44.4423 + vertex 2.55298 4.03351 44.834 + vertex 2.77014 4.03351 44.3682 + endloop + endfacet + facet normal 0.821929 -0.421291 0.383335 + outer loop + vertex 3.04674 5.6 45.119 + vertex 2.74052 4.8417 44.9422 + vertex 3.30591 5.6 44.5633 + endloop + endfacet + facet normal 0.821938 -0.421325 0.38328 + outer loop + vertex 3.30591 5.6 44.5633 + vertex 2.74052 4.8417 44.9422 + vertex 2.97363 4.8417 44.4423 + endloop + endfacet + facet normal 0.74332 -0.57213 0.346616 + outer loop + vertex 3.46236 6.28538 45.359 + vertex 3.04674 5.6 45.119 + vertex 3.75688 6.28538 44.7274 + endloop + endfacet + facet normal 0.743319 -0.572097 0.346673 + outer loop + vertex 3.75688 6.28538 44.7274 + vertex 3.04674 5.6 45.119 + vertex 3.30591 5.6 44.5633 + endloop + endfacet + facet normal 0.642086 -0.705755 0.299392 + outer loop + vertex 3.97473 6.87701 45.6548 + vertex 3.46236 6.28538 45.359 + vertex 4.31283 6.87701 44.9297 + endloop + endfacet + facet normal 0.642088 -0.705745 0.299411 + outer loop + vertex 4.31283 6.87701 44.9297 + vertex 3.46236 6.28538 45.359 + vertex 3.75688 6.28538 44.7274 + endloop + endfacet + facet normal 0.521162 -0.818122 0.243038 + outer loop + vertex 4.56828 7.35692 45.9975 + vertex 3.97473 6.87701 45.6548 + vertex 4.95688 7.35692 45.1642 + endloop + endfacet + facet normal 0.521152 -0.818138 0.243003 + outer loop + vertex 4.95688 7.35692 45.1642 + vertex 3.97473 6.87701 45.6548 + vertex 4.31283 6.87701 44.9297 + endloop + endfacet + facet normal 0.384225 -0.90569 0.179157 + outer loop + vertex 5.22499 7.71053 46.3767 + vertex 4.56828 7.35692 45.9975 + vertex 5.66945 7.71053 45.4235 + endloop + endfacet + facet normal 0.384236 -0.90568 0.179184 + outer loop + vertex 5.66945 7.71053 45.4235 + vertex 4.56828 7.35692 45.9975 + vertex 4.95688 7.35692 45.1642 + endloop + endfacet + facet normal 0.235412 -0.965676 0.109777 + outer loop + vertex 5.9249 7.92708 46.7807 + vertex 5.22499 7.71053 46.3767 + vertex 6.4289 7.92708 45.6999 + endloop + endfacet + facet normal 0.235405 -0.965679 0.109765 + outer loop + vertex 6.4289 7.92708 45.6999 + vertex 5.22499 7.71053 46.3767 + vertex 5.66945 7.71053 45.4235 + endloop + endfacet + facet normal 0.0792847 -0.996166 0.0369712 + outer loop + vertex 6.64674 8 47.1975 + vertex 5.9249 7.92708 46.7807 + vertex 7.21214 8 45.985 + endloop + endfacet + facet normal 0.0792855 -0.996166 0.0369725 + outer loop + vertex 7.21214 8 45.985 + vertex 5.9249 7.92708 46.7807 + vertex 6.4289 7.92708 45.6999 + endloop + endfacet + facet normal 0.962271 -0.0868086 0.257872 + outer loop + vertex 2.77014 4.03351 44.3682 + vertex 2.70162 3.2 44.3433 + vertex 2.90314 4.03351 43.8719 + endloop + endfacet + facet normal 0.962284 -0.0868441 0.257815 + outer loop + vertex 2.90314 4.03351 43.8719 + vertex 2.70162 3.2 44.3433 + vertex 2.83132 3.2 43.8592 + endloop + endfacet + facet normal 0.933246 -0.257906 0.250074 + outer loop + vertex 2.97363 4.8417 44.4423 + vertex 2.77014 4.03351 44.3682 + vertex 3.1164 4.8417 43.9095 + endloop + endfacet + facet normal 0.933244 -0.257894 0.250094 + outer loop + vertex 3.1164 4.8417 43.9095 + vertex 2.77014 4.03351 44.3682 + vertex 2.90314 4.03351 43.8719 + endloop + endfacet + facet normal 0.876017 -0.421314 0.234709 + outer loop + vertex 3.30591 5.6 44.5633 + vertex 2.97363 4.8417 44.4423 + vertex 3.46463 5.6 43.9709 + endloop + endfacet + facet normal 0.876018 -0.421296 0.234739 + outer loop + vertex 3.46463 5.6 43.9709 + vertex 2.97363 4.8417 44.4423 + vertex 3.1164 4.8417 43.9095 + endloop + endfacet + facet normal 0.792238 -0.572103 0.212264 + outer loop + vertex 3.75688 6.28538 44.7274 + vertex 3.30591 5.6 44.5633 + vertex 3.93725 6.28538 44.0542 + endloop + endfacet + facet normal 0.792238 -0.572105 0.212262 + outer loop + vertex 3.93725 6.28538 44.0542 + vertex 3.30591 5.6 44.5633 + vertex 3.46463 5.6 43.9709 + endloop + endfacet + facet normal 0.684317 -0.70575 0.183376 + outer loop + vertex 4.31283 6.87701 44.9297 + vertex 3.75688 6.28538 44.7274 + vertex 4.51989 6.87701 44.157 + endloop + endfacet + facet normal 0.684308 -0.705767 0.183346 + outer loop + vertex 4.51989 6.87701 44.157 + vertex 3.75688 6.28538 44.7274 + vertex 3.93725 6.28538 44.0542 + endloop + endfacet + facet normal 0.555439 -0.818131 0.148822 + outer loop + vertex 4.95688 7.35692 45.1642 + vertex 4.31283 6.87701 44.9297 + vertex 5.19486 7.35692 44.276 + endloop + endfacet + facet normal 0.55545 -0.818121 0.148844 + outer loop + vertex 5.19486 7.35692 44.276 + vertex 4.31283 6.87701 44.9297 + vertex 4.51989 6.87701 44.157 + endloop + endfacet + facet normal 0.409511 -0.905682 0.109731 + outer loop + vertex 5.66945 7.71053 45.4235 + vertex 4.95688 7.35692 45.1642 + vertex 5.94164 7.71053 44.4077 + endloop + endfacet + facet normal 0.409504 -0.905686 0.109721 + outer loop + vertex 5.94164 7.71053 44.4077 + vertex 4.95688 7.35692 45.1642 + vertex 5.19486 7.35692 44.276 + endloop + endfacet + facet normal 0.250888 -0.965679 0.0672251 + outer loop + vertex 6.4289 7.92708 45.6999 + vertex 5.66945 7.71053 45.4235 + vertex 6.73755 7.92708 44.548 + endloop + endfacet + facet normal 0.25089 -0.965678 0.0672275 + outer loop + vertex 6.73755 7.92708 44.548 + vertex 5.66945 7.71053 45.4235 + vertex 5.94164 7.71053 44.4077 + endloop + endfacet + facet normal 0.084502 -0.996166 0.0226415 + outer loop + vertex 7.21214 8 45.985 + vertex 6.4289 7.92708 45.6999 + vertex 7.5584 8 44.6927 + endloop + endfacet + facet normal 0.0845027 -0.996166 0.0226424 + outer loop + vertex 7.5584 8 44.6927 + vertex 6.4289 7.92708 45.6999 + vertex 6.73755 7.92708 44.548 + endloop + endfacet + facet normal 0.992433 -0.0868365 0.086816 + outer loop + vertex 2.90314 4.03351 43.8719 + vertex 2.83132 3.2 43.8592 + vertex 2.94792 4.03351 43.36 + endloop + endfacet + facet normal 0.992432 -0.0868233 0.0868378 + outer loop + vertex 2.94792 4.03351 43.36 + vertex 2.83132 3.2 43.8592 + vertex 2.875 3.2 43.36 + endloop + endfacet + facet normal 0.962497 -0.257895 0.0841988 + outer loop + vertex 3.1164 4.8417 43.9095 + vertex 2.90314 4.03351 43.8719 + vertex 3.16447 4.8417 43.36 + endloop + endfacet + facet normal 0.962497 -0.257896 0.0841973 + outer loop + vertex 3.16447 4.8417 43.36 + vertex 2.90314 4.03351 43.8719 + vertex 2.94792 4.03351 43.36 + endloop + endfacet + facet normal 0.903471 -0.421297 0.0790482 + outer loop + vertex 3.46463 5.6 43.9709 + vertex 3.1164 4.8417 43.9095 + vertex 3.51808 5.6 43.36 + endloop + endfacet + facet normal 0.903469 -0.421305 0.079035 + outer loop + vertex 3.51808 5.6 43.36 + vertex 3.1164 4.8417 43.9095 + vertex 3.16447 4.8417 43.36 + endloop + endfacet + facet normal 0.817056 -0.572109 0.0714895 + outer loop + vertex 3.93725 6.28538 44.0542 + vertex 3.46463 5.6 43.9709 + vertex 3.99799 6.28538 43.36 + endloop + endfacet + facet normal 0.817055 -0.57211 0.0714873 + outer loop + vertex 3.99799 6.28538 43.36 + vertex 3.46463 5.6 43.9709 + vertex 3.51808 5.6 43.36 + endloop + endfacet + facet normal 0.705755 -0.70576 0.0617469 + outer loop + vertex 4.51989 6.87701 44.157 + vertex 3.93725 6.28538 44.0542 + vertex 4.58962 6.87701 43.36 + endloop + endfacet + facet normal 0.705757 -0.705757 0.0617512 + outer loop + vertex 4.58962 6.87701 43.36 + vertex 3.93725 6.28538 44.0542 + vertex 3.99799 6.28538 43.36 + endloop + endfacet + facet normal 0.572856 -0.818122 0.0501187 + outer loop + vertex 5.19486 7.35692 44.276 + vertex 4.51989 6.87701 44.157 + vertex 5.275 7.35692 43.36 + endloop + endfacet + facet normal 0.572857 -0.818122 0.0501196 + outer loop + vertex 5.275 7.35692 43.36 + vertex 4.51989 6.87701 44.157 + vertex 4.58962 6.87701 43.36 + endloop + endfacet + facet normal 0.422337 -0.905685 0.036949 + outer loop + vertex 5.94164 7.71053 44.4077 + vertex 5.19486 7.35692 44.276 + vertex 6.0333 7.71053 43.36 + endloop + endfacet + facet normal 0.422338 -0.905685 0.03695 + outer loop + vertex 6.0333 7.71053 43.36 + vertex 5.19486 7.35692 44.276 + vertex 5.275 7.35692 43.36 + endloop + endfacet + facet normal 0.25875 -0.965679 0.0226384 + outer loop + vertex 6.73755 7.92708 44.548 + vertex 5.94164 7.71053 44.4077 + vertex 6.84149 7.92708 43.36 + endloop + endfacet + facet normal 0.258748 -0.965679 0.0226371 + outer loop + vertex 6.84149 7.92708 43.36 + vertex 5.94164 7.71053 44.4077 + vertex 6.0333 7.71053 43.36 + endloop + endfacet + facet normal 0.08715 -0.996166 0.00762489 + outer loop + vertex 7.5584 8 44.6927 + vertex 6.73755 7.92708 44.548 + vertex 7.675 8 43.36 + endloop + endfacet + facet normal 0.08715 -0.996166 0.00762489 + outer loop + vertex 7.675 8 43.36 + vertex 6.73755 7.92708 44.548 + vertex 6.84149 7.92708 43.36 + endloop + endfacet + facet normal 0.992434 -0.0868235 -0.0868161 + outer loop + vertex 2.94792 4.03351 43.36 + vertex 2.875 3.2 43.36 + vertex 2.90314 4.03351 42.8481 + endloop + endfacet + facet normal 0.992431 -0.0868366 -0.0868377 + outer loop + vertex 2.875 3.2 43.36 + vertex 2.83132 3.2 42.8608 + vertex 2.90314 4.03351 42.8481 + endloop + endfacet + facet normal 0.962497 -0.257896 -0.0841988 + outer loop + vertex 3.16447 4.8417 43.36 + vertex 2.94792 4.03351 43.36 + vertex 3.1164 4.8417 42.8105 + endloop + endfacet + facet normal 0.962497 -0.257895 -0.0841974 + outer loop + vertex 2.94792 4.03351 43.36 + vertex 2.90314 4.03351 42.8481 + vertex 3.1164 4.8417 42.8105 + endloop + endfacet + facet normal 0.903468 -0.421305 -0.0790479 + outer loop + vertex 3.51808 5.6 43.36 + vertex 3.16447 4.8417 43.36 + vertex 3.46463 5.6 42.7491 + endloop + endfacet + facet normal 0.903473 -0.421296 -0.0790354 + outer loop + vertex 3.16447 4.8417 43.36 + vertex 3.1164 4.8417 42.8105 + vertex 3.46463 5.6 42.7491 + endloop + endfacet + facet normal 0.817055 -0.57211 -0.0714894 + outer loop + vertex 3.99799 6.28538 43.36 + vertex 3.51808 5.6 43.36 + vertex 3.93725 6.28538 42.6658 + endloop + endfacet + facet normal 0.817056 -0.572109 -0.0714874 + outer loop + vertex 3.51808 5.6 43.36 + vertex 3.46463 5.6 42.7491 + vertex 3.93725 6.28538 42.6658 + endloop + endfacet + facet normal 0.705757 -0.705757 -0.0617471 + outer loop + vertex 4.58962 6.87701 43.36 + vertex 3.99799 6.28538 43.36 + vertex 4.51989 6.87701 42.563 + endloop + endfacet + facet normal 0.705755 -0.70576 -0.061751 + outer loop + vertex 3.99799 6.28538 43.36 + vertex 3.93725 6.28538 42.6658 + vertex 4.51989 6.87701 42.563 + endloop + endfacet + facet normal 0.572857 -0.818122 -0.0501187 + outer loop + vertex 5.275 7.35692 43.36 + vertex 4.58962 6.87701 43.36 + vertex 5.19486 7.35692 42.444 + endloop + endfacet + facet normal 0.572856 -0.818122 -0.0501195 + outer loop + vertex 4.58962 6.87701 43.36 + vertex 4.51989 6.87701 42.563 + vertex 5.19486 7.35692 42.444 + endloop + endfacet + facet normal 0.422338 -0.905685 -0.0369491 + outer loop + vertex 6.0333 7.71053 43.36 + vertex 5.275 7.35692 43.36 + vertex 5.94164 7.71053 42.3123 + endloop + endfacet + facet normal 0.422337 -0.905685 -0.0369499 + outer loop + vertex 5.275 7.35692 43.36 + vertex 5.19486 7.35692 42.444 + vertex 5.94164 7.71053 42.3123 + endloop + endfacet + facet normal 0.258748 -0.965679 -0.0226383 + outer loop + vertex 6.84149 7.92708 43.36 + vertex 6.0333 7.71053 43.36 + vertex 6.73755 7.92708 42.172 + endloop + endfacet + facet normal 0.25875 -0.965679 -0.0226372 + outer loop + vertex 6.0333 7.71053 43.36 + vertex 5.94164 7.71053 42.3123 + vertex 6.73755 7.92708 42.172 + endloop + endfacet + facet normal 0.08715 -0.996166 -0.00762489 + outer loop + vertex 7.675 8 43.36 + vertex 6.84149 7.92708 43.36 + vertex 7.5584 8 42.0273 + endloop + endfacet + facet normal 0.08715 -0.996166 -0.00762489 + outer loop + vertex 6.84149 7.92708 43.36 + vertex 6.73755 7.92708 42.172 + vertex 7.5584 8 42.0273 + endloop + endfacet + facet normal 0.962268 -0.0868437 -0.257872 + outer loop + vertex 2.83132 3.2 42.8608 + vertex 2.77014 4.03351 42.3518 + vertex 2.90314 4.03351 42.8481 + endloop + endfacet + facet normal 0.962287 -0.0868082 -0.257816 + outer loop + vertex 2.83132 3.2 42.8608 + vertex 2.70162 3.2 42.3767 + vertex 2.77014 4.03351 42.3518 + endloop + endfacet + facet normal 0.933249 -0.257894 -0.250075 + outer loop + vertex 3.1164 4.8417 42.8105 + vertex 2.90314 4.03351 42.8481 + vertex 2.97363 4.8417 42.2777 + endloop + endfacet + facet normal 0.933241 -0.257906 -0.250093 + outer loop + vertex 2.90314 4.03351 42.8481 + vertex 2.77014 4.03351 42.3518 + vertex 2.97363 4.8417 42.2777 + endloop + endfacet + facet normal 0.876025 -0.421297 -0.234711 + outer loop + vertex 3.46463 5.6 42.7491 + vertex 3.1164 4.8417 42.8105 + vertex 3.30591 5.6 42.1567 + endloop + endfacet + facet normal 0.876009 -0.421315 -0.234737 + outer loop + vertex 3.1164 4.8417 42.8105 + vertex 2.97363 4.8417 42.2777 + vertex 3.30591 5.6 42.1567 + endloop + endfacet + facet normal 0.792238 -0.572104 -0.212264 + outer loop + vertex 3.93725 6.28538 42.6658 + vertex 3.46463 5.6 42.7491 + vertex 3.75688 6.28538 41.9926 + endloop + endfacet + facet normal 0.792239 -0.572103 -0.212262 + outer loop + vertex 3.46463 5.6 42.7491 + vertex 3.30591 5.6 42.1567 + vertex 3.75688 6.28538 41.9926 + endloop + endfacet + facet normal 0.684302 -0.705766 -0.183372 + outer loop + vertex 4.51989 6.87701 42.563 + vertex 3.93725 6.28538 42.6658 + vertex 4.31283 6.87701 41.7903 + endloop + endfacet + facet normal 0.684325 -0.705749 -0.183351 + outer loop + vertex 3.93725 6.28538 42.6658 + vertex 3.75688 6.28538 41.9926 + vertex 4.31283 6.87701 41.7903 + endloop + endfacet + facet normal 0.555453 -0.818121 -0.148825 + outer loop + vertex 5.19486 7.35692 42.444 + vertex 4.51989 6.87701 42.563 + vertex 4.95688 7.35692 41.5558 + endloop + endfacet + facet normal 0.555434 -0.818132 -0.148839 + outer loop + vertex 4.51989 6.87701 42.563 + vertex 4.31283 6.87701 41.7903 + vertex 4.95688 7.35692 41.5558 + endloop + endfacet + facet normal 0.409503 -0.905686 -0.109729 + outer loop + vertex 5.94164 7.71053 42.3123 + vertex 5.19486 7.35692 42.444 + vertex 5.66945 7.71053 41.2965 + endloop + endfacet + facet normal 0.409513 -0.905682 -0.109723 + outer loop + vertex 5.19486 7.35692 42.444 + vertex 4.95688 7.35692 41.5558 + vertex 5.66945 7.71053 41.2965 + endloop + endfacet + facet normal 0.25089 -0.965678 -0.0672256 + outer loop + vertex 6.73755 7.92708 42.172 + vertex 5.94164 7.71053 42.3123 + vertex 6.4289 7.92708 41.0201 + endloop + endfacet + facet normal 0.250887 -0.965679 -0.0672268 + outer loop + vertex 5.94164 7.71053 42.3123 + vertex 5.66945 7.71053 41.2965 + vertex 6.4289 7.92708 41.0201 + endloop + endfacet + facet normal 0.0845028 -0.996166 -0.0226418 + outer loop + vertex 7.5584 8 42.0273 + vertex 6.73755 7.92708 42.172 + vertex 7.21214 8 40.735 + endloop + endfacet + facet normal 0.0845018 -0.996166 -0.0226421 + outer loop + vertex 6.73755 7.92708 42.172 + vertex 6.4289 7.92708 41.0201 + vertex 7.21214 8 40.735 + endloop + endfacet + facet normal 0.902921 -0.0868014 -0.42095 + outer loop + vertex 2.70162 3.2 42.3767 + vertex 2.55298 4.03351 41.886 + vertex 2.77014 4.03351 42.3518 + endloop + endfacet + facet normal 0.902881 -0.0868537 -0.421026 + outer loop + vertex 2.70162 3.2 42.3767 + vertex 2.48982 3.2 41.9225 + vertex 2.55298 4.03351 41.886 + endloop + endfacet + facet normal 0.875644 -0.257912 -0.408324 + outer loop + vertex 2.77014 4.03351 42.3518 + vertex 2.74052 4.8417 41.7778 + vertex 2.97363 4.8417 42.2777 + endloop + endfacet + facet normal 0.875692 -0.257861 -0.408255 + outer loop + vertex 2.77014 4.03351 42.3518 + vertex 2.55298 4.03351 41.886 + vertex 2.74052 4.8417 41.7778 + endloop + endfacet + facet normal 0.821916 -0.421323 -0.383329 + outer loop + vertex 3.30591 5.6 42.1567 + vertex 2.97363 4.8417 42.2777 + vertex 3.04674 5.6 41.601 + endloop + endfacet + facet normal 0.821953 -0.421289 -0.383287 + outer loop + vertex 2.97363 4.8417 42.2777 + vertex 2.74052 4.8417 41.7778 + vertex 3.04674 5.6 41.601 + endloop + endfacet + facet normal 0.74334 -0.572099 -0.346625 + outer loop + vertex 3.75688 6.28538 41.9926 + vertex 3.30591 5.6 42.1567 + vertex 3.46236 6.28538 41.361 + endloop + endfacet + facet normal 0.743297 -0.572132 -0.346662 + outer loop + vertex 3.30591 5.6 42.1567 + vertex 3.04674 5.6 41.601 + vertex 3.46236 6.28538 41.361 + endloop + endfacet + facet normal 0.642094 -0.705746 -0.299396 + outer loop + vertex 4.31283 6.87701 41.7903 + vertex 3.75688 6.28538 41.9926 + vertex 3.97473 6.87701 41.0652 + endloop + endfacet + facet normal 0.642079 -0.705756 -0.299406 + outer loop + vertex 3.75688 6.28538 41.9926 + vertex 3.46236 6.28538 41.361 + vertex 3.97473 6.87701 41.0652 + endloop + endfacet + facet normal 0.521142 -0.818137 -0.243029 + outer loop + vertex 4.95688 7.35692 41.5558 + vertex 4.31283 6.87701 41.7903 + vertex 4.56828 7.35692 40.7225 + endloop + endfacet + facet normal 0.521175 -0.81812 -0.243014 + outer loop + vertex 4.31283 6.87701 41.7903 + vertex 3.97473 6.87701 41.0652 + vertex 4.56828 7.35692 40.7225 + endloop + endfacet + facet normal 0.384243 -0.90568 -0.179166 + outer loop + vertex 5.66945 7.71053 41.2965 + vertex 4.95688 7.35692 41.5558 + vertex 5.22499 7.71053 40.3433 + endloop + endfacet + facet normal 0.384216 -0.90569 -0.179175 + outer loop + vertex 4.95688 7.35692 41.5558 + vertex 4.56828 7.35692 40.7225 + vertex 5.22499 7.71053 40.3433 + endloop + endfacet + facet normal 0.235403 -0.965679 -0.109773 + outer loop + vertex 6.4289 7.92708 41.0201 + vertex 5.66945 7.71053 41.2965 + vertex 5.9249 7.92708 39.9393 + endloop + endfacet + facet normal 0.235416 -0.965676 -0.10977 + outer loop + vertex 5.66945 7.71053 41.2965 + vertex 5.22499 7.71053 40.3433 + vertex 5.9249 7.92708 39.9393 + endloop + endfacet + facet normal 0.0792858 -0.996166 -0.0369717 + outer loop + vertex 7.21214 8 40.735 + vertex 6.4289 7.92708 41.0201 + vertex 6.64674 8 39.5225 + endloop + endfacet + facet normal 0.0792842 -0.996166 -0.0369719 + outer loop + vertex 6.4289 7.92708 41.0201 + vertex 5.9249 7.92708 39.9393 + vertex 6.64674 8 39.5225 + endloop + endfacet + facet normal 0.816035 -0.0868595 -0.571438 + outer loop + vertex 2.48982 3.2 41.9225 + vertex 2.25824 4.03351 41.4651 + vertex 2.55298 4.03351 41.886 + endloop + endfacet + facet normal 0.816053 -0.0868425 -0.571416 + outer loop + vertex 2.48982 3.2 41.9225 + vertex 2.20238 3.2 41.512 + vertex 2.25824 4.03351 41.4651 + endloop + endfacet + facet normal 0.79148 -0.257851 -0.554141 + outer loop + vertex 2.55298 4.03351 41.886 + vertex 2.42413 4.8417 41.3259 + vertex 2.74052 4.8417 41.7778 + endloop + endfacet + facet normal 0.791421 -0.257902 -0.554202 + outer loop + vertex 2.55298 4.03351 41.886 + vertex 2.25824 4.03351 41.4651 + vertex 2.42413 4.8417 41.3259 + endloop + endfacet + facet normal 0.742941 -0.421291 -0.520147 + outer loop + vertex 2.74052 4.8417 41.7778 + vertex 2.695 5.6 41.0986 + vertex 3.04674 5.6 41.601 + endloop + endfacet + facet normal 0.742934 -0.421297 -0.520152 + outer loop + vertex 2.74052 4.8417 41.7778 + vertex 2.42413 4.8417 41.3259 + vertex 2.695 5.6 41.0986 + endloop + endfacet + facet normal 0.67185 -0.572136 -0.470401 + outer loop + vertex 3.46236 6.28538 41.361 + vertex 3.04674 5.6 41.601 + vertex 3.06264 6.28538 40.7901 + endloop + endfacet + facet normal 0.67187 -0.572123 -0.470389 + outer loop + vertex 3.04674 5.6 41.601 + vertex 2.695 5.6 41.0986 + vertex 3.06264 6.28538 40.7901 + endloop + endfacet + facet normal 0.580321 -0.705753 -0.406375 + outer loop + vertex 3.97473 6.87701 41.0652 + vertex 3.46236 6.28538 41.361 + vertex 3.51585 6.87701 40.4099 + endloop + endfacet + facet normal 0.580373 -0.705722 -0.406353 + outer loop + vertex 3.46236 6.28538 41.361 + vertex 3.06264 6.28538 40.7901 + vertex 3.51585 6.87701 40.4099 + endloop + endfacet + facet normal 0.471048 -0.818122 -0.329834 + outer loop + vertex 4.56828 7.35692 40.7225 + vertex 3.97473 6.87701 41.0652 + vertex 4.04088 7.35692 39.9693 + endloop + endfacet + facet normal 0.471025 -0.818132 -0.32984 + outer loop + vertex 3.97473 6.87701 41.0652 + vertex 3.51585 6.87701 40.4099 + vertex 4.04088 7.35692 39.9693 + endloop + endfacet + facet normal 0.34726 -0.905691 -0.243175 + outer loop + vertex 5.22499 7.71053 40.3433 + vertex 4.56828 7.35692 40.7225 + vertex 4.62178 7.71053 39.4819 + endloop + endfacet + facet normal 0.347283 -0.905683 -0.243172 + outer loop + vertex 4.56828 7.35692 40.7225 + vertex 4.04088 7.35692 39.9693 + vertex 4.62178 7.71053 39.4819 + endloop + endfacet + facet normal 0.212779 -0.965675 -0.148987 + outer loop + vertex 5.9249 7.92708 39.9393 + vertex 5.22499 7.71053 40.3433 + vertex 5.24088 7.92708 38.9624 + endloop + endfacet + facet normal 0.212758 -0.965679 -0.148988 + outer loop + vertex 5.22499 7.71053 40.3433 + vertex 4.62178 7.71053 39.4819 + vertex 5.24088 7.92708 38.9624 + endloop + endfacet + facet normal 0.0716599 -0.996166 -0.0501763 + outer loop + vertex 5.9249 7.92708 39.9393 + vertex 5.87939 8 38.4266 + vertex 6.64674 8 39.5225 + endloop + endfacet + facet normal 0.0716605 -0.996166 -0.0501763 + outer loop + vertex 5.9249 7.92708 39.9393 + vertex 5.24088 7.92708 38.9624 + vertex 5.87939 8 38.4266 + endloop + endfacet + facet normal 0.704387 -0.0868464 -0.704484 + outer loop + vertex 2.20238 3.2 41.512 + vertex 1.89489 4.03351 41.1018 + vertex 2.25824 4.03351 41.4651 + endloop + endfacet + facet normal 0.704469 -0.0867795 -0.704409 + outer loop + vertex 1.89489 4.03351 41.1018 + vertex 2.20238 3.2 41.512 + vertex 1.84801 3.2 41.1576 + endloop + endfacet + facet normal 0.68314 -0.2579 -0.683234 + outer loop + vertex 1.89489 4.03351 41.1018 + vertex 2.42413 4.8417 41.3259 + vertex 2.25824 4.03351 41.4651 + endloop + endfacet + facet normal 0.68315 -0.25791 -0.68322 + outer loop + vertex 2.42413 4.8417 41.3259 + vertex 1.89489 4.03351 41.1018 + vertex 2.03409 4.8417 40.9359 + endloop + endfacet + facet normal 0.641258 -0.421298 -0.641324 + outer loop + vertex 2.03409 4.8417 40.9359 + vertex 2.695 5.6 41.0986 + vertex 2.42413 4.8417 41.3259 + endloop + endfacet + facet normal 0.641271 -0.421314 -0.6413 + outer loop + vertex 2.695 5.6 41.0986 + vertex 2.03409 4.8417 40.9359 + vertex 2.26138 5.6 40.665 + endloop + endfacet + facet normal 0.579932 -0.572125 -0.579958 + outer loop + vertex 2.26138 5.6 40.665 + vertex 3.06264 6.28538 40.7901 + vertex 2.695 5.6 41.0986 + endloop + endfacet + facet normal 0.579913 -0.572095 -0.580007 + outer loop + vertex 3.06264 6.28538 40.7901 + vertex 2.26138 5.6 40.665 + vertex 2.56986 6.28538 40.2974 + endloop + endfacet + facet normal 0.500942 -0.705714 -0.501024 + outer loop + vertex 2.56986 6.28538 40.2974 + vertex 3.51585 6.87701 40.4099 + vertex 3.06264 6.28538 40.7901 + endloop + endfacet + facet normal 0.500974 -0.70579 -0.500885 + outer loop + vertex 3.51585 6.87701 40.4099 + vertex 2.56986 6.28538 40.2974 + vertex 2.95015 6.87701 39.8441 + endloop + endfacet + facet normal 0.40664 -0.818136 -0.406568 + outer loop + vertex 2.95015 6.87701 39.8441 + vertex 4.04088 7.35692 39.9693 + vertex 3.51585 6.87701 40.4099 + endloop + endfacet + facet normal 0.406635 -0.818111 -0.406622 + outer loop + vertex 4.04088 7.35692 39.9693 + vertex 2.95015 6.87701 39.8441 + vertex 3.3907 7.35692 39.3191 + endloop + endfacet + facet normal 0.299787 -0.905682 -0.299778 + outer loop + vertex 3.3907 7.35692 39.3191 + vertex 4.62178 7.71053 39.4819 + vertex 4.04088 7.35692 39.9693 + endloop + endfacet + facet normal 0.299787 -0.905686 -0.299767 + outer loop + vertex 4.62178 7.71053 39.4819 + vertex 3.3907 7.35692 39.3191 + vertex 3.87813 7.71053 38.7382 + endloop + endfacet + facet normal 0.183668 -0.965679 -0.183656 + outer loop + vertex 3.87813 7.71053 38.7382 + vertex 5.24088 7.92708 38.9624 + vertex 4.62178 7.71053 39.4819 + endloop + endfacet + facet normal 0.183668 -0.965679 -0.18366 + outer loop + vertex 5.24088 7.92708 38.9624 + vertex 3.87813 7.71053 38.7382 + vertex 4.39762 7.92708 38.1191 + endloop + endfacet + facet normal 0.0618593 -0.996166 -0.0618564 + outer loop + vertex 4.39762 7.92708 38.1191 + vertex 5.87939 8 38.4266 + vertex 5.24088 7.92708 38.9624 + endloop + endfacet + facet normal 0.0618601 -0.996166 -0.0618601 + outer loop + vertex 5.87939 8 38.4266 + vertex 4.39762 7.92708 38.1191 + vertex 4.93339 8 37.4806 + endloop + endfacet + facet normal 0.571493 -0.0867713 -0.816006 + outer loop + vertex 1.84801 3.2 41.1576 + vertex 1.47396 4.03351 40.807 + vertex 1.89489 4.03351 41.1018 + endloop + endfacet + facet normal 0.571352 -0.086872 -0.816095 + outer loop + vertex 1.84801 3.2 41.1576 + vertex 1.4375 3.2 40.8702 + vertex 1.47396 4.03351 40.807 + endloop + endfacet + facet normal 0.554185 -0.257911 -0.79143 + outer loop + vertex 1.89489 4.03351 41.1018 + vertex 1.58224 4.8417 40.6195 + vertex 2.03409 4.8417 40.9359 + endloop + endfacet + facet normal 0.554257 -0.257862 -0.791395 + outer loop + vertex 1.89489 4.03351 41.1018 + vertex 1.47396 4.03351 40.807 + vertex 1.58224 4.8417 40.6195 + endloop + endfacet + facet normal 0.520143 -0.421315 -0.74293 + outer loop + vertex 2.03409 4.8417 40.9359 + vertex 1.75904 5.6 40.3133 + vertex 2.26138 5.6 40.665 + endloop + endfacet + facet normal 0.520208 -0.421273 -0.742908 + outer loop + vertex 2.03409 4.8417 40.9359 + vertex 1.58224 4.8417 40.6195 + vertex 1.75904 5.6 40.3133 + endloop + endfacet + facet normal 0.470499 -0.572092 -0.67182 + outer loop + vertex 2.26138 5.6 40.665 + vertex 1.99899 6.28538 39.8976 + vertex 2.56986 6.28538 40.2974 + endloop + endfacet + facet normal 0.470374 -0.572167 -0.671844 + outer loop + vertex 2.26138 5.6 40.665 + vertex 1.75904 5.6 40.3133 + vertex 1.99899 6.28538 39.8976 + endloop + endfacet + facet normal 0.406286 -0.705796 -0.58033 + outer loop + vertex 2.56986 6.28538 40.2974 + vertex 2.29481 6.87701 39.3853 + vertex 2.95015 6.87701 39.8441 + endloop + endfacet + facet normal 0.406421 -0.705724 -0.580324 + outer loop + vertex 2.56986 6.28538 40.2974 + vertex 1.99899 6.28538 39.8976 + vertex 2.29481 6.87701 39.3853 + endloop + endfacet + facet normal 0.329843 -0.818111 -0.471061 + outer loop + vertex 2.95015 6.87701 39.8441 + vertex 2.6375 7.35692 38.7917 + vertex 3.3907 7.35692 39.3191 + endloop + endfacet + facet normal 0.329784 -0.818138 -0.471056 + outer loop + vertex 2.95015 6.87701 39.8441 + vertex 2.29481 6.87701 39.3853 + vertex 2.6375 7.35692 38.7917 + endloop + endfacet + facet normal 0.243162 -0.905687 -0.34728 + outer loop + vertex 3.3907 7.35692 39.3191 + vertex 3.01665 7.71053 38.135 + vertex 3.87813 7.71053 38.7382 + endloop + endfacet + facet normal 0.243171 -0.905684 -0.347282 + outer loop + vertex 3.3907 7.35692 39.3191 + vertex 2.6375 7.35692 38.7917 + vertex 3.01665 7.71053 38.135 + endloop + endfacet + facet normal 0.148978 -0.965679 -0.212769 + outer loop + vertex 3.87813 7.71053 38.7382 + vertex 3.42074 7.92708 37.4351 + vertex 4.39762 7.92708 38.1191 + endloop + endfacet + facet normal 0.148979 -0.965678 -0.212769 + outer loop + vertex 3.87813 7.71053 38.7382 + vertex 3.01665 7.71053 38.135 + vertex 3.42074 7.92708 37.4351 + endloop + endfacet + facet normal 0.0501763 -0.996166 -0.071664 + outer loop + vertex 4.39762 7.92708 38.1191 + vertex 3.8375 8 36.7133 + vertex 4.93339 8 37.4806 + endloop + endfacet + facet normal 0.050179 -0.996166 -0.071665 + outer loop + vertex 4.39762 7.92708 38.1191 + vertex 3.42074 7.92708 37.4351 + vertex 3.8375 8 36.7133 + endloop + endfacet + facet normal 0.420918 -0.0868757 -0.902929 + outer loop + vertex 1.4375 3.2 40.8702 + vertex 1.00825 4.03351 40.5899 + vertex 1.47396 4.03351 40.807 + endloop + endfacet + facet normal 0.421034 -0.0868002 -0.902882 + outer loop + vertex 1.4375 3.2 40.8702 + vertex 0.983308 3.2 40.6584 + vertex 1.00825 4.03351 40.5899 + endloop + endfacet + facet normal 0.408296 -0.257859 -0.875673 + outer loop + vertex 1.47396 4.03351 40.807 + vertex 1.08231 4.8417 40.3864 + vertex 1.58224 4.8417 40.6195 + endloop + endfacet + facet normal 0.408222 -0.257905 -0.875694 + outer loop + vertex 1.47396 4.03351 40.807 + vertex 1.00825 4.03351 40.5899 + vertex 1.08231 4.8417 40.3864 + endloop + endfacet + facet normal 0.383324 -0.421273 -0.821944 + outer loop + vertex 1.58224 4.8417 40.6195 + vertex 1.20325 5.6 40.0541 + vertex 1.75904 5.6 40.3133 + endloop + endfacet + facet normal 0.383249 -0.421319 -0.821955 + outer loop + vertex 1.58224 4.8417 40.6195 + vertex 1.08231 4.8417 40.3864 + vertex 1.20325 5.6 40.0541 + endloop + endfacet + facet normal 0.346585 -0.572171 -0.743303 + outer loop + vertex 1.75904 5.6 40.3133 + vertex 1.36739 6.28538 39.6031 + vertex 1.99899 6.28538 39.8976 + endloop + endfacet + facet normal 0.346649 -0.572133 -0.743302 + outer loop + vertex 1.75904 5.6 40.3133 + vertex 1.20325 5.6 40.0541 + vertex 1.36739 6.28538 39.6031 + endloop + endfacet + facet normal 0.299416 -0.705722 -0.642111 + outer loop + vertex 1.99899 6.28538 39.8976 + vertex 1.56974 6.87701 39.0472 + vertex 2.29481 6.87701 39.3853 + endloop + endfacet + facet normal 0.2994 -0.705731 -0.642109 + outer loop + vertex 1.99899 6.28538 39.8976 + vertex 1.36739 6.28538 39.6031 + vertex 1.56974 6.87701 39.0472 + endloop + endfacet + facet normal 0.243019 -0.818137 -0.521146 + outer loop + vertex 2.29481 6.87701 39.3853 + vertex 1.80416 7.35692 38.4031 + vertex 2.6375 7.35692 38.7917 + endloop + endfacet + facet normal 0.243009 -0.818142 -0.521143 + outer loop + vertex 2.29481 6.87701 39.3853 + vertex 1.56974 6.87701 39.0472 + vertex 1.80416 7.35692 38.4031 + endloop + endfacet + facet normal 0.179153 -0.905683 -0.384243 + outer loop + vertex 2.6375 7.35692 38.7917 + vertex 2.06351 7.71053 37.6906 + vertex 3.01665 7.71053 38.135 + endloop + endfacet + facet normal 0.179185 -0.905671 -0.384256 + outer loop + vertex 2.6375 7.35692 38.7917 + vertex 1.80416 7.35692 38.4031 + vertex 2.06351 7.71053 37.6906 + endloop + endfacet + facet normal 0.109773 -0.965678 -0.235404 + outer loop + vertex 3.01665 7.71053 38.135 + vertex 2.33993 7.92708 36.9311 + vertex 3.42074 7.92708 37.4351 + endloop + endfacet + facet normal 0.109752 -0.965683 -0.235393 + outer loop + vertex 3.01665 7.71053 38.135 + vertex 2.06351 7.71053 37.6906 + vertex 2.33993 7.92708 36.9311 + endloop + endfacet + facet normal 0.0369735 -0.996166 -0.0792897 + outer loop + vertex 3.42074 7.92708 37.4351 + vertex 2.625 8 36.1479 + vertex 3.8375 8 36.7133 + endloop + endfacet + facet normal 0.0369744 -0.996166 -0.0792902 + outer loop + vertex 3.42074 7.92708 37.4351 + vertex 2.33993 7.92708 36.9311 + vertex 2.625 8 36.1479 + endloop + endfacet + facet normal 0.257849 -0.0867984 -0.962279 + outer loop + vertex 0.983308 3.2 40.6584 + vertex 0.511901 4.03351 40.4569 + vertex 1.00825 4.03351 40.5899 + endloop + endfacet + facet normal 0.257831 -0.0868097 -0.962282 + outer loop + vertex 0.983308 3.2 40.6584 + vertex 0.499238 3.2 40.5287 + vertex 0.511901 4.03351 40.4569 + endloop + endfacet + facet normal 0.250121 -0.257906 -0.933233 + outer loop + vertex 1.00825 4.03351 40.5899 + vertex 0.549505 4.8417 40.2436 + vertex 1.08231 4.8417 40.3864 + endloop + endfacet + facet normal 0.250067 -0.257938 -0.933238 + outer loop + vertex 1.00825 4.03351 40.5899 + vertex 0.511901 4.03351 40.4569 + vertex 0.549505 4.8417 40.2436 + endloop + endfacet + facet normal 0.234702 -0.421318 -0.876017 + outer loop + vertex 1.08231 4.8417 40.3864 + vertex 0.610908 5.6 39.8954 + vertex 1.20325 5.6 40.0541 + endloop + endfacet + facet normal 0.234787 -0.421267 -0.876019 + outer loop + vertex 1.08231 4.8417 40.3864 + vertex 0.549505 4.8417 40.2436 + vertex 0.610908 5.6 39.8954 + endloop + endfacet + facet normal 0.212197 -0.572132 -0.792235 + outer loop + vertex 1.20325 5.6 40.0541 + vertex 0.694242 6.28538 39.4228 + vertex 1.36739 6.28538 39.6031 + endloop + endfacet + facet normal 0.212258 -0.572096 -0.792245 + outer loop + vertex 1.20325 5.6 40.0541 + vertex 0.610908 5.6 39.8954 + vertex 0.694242 6.28538 39.4228 + endloop + endfacet + facet normal 0.183401 -0.70573 -0.684331 + outer loop + vertex 1.36739 6.28538 39.6031 + vertex 0.796979 6.87701 38.8401 + vertex 1.56974 6.87701 39.0472 + endloop + endfacet + facet normal 0.183286 -0.705795 -0.684296 + outer loop + vertex 1.36739 6.28538 39.6031 + vertex 0.694242 6.28538 39.4228 + vertex 0.796979 6.87701 38.8401 + endloop + endfacet + facet normal 0.148834 -0.818143 -0.555419 + outer loop + vertex 1.56974 6.87701 39.0472 + vertex 0.915994 7.35692 38.1651 + vertex 1.80416 7.35692 38.4031 + endloop + endfacet + facet normal 0.148855 -0.818133 -0.555428 + outer loop + vertex 1.56974 6.87701 39.0472 + vertex 0.796979 6.87701 38.8401 + vertex 0.915994 7.35692 38.1651 + endloop + endfacet + facet normal 0.109737 -0.90567 -0.409535 + outer loop + vertex 1.80416 7.35692 38.4031 + vertex 1.04767 7.71053 37.4184 + vertex 2.06351 7.71053 37.6906 + endloop + endfacet + facet normal 0.109743 -0.905668 -0.409539 + outer loop + vertex 1.80416 7.35692 38.4031 + vertex 0.915994 7.35692 38.1651 + vertex 1.04767 7.71053 37.4184 + endloop + endfacet + facet normal 0.0672298 -0.965684 -0.250869 + outer loop + vertex 2.06351 7.71053 37.6906 + vertex 1.18801 7.92708 36.6224 + vertex 2.33993 7.92708 36.9311 + endloop + endfacet + facet normal 0.0672197 -0.965686 -0.250861 + outer loop + vertex 2.06351 7.71053 37.6906 + vertex 1.04767 7.71053 37.4184 + vertex 1.18801 7.92708 36.6224 + endloop + endfacet + facet normal 0.022646 -0.996166 -0.0845055 + outer loop + vertex 2.33993 7.92708 36.9311 + vertex 1.33275 8 35.8016 + vertex 2.625 8 36.1479 + endloop + endfacet + facet normal 0.0226465 -0.996166 -0.084506 + outer loop + vertex 2.33993 7.92708 36.9311 + vertex 1.18801 7.92708 36.6224 + vertex 1.33275 8 35.8016 + endloop + endfacet + facet normal 0.0868546 -0.0868093 -0.992432 + outer loop + vertex 0.499238 3.2 40.5287 + vertex 0 4.03351 40.4121 + vertex 0.511901 4.03351 40.4569 + endloop + endfacet + facet normal 0.0868709 -0.0867995 -0.992431 + outer loop + vertex 0 3.2 40.485 + vertex 0 4.03351 40.4121 + vertex 0.499238 3.2 40.5287 + endloop + endfacet + facet normal 0.0842491 -0.257941 -0.96248 + outer loop + vertex 0.511901 4.03351 40.4569 + vertex 0 4.8417 40.1955 + vertex 0.549505 4.8417 40.2436 + endloop + endfacet + facet normal 0.0842332 -0.25795 -0.962479 + outer loop + vertex 0 4.03351 40.4121 + vertex 0 4.8417 40.1955 + vertex 0.511901 4.03351 40.4569 + endloop + endfacet + facet normal 0.0791216 -0.42127 -0.903477 + outer loop + vertex 0.549505 4.8417 40.2436 + vertex 0 5.6 39.8419 + vertex 0.610908 5.6 39.8954 + endloop + endfacet + facet normal 0.0790837 -0.421294 -0.90347 + outer loop + vertex 0 4.8417 40.1955 + vertex 0 5.6 39.8419 + vertex 0.549505 4.8417 40.2436 + endloop + endfacet + facet normal 0.0715559 -0.572098 -0.817058 + outer loop + vertex 0.610908 5.6 39.8954 + vertex 0 6.28538 39.362 + vertex 0.694242 6.28538 39.4228 + endloop + endfacet + facet normal 0.0715534 -0.5721 -0.817057 + outer loop + vertex 0 5.6 39.8419 + vertex 0 6.28538 39.362 + vertex 0.610908 5.6 39.8954 + endloop + endfacet + facet normal 0.0617194 -0.705791 -0.705726 + outer loop + vertex 0.694242 6.28538 39.4228 + vertex 0 6.87701 38.7704 + vertex 0.796979 6.87701 38.8401 + endloop + endfacet + facet normal 0.0618098 -0.705737 -0.705773 + outer loop + vertex 0 6.28538 39.362 + vertex 0 6.87701 38.7704 + vertex 0.694242 6.28538 39.4228 + endloop + endfacet + facet normal 0.0500928 -0.818133 -0.572842 + outer loop + vertex 0.796979 6.87701 38.8401 + vertex 0 7.35692 38.085 + vertex 0.915994 7.35692 38.1651 + endloop + endfacet + facet normal 0.0500984 -0.81813 -0.572846 + outer loop + vertex 0 6.87701 38.7704 + vertex 0 7.35692 38.085 + vertex 0.796979 6.87701 38.8401 + endloop + endfacet + facet normal 0.0369692 -0.905668 -0.422372 + outer loop + vertex 0.915994 7.35692 38.1651 + vertex 0 7.71053 37.3267 + vertex 1.04767 7.71053 37.4184 + endloop + endfacet + facet normal 0.0369318 -0.905685 -0.422339 + outer loop + vertex 0 7.35692 38.085 + vertex 0 7.71053 37.3267 + vertex 0.915994 7.35692 38.1651 + endloop + endfacet + facet normal 0.0226272 -0.965686 -0.258723 + outer loop + vertex 1.04767 7.71053 37.4184 + vertex 0 7.92708 36.5185 + vertex 1.18801 7.92708 36.6224 + endloop + endfacet + facet normal 0.0226474 -0.96568 -0.258745 + outer loop + vertex 0 7.71053 37.3267 + vertex 0 7.92708 36.5185 + vertex 1.04767 7.71053 37.4184 + endloop + endfacet + facet normal 0.00762503 -0.996166 -0.0871549 + outer loop + vertex 1.18801 7.92708 36.6224 + vertex 0 8 35.685 + vertex 1.33275 8 35.8016 + endloop + endfacet + facet normal 0.00762199 -0.996166 -0.0871511 + outer loop + vertex 0 7.92708 36.5185 + vertex 0 8 35.685 + vertex 1.18801 7.92708 36.6224 + endloop + endfacet + facet normal -0.0868546 -0.0867996 -0.992432 + outer loop + vertex 0 3.2 40.485 + vertex -0.511901 4.03351 40.4569 + vertex 0 4.03351 40.4121 + endloop + endfacet + facet normal -0.0868708 -0.0868094 -0.99243 + outer loop + vertex -0.499238 3.2 40.5287 + vertex -0.511901 4.03351 40.4569 + vertex 0 3.2 40.485 + endloop + endfacet + facet normal -0.0842489 -0.25795 -0.962478 + outer loop + vertex 0 4.03351 40.4121 + vertex -0.549505 4.8417 40.2436 + vertex 0 4.8417 40.1955 + endloop + endfacet + facet normal -0.0842335 -0.25794 -0.962482 + outer loop + vertex -0.511901 4.03351 40.4569 + vertex -0.549505 4.8417 40.2436 + vertex 0 4.03351 40.4121 + endloop + endfacet + facet normal -0.0791207 -0.421292 -0.903467 + outer loop + vertex 0 4.8417 40.1955 + vertex -0.610908 5.6 39.8954 + vertex 0 5.6 39.8419 + endloop + endfacet + facet normal -0.0790847 -0.421269 -0.903481 + outer loop + vertex -0.549505 4.8417 40.2436 + vertex -0.610908 5.6 39.8954 + vertex 0 4.8417 40.1955 + endloop + endfacet + facet normal -0.0715558 -0.5721 -0.817057 + outer loop + vertex 0 5.6 39.8419 + vertex -0.694242 6.28538 39.4228 + vertex 0 6.28538 39.362 + endloop + endfacet + facet normal -0.0715535 -0.572098 -0.817058 + outer loop + vertex -0.610908 5.6 39.8954 + vertex -0.694242 6.28538 39.4228 + vertex 0 5.6 39.8419 + endloop + endfacet + facet normal -0.0617239 -0.705741 -0.705776 + outer loop + vertex 0 6.28538 39.362 + vertex -0.796979 6.87701 38.8401 + vertex 0 6.87701 38.7704 + endloop + endfacet + facet normal -0.0618048 -0.705795 -0.705715 + outer loop + vertex -0.694242 6.28538 39.4228 + vertex -0.796979 6.87701 38.8401 + vertex 0 6.28538 39.362 + endloop + endfacet + facet normal -0.0500931 -0.818131 -0.572847 + outer loop + vertex 0 6.87701 38.7704 + vertex -0.915994 7.35692 38.1651 + vertex 0 7.35692 38.085 + endloop + endfacet + facet normal -0.050098 -0.818134 -0.572842 + outer loop + vertex -0.796979 6.87701 38.8401 + vertex -0.915994 7.35692 38.1651 + vertex 0 6.87701 38.7704 + endloop + endfacet + facet normal -0.0369662 -0.905684 -0.422338 + outer loop + vertex 0 7.35692 38.085 + vertex -1.04767 7.71053 37.4184 + vertex 0 7.71053 37.3267 + endloop + endfacet + facet normal -0.0369352 -0.905667 -0.422378 + outer loop + vertex -0.915994 7.35692 38.1651 + vertex -1.04767 7.71053 37.4184 + vertex 0 7.35692 38.085 + endloop + endfacet + facet normal -0.0226291 -0.96568 -0.258745 + outer loop + vertex 0 7.71053 37.3267 + vertex -1.18801 7.92708 36.6224 + vertex 0 7.92708 36.5185 + endloop + endfacet + facet normal -0.0226452 -0.965687 -0.25872 + outer loop + vertex -1.04767 7.71053 37.4184 + vertex -1.18801 7.92708 36.6224 + vertex 0 7.71053 37.3267 + endloop + endfacet + facet normal -0.0076247 -0.996166 -0.0871511 + outer loop + vertex 0 7.92708 36.5185 + vertex -1.33275 8 35.8016 + vertex 0 8 35.685 + endloop + endfacet + facet normal -0.00762236 -0.996166 -0.0871554 + outer loop + vertex -1.18801 7.92708 36.6224 + vertex -1.33275 8 35.8016 + vertex 0 7.92708 36.5185 + endloop + endfacet + facet normal -0.257849 -0.0868096 -0.962278 + outer loop + vertex -0.499238 3.2 40.5287 + vertex -1.00825 4.03351 40.5899 + vertex -0.511901 4.03351 40.4569 + endloop + endfacet + facet normal -0.257831 -0.0867983 -0.962283 + outer loop + vertex -0.983308 3.2 40.6584 + vertex -1.00825 4.03351 40.5899 + vertex -0.499238 3.2 40.5287 + endloop + endfacet + facet normal -0.250119 -0.257937 -0.933225 + outer loop + vertex -0.511901 4.03351 40.4569 + vertex -1.08231 4.8417 40.3864 + vertex -0.549505 4.8417 40.2436 + endloop + endfacet + facet normal -0.25007 -0.257905 -0.933247 + outer loop + vertex -1.00825 4.03351 40.5899 + vertex -1.08231 4.8417 40.3864 + vertex -0.511901 4.03351 40.4569 + endloop + endfacet + facet normal -0.234708 -0.421269 -0.876039 + outer loop + vertex -0.549505 4.8417 40.2436 + vertex -1.20325 5.6 40.0541 + vertex -0.610908 5.6 39.8954 + endloop + endfacet + facet normal -0.23478 -0.421321 -0.875995 + outer loop + vertex -1.08231 4.8417 40.3864 + vertex -1.20325 5.6 40.0541 + vertex -0.549505 4.8417 40.2436 + endloop + endfacet + facet normal -0.212203 -0.572099 -0.792258 + outer loop + vertex -0.610908 5.6 39.8954 + vertex -1.36739 6.28538 39.6031 + vertex -0.694242 6.28538 39.4228 + endloop + endfacet + facet normal -0.212251 -0.572135 -0.792219 + outer loop + vertex -1.20325 5.6 40.0541 + vertex -1.36739 6.28538 39.6031 + vertex -0.610908 5.6 39.8954 + endloop + endfacet + facet normal -0.183385 -0.70579 -0.684273 + outer loop + vertex -0.694242 6.28538 39.4228 + vertex -1.56974 6.87701 39.0472 + vertex -0.796979 6.87701 38.8401 + endloop + endfacet + facet normal -0.183304 -0.705725 -0.684362 + outer loop + vertex -1.36739 6.28538 39.6031 + vertex -1.56974 6.87701 39.0472 + vertex -0.694242 6.28538 39.4228 + endloop + endfacet + facet normal -0.148838 -0.818134 -0.555432 + outer loop + vertex -0.796979 6.87701 38.8401 + vertex -1.80416 7.35692 38.4031 + vertex -0.915994 7.35692 38.1651 + endloop + endfacet + facet normal -0.148851 -0.818144 -0.555413 + outer loop + vertex -1.56974 6.87701 39.0472 + vertex -1.80416 7.35692 38.4031 + vertex -0.796979 6.87701 38.8401 + endloop + endfacet + facet normal -0.109738 -0.905668 -0.40954 + outer loop + vertex -0.915994 7.35692 38.1651 + vertex -2.06351 7.71053 37.6906 + vertex -1.04767 7.71053 37.4184 + endloop + endfacet + facet normal -0.109742 -0.905671 -0.409533 + outer loop + vertex -1.80416 7.35692 38.4031 + vertex -2.06351 7.71053 37.6906 + vertex -0.915994 7.35692 38.1651 + endloop + endfacet + facet normal -0.0672273 -0.965686 -0.25086 + outer loop + vertex -1.04767 7.71053 37.4184 + vertex -2.33993 7.92708 36.9311 + vertex -1.18801 7.92708 36.6224 + endloop + endfacet + facet normal -0.0672225 -0.965683 -0.250872 + outer loop + vertex -2.06351 7.71053 37.6906 + vertex -2.33993 7.92708 36.9311 + vertex -1.04767 7.71053 37.4184 + endloop + endfacet + facet normal -0.0226461 -0.996166 -0.0845061 + outer loop + vertex -1.18801 7.92708 36.6224 + vertex -2.625 8 36.1479 + vertex -1.33275 8 35.8016 + endloop + endfacet + facet normal -0.0226464 -0.996166 -0.0845054 + outer loop + vertex -2.33993 7.92708 36.9311 + vertex -2.625 8 36.1479 + vertex -1.18801 7.92708 36.6224 + endloop + endfacet + facet normal -0.420921 -0.0868012 -0.902935 + outer loop + vertex -0.983308 3.2 40.6584 + vertex -1.47396 4.03351 40.807 + vertex -1.00825 4.03351 40.5899 + endloop + endfacet + facet normal -0.421033 -0.0868777 -0.902875 + outer loop + vertex -1.4375 3.20001 40.8702 + vertex -1.47396 4.03351 40.807 + vertex -0.983308 3.2 40.6584 + endloop + endfacet + facet normal -0.408291 -0.257904 -0.875662 + outer loop + vertex -1.00825 4.03351 40.5899 + vertex -1.58224 4.8417 40.6195 + vertex -1.08231 4.8417 40.3864 + endloop + endfacet + facet normal -0.408228 -0.257857 -0.875705 + outer loop + vertex -1.47396 4.03351 40.807 + vertex -1.58224 4.8417 40.6195 + vertex -1.00825 4.03351 40.5899 + endloop + endfacet + facet normal -0.383316 -0.421316 -0.821926 + outer loop + vertex -1.08231 4.8417 40.3864 + vertex -1.75904 5.6 40.3133 + vertex -1.20325 5.6 40.0541 + endloop + endfacet + facet normal -0.383259 -0.42127 -0.821976 + outer loop + vertex -1.58224 4.8417 40.6195 + vertex -1.75904 5.6 40.3133 + vertex -1.08231 4.8417 40.3864 + endloop + endfacet + facet normal -0.346595 -0.572135 -0.743326 + outer loop + vertex -1.20325 5.6 40.0541 + vertex -1.99899 6.28538 39.8976 + vertex -1.36739 6.28538 39.6031 + endloop + endfacet + facet normal -0.346637 -0.572173 -0.743277 + outer loop + vertex -1.75904 5.6 40.3133 + vertex -1.99899 6.28538 39.8976 + vertex -1.20325 5.6 40.0541 + endloop + endfacet + facet normal -0.299413 -0.70573 -0.642103 + outer loop + vertex -1.36739 6.28538 39.6031 + vertex -2.29481 6.87701 39.3853 + vertex -1.56974 6.87701 39.0472 + endloop + endfacet + facet normal -0.299404 -0.705722 -0.642117 + outer loop + vertex -1.99899 6.28538 39.8976 + vertex -2.29481 6.87701 39.3853 + vertex -1.36739 6.28538 39.6031 + endloop + endfacet + facet normal -0.243016 -0.818142 -0.52114 + outer loop + vertex -1.56974 6.87701 39.0472 + vertex -2.6375 7.35692 38.7917 + vertex -1.80416 7.35692 38.4031 + endloop + endfacet + facet normal -0.243012 -0.818137 -0.52115 + outer loop + vertex -2.29481 6.87701 39.3853 + vertex -2.6375 7.35692 38.7917 + vertex -1.56974 6.87701 39.0472 + endloop + endfacet + facet normal -0.179163 -0.905672 -0.384265 + outer loop + vertex -1.80416 7.35692 38.4031 + vertex -3.01665 7.71053 38.135 + vertex -2.06351 7.71053 37.6906 + endloop + endfacet + facet normal -0.179174 -0.905684 -0.384232 + outer loop + vertex -2.6375 7.35692 38.7917 + vertex -3.01665 7.71053 38.135 + vertex -1.80416 7.35692 38.4031 + endloop + endfacet + facet normal -0.109765 -0.965683 -0.235388 + outer loop + vertex -2.06351 7.71053 37.6906 + vertex -3.42074 7.92708 37.4351 + vertex -2.33993 7.92708 36.9311 + endloop + endfacet + facet normal -0.10976 -0.965678 -0.235412 + outer loop + vertex -3.01665 7.71053 38.135 + vertex -3.42074 7.92708 37.4351 + vertex -2.06351 7.71053 37.6906 + endloop + endfacet + facet normal -0.0369739 -0.996166 -0.0792904 + outer loop + vertex -2.33993 7.92708 36.9311 + vertex -3.8375 8 36.7133 + vertex -2.625 8 36.1479 + endloop + endfacet + facet normal -0.036974 -0.996166 -0.0792894 + outer loop + vertex -3.42074 7.92708 37.4351 + vertex -3.8375 8 36.7133 + vertex -2.33993 7.92708 36.9311 + endloop + endfacet + facet normal -0.571488 -0.0868717 -0.815999 + outer loop + vertex -1.47396 4.03351 40.807 + vertex -1.4375 3.20001 40.8702 + vertex -1.89489 4.03351 41.1018 + endloop + endfacet + facet normal -0.571355 -0.08677 -0.816103 + outer loop + vertex -1.84801 3.2 41.1576 + vertex -1.89489 4.03351 41.1018 + vertex -1.4375 3.20001 40.8702 + endloop + endfacet + facet normal -0.554192 -0.257864 -0.79144 + outer loop + vertex -1.47396 4.03351 40.807 + vertex -2.03409 4.8417 40.9359 + vertex -1.58224 4.8417 40.6195 + endloop + endfacet + facet normal -0.554249 -0.257912 -0.791384 + outer loop + vertex -1.89489 4.03351 41.1018 + vertex -2.03409 4.8417 40.9359 + vertex -1.47396 4.03351 40.807 + endloop + endfacet + facet normal -0.520153 -0.421275 -0.742945 + outer loop + vertex -1.58224 4.8417 40.6195 + vertex -2.26138 5.6 40.665 + vertex -1.75904 5.6 40.3133 + endloop + endfacet + facet normal -0.520197 -0.421317 -0.742891 + outer loop + vertex -2.03409 4.8417 40.9359 + vertex -2.26138 5.6 40.665 + vertex -1.58224 4.8417 40.6195 + endloop + endfacet + facet normal -0.470471 -0.572162 -0.67178 + outer loop + vertex -1.75904 5.6 40.3133 + vertex -2.56986 6.28538 40.2974 + vertex -1.99899 6.28538 39.8976 + endloop + endfacet + facet normal -0.470405 -0.572087 -0.671889 + outer loop + vertex -2.26138 5.6 40.665 + vertex -2.56986 6.28538 40.2974 + vertex -1.75904 5.6 40.3133 + endloop + endfacet + facet normal -0.406325 -0.705729 -0.580385 + outer loop + vertex -1.99899 6.28538 39.8976 + vertex -2.95015 6.87701 39.8441 + vertex -2.29481 6.87701 39.3853 + endloop + endfacet + facet normal -0.406377 -0.705801 -0.580261 + outer loop + vertex -2.56986 6.28538 40.2974 + vertex -2.95015 6.87701 39.8441 + vertex -1.99899 6.28538 39.8976 + endloop + endfacet + facet normal -0.329822 -0.818136 -0.471032 + outer loop + vertex -2.29481 6.87701 39.3853 + vertex -3.3907 7.35692 39.3191 + vertex -2.6375 7.35692 38.7917 + endloop + endfacet + facet normal -0.329807 -0.818109 -0.471089 + outer loop + vertex -2.95015 6.87701 39.8441 + vertex -3.3907 7.35692 39.3191 + vertex -2.29481 6.87701 39.3853 + endloop + endfacet + facet normal -0.243166 -0.905684 -0.347286 + outer loop + vertex -2.6375 7.35692 38.7917 + vertex -3.87813 7.71053 38.7382 + vertex -3.01665 7.71053 38.135 + endloop + endfacet + facet normal -0.243167 -0.905687 -0.347276 + outer loop + vertex -3.3907 7.35692 39.3191 + vertex -3.87813 7.71053 38.7382 + vertex -2.6375 7.35692 38.7917 + endloop + endfacet + facet normal -0.148978 -0.965678 -0.212769 + outer loop + vertex -3.01665 7.71053 38.135 + vertex -4.39762 7.92708 38.1191 + vertex -3.42074 7.92708 37.4351 + endloop + endfacet + facet normal -0.148978 -0.965679 -0.212768 + outer loop + vertex -3.87813 7.71053 38.7382 + vertex -4.39762 7.92708 38.1191 + vertex -3.01665 7.71053 38.135 + endloop + endfacet + facet normal -0.0501776 -0.996166 -0.0716658 + outer loop + vertex -3.42074 7.92708 37.4351 + vertex -4.93339 8 37.4806 + vertex -3.8375 8 36.7133 + endloop + endfacet + facet normal -0.0501776 -0.996166 -0.0716629 + outer loop + vertex -4.39762 7.92708 38.1191 + vertex -4.93339 8 37.4806 + vertex -3.42074 7.92708 37.4351 + endloop + endfacet + facet normal -0.704469 -0.0867795 -0.704409 + outer loop + vertex -2.20238 3.2 41.512 + vertex -1.89489 4.03351 41.1018 + vertex -1.84801 3.2 41.1576 + endloop + endfacet + facet normal -0.704387 -0.0868464 -0.704484 + outer loop + vertex -1.89489 4.03351 41.1018 + vertex -2.20238 3.2 41.512 + vertex -2.25824 4.03351 41.4651 + endloop + endfacet + facet normal -0.68315 -0.25791 -0.68322 + outer loop + vertex -1.89489 4.03351 41.1018 + vertex -2.42413 4.8417 41.3259 + vertex -2.03409 4.8417 40.9359 + endloop + endfacet + facet normal -0.68314 -0.2579 -0.683234 + outer loop + vertex -2.42413 4.8417 41.3259 + vertex -1.89489 4.03351 41.1018 + vertex -2.25824 4.03351 41.4651 + endloop + endfacet + facet normal -0.641271 -0.421314 -0.6413 + outer loop + vertex -2.03409 4.8417 40.9359 + vertex -2.695 5.6 41.0986 + vertex -2.26138 5.6 40.665 + endloop + endfacet + facet normal -0.641258 -0.421298 -0.641324 + outer loop + vertex -2.695 5.6 41.0986 + vertex -2.03409 4.8417 40.9359 + vertex -2.42413 4.8417 41.3259 + endloop + endfacet + facet normal -0.579913 -0.572095 -0.580007 + outer loop + vertex -2.26138 5.6 40.665 + vertex -3.06264 6.28538 40.7901 + vertex -2.56986 6.28538 40.2974 + endloop + endfacet + facet normal -0.579932 -0.572125 -0.579958 + outer loop + vertex -3.06264 6.28538 40.7901 + vertex -2.26138 5.6 40.665 + vertex -2.695 5.6 41.0986 + endloop + endfacet + facet normal -0.500942 -0.705714 -0.501024 + outer loop + vertex -3.51585 6.87701 40.4099 + vertex -2.56986 6.28538 40.2974 + vertex -3.06264 6.28538 40.7901 + endloop + endfacet + facet normal -0.500974 -0.70579 -0.500885 + outer loop + vertex -2.56986 6.28538 40.2974 + vertex -3.51585 6.87701 40.4099 + vertex -2.95015 6.87701 39.8441 + endloop + endfacet + facet normal -0.40664 -0.818136 -0.406568 + outer loop + vertex -4.04088 7.35692 39.9693 + vertex -2.95015 6.87701 39.8441 + vertex -3.51585 6.87701 40.4099 + endloop + endfacet + facet normal -0.406635 -0.818111 -0.406622 + outer loop + vertex -2.95015 6.87701 39.8441 + vertex -4.04088 7.35692 39.9693 + vertex -3.3907 7.35692 39.3191 + endloop + endfacet + facet normal -0.299787 -0.905682 -0.299778 + outer loop + vertex -4.62178 7.71053 39.4819 + vertex -3.3907 7.35692 39.3191 + vertex -4.04088 7.35692 39.9693 + endloop + endfacet + facet normal -0.299787 -0.905686 -0.299767 + outer loop + vertex -3.3907 7.35692 39.3191 + vertex -4.62178 7.71053 39.4819 + vertex -3.87813 7.71053 38.7382 + endloop + endfacet + facet normal -0.183668 -0.965679 -0.183656 + outer loop + vertex -5.24088 7.92708 38.9624 + vertex -3.87813 7.71053 38.7382 + vertex -4.62178 7.71053 39.4819 + endloop + endfacet + facet normal -0.183668 -0.965679 -0.18366 + outer loop + vertex -3.87813 7.71053 38.7382 + vertex -5.24088 7.92708 38.9624 + vertex -4.39762 7.92708 38.1191 + endloop + endfacet + facet normal -0.0618593 -0.996166 -0.0618564 + outer loop + vertex -5.87939 8 38.4266 + vertex -4.39762 7.92708 38.1191 + vertex -5.24088 7.92708 38.9624 + endloop + endfacet + facet normal -0.0618601 -0.996166 -0.0618601 + outer loop + vertex -4.39762 7.92708 38.1191 + vertex -5.87939 8 38.4266 + vertex -4.93339 8 37.4806 + endloop + endfacet + facet normal -0.816037 -0.0868427 -0.571439 + outer loop + vertex -2.20238 3.2 41.512 + vertex -2.55298 4.03351 41.886 + vertex -2.25824 4.03351 41.4651 + endloop + endfacet + facet normal -0.816053 -0.0868608 -0.571414 + outer loop + vertex -2.55298 4.03351 41.886 + vertex -2.20238 3.2 41.512 + vertex -2.48982 3.20001 41.9225 + endloop + endfacet + facet normal -0.791469 -0.2579 -0.554134 + outer loop + vertex -2.25824 4.03351 41.4651 + vertex -2.74052 4.8417 41.7778 + vertex -2.42413 4.8417 41.3259 + endloop + endfacet + facet normal -0.791433 -0.257849 -0.55421 + outer loop + vertex -2.55298 4.03351 41.886 + vertex -2.74052 4.8417 41.7778 + vertex -2.25824 4.03351 41.4651 + endloop + endfacet + facet normal -0.742939 -0.421296 -0.520146 + outer loop + vertex -2.42413 4.8417 41.3259 + vertex -3.04674 5.6 41.601 + vertex -2.695 5.6 41.0986 + endloop + endfacet + facet normal -0.742936 -0.421291 -0.520154 + outer loop + vertex -2.74052 4.8417 41.7778 + vertex -3.04674 5.6 41.601 + vertex -2.42413 4.8417 41.3259 + endloop + endfacet + facet normal -0.671858 -0.572124 -0.470406 + outer loop + vertex -2.695 5.6 41.0986 + vertex -3.46236 6.28538 41.361 + vertex -3.06264 6.28538 40.7901 + endloop + endfacet + facet normal -0.671862 -0.572137 -0.470384 + outer loop + vertex -3.04674 5.6 41.601 + vertex -3.46236 6.28538 41.361 + vertex -2.695 5.6 41.0986 + endloop + endfacet + facet normal -0.580344 -0.705724 -0.406391 + outer loop + vertex -3.06264 6.28538 40.7901 + vertex -3.97473 6.87701 41.0652 + vertex -3.51585 6.87701 40.4099 + endloop + endfacet + facet normal -0.580347 -0.705755 -0.406334 + outer loop + vertex -3.46236 6.28538 41.361 + vertex -3.97473 6.87701 41.0652 + vertex -3.06264 6.28538 40.7901 + endloop + endfacet + facet normal -0.471037 -0.818132 -0.329826 + outer loop + vertex -3.51585 6.87701 40.4099 + vertex -4.56828 7.35692 40.7225 + vertex -4.04088 7.35692 39.9693 + endloop + endfacet + facet normal -0.471039 -0.818121 -0.329849 + outer loop + vertex -3.97473 6.87701 41.0652 + vertex -4.56828 7.35692 40.7225 + vertex -3.51585 6.87701 40.4099 + endloop + endfacet + facet normal -0.347273 -0.905684 -0.243184 + outer loop + vertex -4.04088 7.35692 39.9693 + vertex -5.22499 7.71053 40.3433 + vertex -4.62178 7.71053 39.4819 + endloop + endfacet + facet normal -0.347268 -0.905691 -0.243162 + outer loop + vertex -4.56828 7.35692 40.7225 + vertex -5.22499 7.71053 40.3433 + vertex -4.04088 7.35692 39.9693 + endloop + endfacet + facet normal -0.212767 -0.965679 -0.148978 + outer loop + vertex -4.62178 7.71053 39.4819 + vertex -5.9249 7.92708 39.9393 + vertex -5.24088 7.92708 38.9624 + endloop + endfacet + facet normal -0.212773 -0.965675 -0.148998 + outer loop + vertex -5.22499 7.71053 40.3433 + vertex -5.9249 7.92708 39.9393 + vertex -4.62178 7.71053 39.4819 + endloop + endfacet + facet normal -0.0716603 -0.996166 -0.0501766 + outer loop + vertex -5.24088 7.92708 38.9624 + vertex -6.64674 8 39.5225 + vertex -5.87939 8 38.4266 + endloop + endfacet + facet normal -0.0716601 -0.996166 -0.050176 + outer loop + vertex -5.9249 7.92708 39.9393 + vertex -6.64674 8 39.5225 + vertex -5.24088 7.92708 38.9624 + endloop + endfacet + facet normal -0.902917 -0.086854 -0.420948 + outer loop + vertex -2.55298 4.03351 41.886 + vertex -2.48982 3.20001 41.9225 + vertex -2.77014 4.03351 42.3518 + endloop + endfacet + facet normal -0.902884 -0.0868007 -0.42103 + outer loop + vertex -2.70162 3.2 42.3767 + vertex -2.77014 4.03351 42.3518 + vertex -2.48982 3.20001 41.9225 + endloop + endfacet + facet normal -0.875656 -0.257862 -0.40833 + outer loop + vertex -2.55298 4.03351 41.886 + vertex -2.97363 4.8417 42.2777 + vertex -2.74052 4.8417 41.7778 + endloop + endfacet + facet normal -0.875679 -0.257914 -0.408249 + outer loop + vertex -2.77014 4.03351 42.3518 + vertex -2.97363 4.8417 42.2777 + vertex -2.55298 4.03351 41.886 + endloop + endfacet + facet normal -0.821929 -0.421291 -0.383335 + outer loop + vertex -2.74052 4.8417 41.7778 + vertex -3.30591 5.6 42.1567 + vertex -3.04674 5.6 41.601 + endloop + endfacet + facet normal -0.821938 -0.421325 -0.38328 + outer loop + vertex -2.97363 4.8417 42.2777 + vertex -3.30591 5.6 42.1567 + vertex -2.74052 4.8417 41.7778 + endloop + endfacet + facet normal -0.74332 -0.57213 -0.346616 + outer loop + vertex -3.04674 5.6 41.601 + vertex -3.75688 6.28538 41.9926 + vertex -3.46236 6.28538 41.361 + endloop + endfacet + facet normal -0.743319 -0.572097 -0.346673 + outer loop + vertex -3.30591 5.6 42.1567 + vertex -3.75688 6.28538 41.9926 + vertex -3.04674 5.6 41.601 + endloop + endfacet + facet normal -0.642086 -0.705755 -0.299392 + outer loop + vertex -3.46236 6.28538 41.361 + vertex -4.31283 6.87701 41.7903 + vertex -3.97473 6.87701 41.0652 + endloop + endfacet + facet normal -0.642088 -0.705745 -0.299411 + outer loop + vertex -3.75688 6.28538 41.9926 + vertex -4.31283 6.87701 41.7903 + vertex -3.46236 6.28538 41.361 + endloop + endfacet + facet normal -0.521162 -0.818122 -0.243038 + outer loop + vertex -3.97473 6.87701 41.0652 + vertex -4.95688 7.35692 41.5558 + vertex -4.56828 7.35692 40.7225 + endloop + endfacet + facet normal -0.521152 -0.818138 -0.243003 + outer loop + vertex -4.31283 6.87701 41.7903 + vertex -4.95688 7.35692 41.5558 + vertex -3.97473 6.87701 41.0652 + endloop + endfacet + facet normal -0.384225 -0.90569 -0.179157 + outer loop + vertex -4.56828 7.35692 40.7225 + vertex -5.66945 7.71053 41.2965 + vertex -5.22499 7.71053 40.3433 + endloop + endfacet + facet normal -0.384236 -0.90568 -0.179184 + outer loop + vertex -4.95688 7.35692 41.5558 + vertex -5.66945 7.71053 41.2965 + vertex -4.56828 7.35692 40.7225 + endloop + endfacet + facet normal -0.235412 -0.965676 -0.109777 + outer loop + vertex -5.22499 7.71053 40.3433 + vertex -6.4289 7.92708 41.0201 + vertex -5.9249 7.92708 39.9393 + endloop + endfacet + facet normal -0.235405 -0.965679 -0.109765 + outer loop + vertex -5.66945 7.71053 41.2965 + vertex -6.4289 7.92708 41.0201 + vertex -5.22499 7.71053 40.3433 + endloop + endfacet + facet normal -0.0792847 -0.996166 -0.0369712 + outer loop + vertex -5.9249 7.92708 39.9393 + vertex -7.21214 8 40.735 + vertex -6.64674 8 39.5225 + endloop + endfacet + facet normal -0.0792855 -0.996166 -0.0369725 + outer loop + vertex -6.4289 7.92708 41.0201 + vertex -7.21214 8 40.735 + vertex -5.9249 7.92708 39.9393 + endloop + endfacet + facet normal -0.962271 -0.0868086 -0.257872 + outer loop + vertex -2.70162 3.2 42.3767 + vertex -2.90314 4.03351 42.8481 + vertex -2.77014 4.03351 42.3518 + endloop + endfacet + facet normal -0.962284 -0.0868441 -0.257815 + outer loop + vertex -2.83132 3.2 42.8608 + vertex -2.90314 4.03351 42.8481 + vertex -2.70162 3.2 42.3767 + endloop + endfacet + facet normal -0.933246 -0.257906 -0.250074 + outer loop + vertex -2.77014 4.03351 42.3518 + vertex -3.1164 4.8417 42.8105 + vertex -2.97363 4.8417 42.2777 + endloop + endfacet + facet normal -0.933244 -0.257894 -0.250094 + outer loop + vertex -2.90314 4.03351 42.8481 + vertex -3.1164 4.8417 42.8105 + vertex -2.77014 4.03351 42.3518 + endloop + endfacet + facet normal -0.876017 -0.421314 -0.234709 + outer loop + vertex -2.97363 4.8417 42.2777 + vertex -3.46463 5.6 42.7491 + vertex -3.30591 5.6 42.1567 + endloop + endfacet + facet normal -0.876018 -0.421296 -0.234739 + outer loop + vertex -3.1164 4.8417 42.8105 + vertex -3.46463 5.6 42.7491 + vertex -2.97363 4.8417 42.2777 + endloop + endfacet + facet normal -0.792238 -0.572103 -0.212264 + outer loop + vertex -3.30591 5.6 42.1567 + vertex -3.93725 6.28538 42.6658 + vertex -3.75688 6.28538 41.9926 + endloop + endfacet + facet normal -0.792238 -0.572105 -0.212262 + outer loop + vertex -3.46463 5.6 42.7491 + vertex -3.93725 6.28538 42.6658 + vertex -3.30591 5.6 42.1567 + endloop + endfacet + facet normal -0.684317 -0.70575 -0.183376 + outer loop + vertex -3.75688 6.28538 41.9926 + vertex -4.51989 6.87701 42.563 + vertex -4.31283 6.87701 41.7903 + endloop + endfacet + facet normal -0.684308 -0.705767 -0.183346 + outer loop + vertex -3.93725 6.28538 42.6658 + vertex -4.51989 6.87701 42.563 + vertex -3.75688 6.28538 41.9926 + endloop + endfacet + facet normal -0.555439 -0.818131 -0.148822 + outer loop + vertex -4.31283 6.87701 41.7903 + vertex -5.19486 7.35692 42.444 + vertex -4.95688 7.35692 41.5558 + endloop + endfacet + facet normal -0.55545 -0.818121 -0.148844 + outer loop + vertex -4.51989 6.87701 42.563 + vertex -5.19486 7.35692 42.444 + vertex -4.31283 6.87701 41.7903 + endloop + endfacet + facet normal -0.409511 -0.905682 -0.109731 + outer loop + vertex -4.95688 7.35692 41.5558 + vertex -5.94164 7.71053 42.3123 + vertex -5.66945 7.71053 41.2965 + endloop + endfacet + facet normal -0.409504 -0.905686 -0.109721 + outer loop + vertex -5.19486 7.35692 42.444 + vertex -5.94164 7.71053 42.3123 + vertex -4.95688 7.35692 41.5558 + endloop + endfacet + facet normal -0.250888 -0.965679 -0.0672251 + outer loop + vertex -5.66945 7.71053 41.2965 + vertex -6.73755 7.92708 42.172 + vertex -6.4289 7.92708 41.0201 + endloop + endfacet + facet normal -0.25089 -0.965678 -0.0672275 + outer loop + vertex -5.94164 7.71053 42.3123 + vertex -6.73755 7.92708 42.172 + vertex -5.66945 7.71053 41.2965 + endloop + endfacet + facet normal -0.084502 -0.996166 -0.0226415 + outer loop + vertex -6.4289 7.92708 41.0201 + vertex -7.5584 8 42.0273 + vertex -7.21214 8 40.735 + endloop + endfacet + facet normal -0.0845027 -0.996166 -0.0226424 + outer loop + vertex -6.73755 7.92708 42.172 + vertex -7.5584 8 42.0273 + vertex -6.4289 7.92708 41.0201 + endloop + endfacet + facet normal -0.992433 -0.0868365 -0.086816 + outer loop + vertex -2.83132 3.2 42.8608 + vertex -2.94792 4.03351 43.36 + vertex -2.90314 4.03351 42.8481 + endloop + endfacet + facet normal -0.992432 -0.0868233 -0.0868378 + outer loop + vertex -2.875 3.2 43.36 + vertex -2.94792 4.03351 43.36 + vertex -2.83132 3.2 42.8608 + endloop + endfacet + facet normal -0.962497 -0.257895 -0.0841988 + outer loop + vertex -2.90314 4.03351 42.8481 + vertex -3.16447 4.8417 43.36 + vertex -3.1164 4.8417 42.8105 + endloop + endfacet + facet normal -0.962497 -0.257896 -0.0841973 + outer loop + vertex -2.94792 4.03351 43.36 + vertex -3.16447 4.8417 43.36 + vertex -2.90314 4.03351 42.8481 + endloop + endfacet + facet normal -0.903471 -0.421297 -0.0790482 + outer loop + vertex -3.1164 4.8417 42.8105 + vertex -3.51808 5.6 43.36 + vertex -3.46463 5.6 42.7491 + endloop + endfacet + facet normal -0.903469 -0.421305 -0.079035 + outer loop + vertex -3.16447 4.8417 43.36 + vertex -3.51808 5.6 43.36 + vertex -3.1164 4.8417 42.8105 + endloop + endfacet + facet normal -0.817056 -0.572109 -0.0714895 + outer loop + vertex -3.46463 5.6 42.7491 + vertex -3.99799 6.28538 43.36 + vertex -3.93725 6.28538 42.6658 + endloop + endfacet + facet normal -0.817055 -0.57211 -0.0714873 + outer loop + vertex -3.51808 5.6 43.36 + vertex -3.99799 6.28538 43.36 + vertex -3.46463 5.6 42.7491 + endloop + endfacet + facet normal -0.705755 -0.70576 -0.0617469 + outer loop + vertex -3.93725 6.28538 42.6658 + vertex -4.58962 6.87701 43.36 + vertex -4.51989 6.87701 42.563 + endloop + endfacet + facet normal -0.705757 -0.705757 -0.0617512 + outer loop + vertex -3.99799 6.28538 43.36 + vertex -4.58962 6.87701 43.36 + vertex -3.93725 6.28538 42.6658 + endloop + endfacet + facet normal -0.572856 -0.818122 -0.0501187 + outer loop + vertex -4.51989 6.87701 42.563 + vertex -5.275 7.35692 43.36 + vertex -5.19486 7.35692 42.444 + endloop + endfacet + facet normal -0.572857 -0.818122 -0.0501196 + outer loop + vertex -4.58962 6.87701 43.36 + vertex -5.275 7.35692 43.36 + vertex -4.51989 6.87701 42.563 + endloop + endfacet + facet normal -0.422337 -0.905685 -0.036949 + outer loop + vertex -5.19486 7.35692 42.444 + vertex -6.0333 7.71053 43.36 + vertex -5.94164 7.71053 42.3123 + endloop + endfacet + facet normal -0.422338 -0.905685 -0.03695 + outer loop + vertex -5.275 7.35692 43.36 + vertex -6.0333 7.71053 43.36 + vertex -5.19486 7.35692 42.444 + endloop + endfacet + facet normal -0.25875 -0.965679 -0.0226384 + outer loop + vertex -5.94164 7.71053 42.3123 + vertex -6.84149 7.92708 43.36 + vertex -6.73755 7.92708 42.172 + endloop + endfacet + facet normal -0.258748 -0.965679 -0.0226371 + outer loop + vertex -6.0333 7.71053 43.36 + vertex -6.84149 7.92708 43.36 + vertex -5.94164 7.71053 42.3123 + endloop + endfacet + facet normal -0.08715 -0.996166 -0.00762489 + outer loop + vertex -6.73755 7.92708 42.172 + vertex -7.675 8 43.36 + vertex -7.5584 8 42.0273 + endloop + endfacet + facet normal -0.08715 -0.996166 -0.00762489 + outer loop + vertex -6.84149 7.92708 43.36 + vertex -7.675 8 43.36 + vertex -6.73755 7.92708 42.172 + endloop + endfacet + facet normal 0.996194 -0 0.087167 + outer loop + vertex 2.83132 1.5748 43.8592 + vertex 2.875 3.2 43.36 + vertex 2.83132 3.2 43.8592 + endloop + endfacet + facet normal 0.996194 0 0.087167 + outer loop + vertex 2.875 3.2 43.36 + vertex 2.83132 1.5748 43.8592 + vertex 2.875 1.5748 43.36 + endloop + endfacet + facet normal -0.996194 0 0.087167 + outer loop + vertex -2.875 1.5748 43.36 + vertex -2.83132 3.2 43.8592 + vertex -2.875 3.2 43.36 + endloop + endfacet + facet normal -0.996194 0 0.087167 + outer loop + vertex -2.83132 3.2 43.8592 + vertex -2.875 1.5748 43.36 + vertex -2.83132 1.5748 43.8592 + endloop + endfacet + facet normal 0.0872 0 0.996191 + outer loop + vertex 0 3.2 46.235 + vertex 0.499238 1.5748 46.1913 + vertex 0.499238 3.2 46.1913 + endloop + endfacet + facet normal 0.0872 0 0.996191 + outer loop + vertex 0.499238 1.5748 46.1913 + vertex 0 3.2 46.235 + vertex 0 1.5748 46.235 + endloop + endfacet + facet normal 0.707137 0 0.707077 + outer loop + vertex 1.84801 3.2 45.5624 + vertex 2.20238 1.5748 45.208 + vertex 2.20238 3.2 45.208 + endloop + endfacet + facet normal 0.707137 0 0.707077 + outer loop + vertex 2.20238 1.5748 45.208 + vertex 1.84801 3.2 45.5624 + vertex 1.84801 1.5748 45.5624 + endloop + endfacet + facet normal -0.57352 0 0.819192 + outer loop + vertex -1.84801 3.2 45.5624 + vertex -1.4375 1.5748 45.8498 + vertex -1.4375 3.2 45.8498 + endloop + endfacet + facet normal -0.57352 0 0.819192 + outer loop + vertex -1.4375 1.5748 45.8498 + vertex -1.84801 3.2 45.5624 + vertex -1.84801 1.5748 45.5624 + endloop + endfacet + facet normal -0.258808 0 0.965929 + outer loop + vertex -0.983308 3.2 46.0616 + vertex -0.499238 1.5748 46.1913 + vertex -0.499238 3.2 46.1913 + endloop + endfacet + facet normal -0.258808 0 0.965929 + outer loop + vertex -0.499238 1.5748 46.1913 + vertex -0.983308 3.2 46.0616 + vertex -0.983308 1.5748 46.0616 + endloop + endfacet + facet normal 0.906305 -0 0.422623 + outer loop + vertex 2.48982 1.5748 44.7975 + vertex 2.70162 3.2 44.3433 + vertex 2.48982 3.2 44.7975 + endloop + endfacet + facet normal 0.906305 0 0.422623 + outer loop + vertex 2.70162 3.2 44.3433 + vertex 2.48982 1.5748 44.7975 + vertex 2.70162 1.5748 44.3433 + endloop + endfacet + facet normal 0.965933 -0 0.258793 + outer loop + vertex 2.70162 1.5748 44.3433 + vertex 2.83132 3.2 43.8592 + vertex 2.70162 3.2 44.3433 + endloop + endfacet + facet normal 0.965933 0 0.258793 + outer loop + vertex 2.83132 3.2 43.8592 + vertex 2.70162 1.5748 44.3433 + vertex 2.83132 1.5748 43.8592 + endloop + endfacet + facet normal 0.819148 -0 0.573583 + outer loop + vertex 2.20238 1.5748 45.208 + vertex 2.48982 3.2 44.7975 + vertex 2.20238 3.2 45.208 + endloop + endfacet + facet normal 0.819148 0 0.573583 + outer loop + vertex 2.48982 3.2 44.7975 + vertex 2.20238 1.5748 45.208 + vertex 2.48982 1.5748 44.7975 + endloop + endfacet + facet normal 0.422629 0 0.906303 + outer loop + vertex 0.983308 3.2 46.0616 + vertex 1.4375 1.5748 45.8498 + vertex 1.4375 3.20001 45.8498 + endloop + endfacet + facet normal 0.422629 0 0.906303 + outer loop + vertex 1.4375 1.5748 45.8498 + vertex 0.983308 3.2 46.0616 + vertex 0.983308 1.5748 46.0616 + endloop + endfacet + facet normal 0.258808 0 0.965929 + outer loop + vertex 0.499238 3.2 46.1913 + vertex 0.983308 1.5748 46.0616 + vertex 0.983308 3.2 46.0616 + endloop + endfacet + facet normal 0.258808 0 0.965929 + outer loop + vertex 0.983308 1.5748 46.0616 + vertex 0.499238 3.2 46.1913 + vertex 0.499238 1.5748 46.1913 + endloop + endfacet + facet normal 0.57352 0 0.819192 + outer loop + vertex 1.4375 3.20001 45.8498 + vertex 1.84801 1.5748 45.5624 + vertex 1.84801 3.2 45.5624 + endloop + endfacet + facet normal 0.57352 0 0.819192 + outer loop + vertex 1.4375 3.20001 45.8498 + vertex 1.4375 1.5748 45.8498 + vertex 1.84801 1.5748 45.5624 + endloop + endfacet + facet normal -0.819148 0 0.573583 + outer loop + vertex -2.48982 1.5748 44.7975 + vertex -2.20238 3.2 45.208 + vertex -2.48982 3.20001 44.7975 + endloop + endfacet + facet normal -0.819148 0 0.573583 + outer loop + vertex -2.20238 3.2 45.208 + vertex -2.48982 1.5748 44.7975 + vertex -2.20238 1.5748 45.208 + endloop + endfacet + facet normal -0.707137 0 0.707077 + outer loop + vertex -2.20238 3.2 45.208 + vertex -1.84801 1.5748 45.5624 + vertex -1.84801 3.2 45.5624 + endloop + endfacet + facet normal -0.707137 0 0.707077 + outer loop + vertex -1.84801 1.5748 45.5624 + vertex -2.20238 3.2 45.208 + vertex -2.20238 1.5748 45.208 + endloop + endfacet + facet normal -0.422629 0 0.906303 + outer loop + vertex -1.4375 3.2 45.8498 + vertex -0.983308 1.5748 46.0616 + vertex -0.983308 3.2 46.0616 + endloop + endfacet + facet normal -0.422629 0 0.906303 + outer loop + vertex -0.983308 1.5748 46.0616 + vertex -1.4375 3.2 45.8498 + vertex -1.4375 1.5748 45.8498 + endloop + endfacet + facet normal -0.0872 0 0.996191 + outer loop + vertex -0.499238 3.2 46.1913 + vertex 0 1.5748 46.235 + vertex 0 3.2 46.235 + endloop + endfacet + facet normal -0.0872 0 0.996191 + outer loop + vertex 0 1.5748 46.235 + vertex -0.499238 3.2 46.1913 + vertex -0.499238 1.5748 46.1913 + endloop + endfacet + facet normal 0.996194 0 -0.087167 + outer loop + vertex 2.875 1.5748 43.36 + vertex 2.83132 3.2 42.8608 + vertex 2.875 3.2 43.36 + endloop + endfacet + facet normal 0.996194 0 -0.087167 + outer loop + vertex 2.83132 3.2 42.8608 + vertex 2.875 1.5748 43.36 + vertex 2.83132 1.5748 42.8608 + endloop + endfacet + facet normal 0.57352 0 -0.819192 + outer loop + vertex 1.4375 1.5748 40.8702 + vertex 1.84801 3.2 41.1576 + vertex 1.84801 1.5748 41.1576 + endloop + endfacet + facet normal 0.57352 0 -0.819192 + outer loop + vertex 1.84801 3.2 41.1576 + vertex 1.4375 1.5748 40.8702 + vertex 1.4375 3.2 40.8702 + endloop + endfacet + facet normal -0.707137 0 -0.707077 + outer loop + vertex -2.20238 1.5748 41.512 + vertex -1.84801 3.2 41.1576 + vertex -1.84801 1.5748 41.1576 + endloop + endfacet + facet normal -0.707137 0 -0.707077 + outer loop + vertex -1.84801 3.2 41.1576 + vertex -2.20238 1.5748 41.512 + vertex -2.20238 3.2 41.512 + endloop + endfacet + facet normal -0.906305 -0 -0.422623 + outer loop + vertex -2.48982 3.20001 41.9225 + vertex -2.48982 1.5748 41.9225 + vertex -2.70162 3.2 42.3767 + endloop + endfacet + facet normal -0.906305 -0 -0.422623 + outer loop + vertex -2.70162 3.2 42.3767 + vertex -2.48982 1.5748 41.9225 + vertex -2.70162 1.5748 42.3767 + endloop + endfacet + facet normal -0.996194 0 -0.087167 + outer loop + vertex -2.83132 1.5748 42.8608 + vertex -2.875 3.2 43.36 + vertex -2.83132 3.2 42.8608 + endloop + endfacet + facet normal -0.996194 -0 -0.087167 + outer loop + vertex -2.875 3.2 43.36 + vertex -2.83132 1.5748 42.8608 + vertex -2.875 1.5748 43.36 + endloop + endfacet + facet normal -0.906305 0 0.422623 + outer loop + vertex -2.70162 1.5748 44.3433 + vertex -2.48982 3.20001 44.7975 + vertex -2.70162 3.2 44.3433 + endloop + endfacet + facet normal -0.906305 0 0.422623 + outer loop + vertex -2.48982 3.20001 44.7975 + vertex -2.70162 1.5748 44.3433 + vertex -2.48982 1.5748 44.7975 + endloop + endfacet + facet normal -0.965933 0 0.258793 + outer loop + vertex -2.83132 1.5748 43.8592 + vertex -2.70162 3.2 44.3433 + vertex -2.83132 3.2 43.8592 + endloop + endfacet + facet normal -0.965933 0 0.258793 + outer loop + vertex -2.70162 3.2 44.3433 + vertex -2.83132 1.5748 43.8592 + vertex -2.70162 1.5748 44.3433 + endloop + endfacet + facet normal 0.0872 0 -0.996191 + outer loop + vertex 0 1.5748 40.485 + vertex 0.499238 3.2 40.5287 + vertex 0.499238 1.5748 40.5287 + endloop + endfacet + facet normal 0.0872 0 -0.996191 + outer loop + vertex 0.499238 3.2 40.5287 + vertex 0 1.5748 40.485 + vertex 0 3.2 40.485 + endloop + endfacet + facet normal 0.422629 0 -0.906303 + outer loop + vertex 0.983308 1.5748 40.6584 + vertex 1.4375 3.2 40.8702 + vertex 1.4375 1.5748 40.8702 + endloop + endfacet + facet normal 0.422629 0 -0.906303 + outer loop + vertex 1.4375 3.2 40.8702 + vertex 0.983308 1.5748 40.6584 + vertex 0.983308 3.2 40.6584 + endloop + endfacet + facet normal 0.707137 0 -0.707077 + outer loop + vertex 1.84801 1.5748 41.1576 + vertex 2.20238 3.2 41.512 + vertex 2.20238 1.5748 41.512 + endloop + endfacet + facet normal 0.707137 0 -0.707077 + outer loop + vertex 2.20238 3.2 41.512 + vertex 1.84801 1.5748 41.1576 + vertex 1.84801 3.2 41.1576 + endloop + endfacet + facet normal 0.965933 0 -0.258793 + outer loop + vertex 2.83132 1.5748 42.8608 + vertex 2.70162 3.2 42.3767 + vertex 2.83132 3.2 42.8608 + endloop + endfacet + facet normal 0.965933 0 -0.258793 + outer loop + vertex 2.70162 3.2 42.3767 + vertex 2.83132 1.5748 42.8608 + vertex 2.70162 1.5748 42.3767 + endloop + endfacet + facet normal 0.819148 0 -0.573583 + outer loop + vertex 2.48982 1.5748 41.9225 + vertex 2.20238 3.2 41.512 + vertex 2.48982 3.2 41.9225 + endloop + endfacet + facet normal 0.819148 0 -0.573583 + outer loop + vertex 2.20238 3.2 41.512 + vertex 2.48982 1.5748 41.9225 + vertex 2.20238 1.5748 41.512 + endloop + endfacet + facet normal -0.57352 0 -0.819192 + outer loop + vertex -1.84801 1.5748 41.1576 + vertex -1.4375 3.20001 40.8702 + vertex -1.4375 1.5748 40.8702 + endloop + endfacet + facet normal -0.57352 0 -0.819192 + outer loop + vertex -1.4375 3.20001 40.8702 + vertex -1.84801 1.5748 41.1576 + vertex -1.84801 3.2 41.1576 + endloop + endfacet + facet normal -0.422629 0 -0.906303 + outer loop + vertex -0.983308 1.5748 40.6584 + vertex -1.4375 3.20001 40.8702 + vertex -0.983308 3.2 40.6584 + endloop + endfacet + facet normal -0.422629 0 -0.906303 + outer loop + vertex -1.4375 1.5748 40.8702 + vertex -1.4375 3.20001 40.8702 + vertex -0.983308 1.5748 40.6584 + endloop + endfacet + facet normal -0.0872 0 -0.996191 + outer loop + vertex -0.499238 1.5748 40.5287 + vertex 0 3.2 40.485 + vertex 0 1.5748 40.485 + endloop + endfacet + facet normal -0.0872 0 -0.996191 + outer loop + vertex 0 3.2 40.485 + vertex -0.499238 1.5748 40.5287 + vertex -0.499238 3.2 40.5287 + endloop + endfacet + facet normal -0.819148 0 -0.573583 + outer loop + vertex -2.20238 1.5748 41.512 + vertex -2.48982 3.20001 41.9225 + vertex -2.20238 3.2 41.512 + endloop + endfacet + facet normal -0.819148 -0 -0.573583 + outer loop + vertex -2.48982 3.20001 41.9225 + vertex -2.20238 1.5748 41.512 + vertex -2.48982 1.5748 41.9225 + endloop + endfacet + facet normal -0.965933 0 -0.258793 + outer loop + vertex -2.70162 1.5748 42.3767 + vertex -2.83132 3.2 42.8608 + vertex -2.70162 3.2 42.3767 + endloop + endfacet + facet normal -0.965933 -0 -0.258793 + outer loop + vertex -2.83132 3.2 42.8608 + vertex -2.70162 1.5748 42.3767 + vertex -2.83132 1.5748 42.8608 + endloop + endfacet + facet normal 0.258808 0 -0.965929 + outer loop + vertex 0.499238 1.5748 40.5287 + vertex 0.983308 3.2 40.6584 + vertex 0.983308 1.5748 40.6584 + endloop + endfacet + facet normal 0.258808 0 -0.965929 + outer loop + vertex 0.983308 3.2 40.6584 + vertex 0.499238 1.5748 40.5287 + vertex 0.499238 3.2 40.5287 + endloop + endfacet + facet normal 0.906305 0 -0.422623 + outer loop + vertex 2.70162 1.5748 42.3767 + vertex 2.48982 3.2 41.9225 + vertex 2.70162 3.2 42.3767 + endloop + endfacet + facet normal 0.906305 0 -0.422623 + outer loop + vertex 2.48982 3.2 41.9225 + vertex 2.70162 1.5748 42.3767 + vertex 2.48982 1.5748 41.9225 + endloop + endfacet + facet normal -0.258808 0 -0.965929 + outer loop + vertex -0.983308 1.5748 40.6584 + vertex -0.499238 3.2 40.5287 + vertex -0.499238 1.5748 40.5287 + endloop + endfacet + facet normal -0.258808 0 -0.965929 + outer loop + vertex -0.499238 3.2 40.5287 + vertex -0.983308 1.5748 40.6584 + vertex -0.983308 3.2 40.6584 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -0.21706 0.0999994 44.591 + vertex 0 0.0999994 45.485 + vertex -1.0625 0.0999994 45.2003 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.21706 0.0999994 44.591 + vertex 0 0.0999994 45.485 + vertex 0 0.0999994 44.61 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 0 0.0999994 45.485 + vertex -0.21706 0.0999994 44.591 + vertex 0 0.0999994 44.61 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 0.0999994 45.2003 + vertex -0.427525 0.0999994 44.5346 + vertex -0.21706 0.0999994 44.591 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 0.0999994 45.2003 + vertex -0.624999 0.0999994 44.4425 + vertex -0.427525 0.0999994 44.5346 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -0.803484 0.0999994 44.3176 + vertex -1.0625 0.0999994 45.2003 + vertex -1.8403 0.0999994 44.4225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 0.0999994 45.2003 + vertex -0.803484 0.0999994 44.3176 + vertex -0.624999 0.0999994 44.4425 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.8403 0.0999994 44.4225 + vertex -0.957555 0.0999994 44.1635 + vertex -0.803484 0.0999994 44.3176 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.8403 0.0999994 44.4225 + vertex -1.08253 0.0999994 43.985 + vertex -0.957555 0.0999994 44.1635 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.8403 0.0999994 44.4225 + vertex -1.17461 0.0999994 43.7875 + vertex -1.08253 0.0999994 43.985 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.8403 0.0999994 44.4225 + vertex -1.23101 0.0999994 43.5771 + vertex -1.17461 0.0999994 43.7875 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.125 0.0999994 43.36 + vertex -1.23101 0.0999994 43.5771 + vertex -1.8403 0.0999994 44.4225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.125 0.0999994 43.36 + vertex -1.25 0.0999994 43.36 + vertex -1.23101 0.0999994 43.5771 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.125 0.0999994 43.36 + vertex -1.23101 0.0999994 43.1429 + vertex -1.25 0.0999994 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.125 0.0999994 43.36 + vertex -1.17461 0.0999994 42.9325 + vertex -1.23101 0.0999994 43.1429 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.8403 0.0999994 42.2975 + vertex -1.17461 0.0999994 42.9325 + vertex -2.125 0.0999994 43.36 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -1.17461 0.0999994 42.9325 + vertex -1.8403 0.0999994 42.2975 + vertex -1.08253 0.0999994 42.735 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -1.08253 0.0999994 42.735 + vertex -1.8403 0.0999994 42.2975 + vertex -0.957555 0.0999994 42.5565 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -0.957555 0.0999994 42.5565 + vertex -1.8403 0.0999994 42.2975 + vertex -0.803484 0.0999994 42.4024 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 0.0999994 41.5197 + vertex -0.803484 0.0999994 42.4024 + vertex -1.8403 0.0999994 42.2975 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -0.803484 0.0999994 42.4024 + vertex -1.0625 0.0999994 41.5197 + vertex -0.625 0.0999994 42.2775 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -0.625 0.0999994 42.2775 + vertex -1.0625 0.0999994 41.5197 + vertex -0.427525 0.0999994 42.1854 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 0.0999994 41.235 + vertex -0.21706 0.0999994 42.129 + vertex -1.0625 0.0999994 41.5197 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -0.427525 0.0999994 42.1854 + vertex -1.0625 0.0999994 41.5197 + vertex -0.21706 0.0999994 42.129 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 0.0999994 45.485 + vertex 0.21706 0.0999994 44.591 + vertex 1.0625 0.0999994 45.2003 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.427525 0.0999994 44.5346 + vertex 1.0625 0.0999994 45.2003 + vertex 0.21706 0.0999994 44.591 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.625 0.0999994 44.4425 + vertex 1.0625 0.0999994 45.2003 + vertex 0.427525 0.0999994 44.5346 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.803484 0.0999994 44.3176 + vertex 1.0625 0.0999994 45.2003 + vertex 0.625 0.0999994 44.4425 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 1.0625 0.0999994 45.2003 + vertex 0.803484 0.0999994 44.3176 + vertex 1.8403 0.0999994 44.4225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.957555 0.0999994 44.1635 + vertex 1.8403 0.0999994 44.4225 + vertex 0.803484 0.0999994 44.3176 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.08253 0.0999994 43.985 + vertex 1.8403 0.0999994 44.4225 + vertex 0.957555 0.0999994 44.1635 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.17461 0.0999994 43.7875 + vertex 1.8403 0.0999994 44.4225 + vertex 1.08253 0.0999994 43.985 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.23101 0.0999994 43.5771 + vertex 1.8403 0.0999994 44.4225 + vertex 1.17461 0.0999994 43.7875 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.125 0.0999994 43.36 + vertex 1.23101 0.0999994 43.5771 + vertex 1.25 0.0999994 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.23101 0.0999994 43.5771 + vertex 2.125 0.0999994 43.36 + vertex 1.8403 0.0999994 44.4225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.23101 0.0999994 43.1429 + vertex 2.125 0.0999994 43.36 + vertex 1.25 0.0999994 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.17461 0.0999994 42.9325 + vertex 2.125 0.0999994 43.36 + vertex 1.23101 0.0999994 43.1429 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 0.0999994 42.2975 + vertex 1.17461 0.0999994 42.9325 + vertex 1.08253 0.0999994 42.735 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 0.0999994 42.2975 + vertex 1.08253 0.0999994 42.735 + vertex 0.957555 0.0999994 42.5565 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 0.0999994 42.2975 + vertex 0.957555 0.0999994 42.5565 + vertex 0.803484 0.0999994 42.4024 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.17461 0.0999994 42.9325 + vertex 1.8403 0.0999994 42.2975 + vertex 2.125 0.0999994 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.0625 0.0999994 41.5197 + vertex 0.803484 0.0999994 42.4024 + vertex 0.624999 0.0999994 42.2775 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.0625 0.0999994 41.5197 + vertex 0.624999 0.0999994 42.2775 + vertex 0.427525 0.0999994 42.1854 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.0625 0.0999994 41.5197 + vertex 0.427525 0.0999994 42.1854 + vertex 0.21706 0.0999994 42.129 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -0.21706 0.0999994 42.129 + vertex 0 0.0999994 41.235 + vertex 0 0.0999994 42.11 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 0.0999994 42.11 + vertex 0 0.0999994 41.235 + vertex 0.21706 0.0999994 42.129 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.803484 0.0999994 42.4024 + vertex 1.0625 0.0999994 41.5197 + vertex 1.8403 0.0999994 42.2975 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 0.21706 0.0999994 42.129 + vertex 0 0.0999994 41.235 + vertex 1.0625 0.0999994 41.5197 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.1535 3.99982 78.7367 + vertex -34.1535 4 78.7368 + vertex -34.1535 3.99982 78.7366 + endloop + endfacet + facet normal -0.996184 0 0.0872797 + outer loop + vertex -34.1535 3.99982 78.7366 + vertex -34.19 4 78.32 + vertex -34.19 3.99996 78.32 + endloop + endfacet + facet normal -0.99154 -0.096479 0.0868311 + outer loop + vertex -34.19 4 78.32 + vertex -34.1535 3.99982 78.7366 + vertex -34.1535 4 78.7368 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -34.19 3.99994 78.32 + vertex -34.19 3.99996 78.32 + vertex -34.19 4 78.32 + endloop + endfacet + facet normal -0.965957 0 0.258704 + outer loop + vertex -34.0453 3.99994 79.1408 + vertex -34.1535 4 78.7368 + vertex -34.1535 3.99994 78.7368 + endloop + endfacet + facet normal -1 0 -0 + outer loop + vertex -34.1535 3.99994 78.7368 + vertex -34.1535 4 78.7368 + vertex -34.1535 3.99982 78.7367 + endloop + endfacet + facet normal -0.965957 0 0.258704 + outer loop + vertex -34.1535 4 78.7368 + vertex -34.0453 3.99994 79.1408 + vertex -34.0453 4 79.1408 + endloop + endfacet + facet normal -0.90633 0 0.422571 + outer loop + vertex -33.8685 4 79.52 + vertex -34.0453 4 79.1408 + vertex -34.0453 3.99996 79.1408 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -34.0453 4 79.1408 + vertex -34.0453 3.99994 79.1408 + vertex -34.0453 3.99996 79.1408 + endloop + endfacet + facet normal -0.819141 0 0.573592 + outer loop + vertex -33.6285 3.99997 79.8627 + vertex -33.6285 4 79.8627 + vertex -33.8656 4 79.5241 + endloop + endfacet + facet normal -0.707107 0 0.707107 + outer loop + vertex -33.3327 3.99998 80.1585 + vertex -33.6285 4 79.8627 + vertex -33.6285 3.99998 79.8627 + endloop + endfacet + facet normal -0.707107 0 0.707107 + outer loop + vertex -33.6285 4 79.8627 + vertex -33.3327 3.99998 80.1585 + vertex -33.3327 4 80.1585 + endloop + endfacet + facet normal 0 0 -0 + outer loop + vertex -33.6285 3.99998 79.8627 + vertex -33.6285 4 79.8627 + vertex -33.6285 3.99997 79.8627 + endloop + endfacet + facet normal -0.573581 0 0.819149 + outer loop + vertex -33.0545 4 80.3533 + vertex -33.3327 4 80.1585 + vertex -33.3327 3.99998 80.1585 + endloop + endfacet + facet normal 0 -0 1 + outer loop + vertex -32.6108 3.99999 80.5753 + vertex -32.6108 4 80.5753 + vertex -32.6109 3.99999 80.5753 + endloop + endfacet + facet normal -0.0973137 0.973137 0.20865 + outer loop + vertex -32.6109 3.99999 80.5753 + vertex -32.6108 4 80.5753 + vertex -32.8831 4 80.4483 + endloop + endfacet + facet normal -0.258704 0 0.965957 + outer loop + vertex -32.6108 4 80.5753 + vertex -32.2068 3.99999 80.6835 + vertex -32.2068 4 80.6835 + endloop + endfacet + facet normal -0.258704 0 0.965957 + outer loop + vertex -32.2068 3.99999 80.6835 + vertex -32.6108 4 80.5753 + vertex -32.6108 3.99999 80.5753 + endloop + endfacet + facet normal -0.0872381 0 0.996187 + outer loop + vertex -31.79 4 80.72 + vertex -32.2068 4 80.6835 + vertex -32.2068 3.99999 80.6835 + endloop + endfacet + facet normal -0.996158 0.00764171 0.0872356 + outer loop + vertex -34.1535 4 78.7368 + vertex -34.1535 4.6975 78.6757 + vertex -34.19 4 78.32 + endloop + endfacet + facet normal -0.996157 0.0076345 0.0872496 + outer loop + vertex -34.1535 4.6975 78.6757 + vertex -34.19 4.62513 78.2653 + vertex -34.19 4 78.32 + endloop + endfacet + facet normal -0.96571 0.0226069 0.258638 + outer loop + vertex -34.0453 4 79.1408 + vertex -34.0453 4.76767 79.0737 + vertex -34.1535 4 78.7368 + endloop + endfacet + facet normal -0.965732 0.0226486 0.25855 + outer loop + vertex -34.0453 4.76767 79.0737 + vertex -34.1535 4.6975 78.6757 + vertex -34.1535 4 78.7368 + endloop + endfacet + facet normal -0.905711 0.0369335 0.422283 + outer loop + vertex -33.8685 4 79.52 + vertex -33.8685 4.83351 79.4471 + vertex -34.0453 4 79.1408 + endloop + endfacet + facet normal -0.905693 0.0369143 0.422325 + outer loop + vertex -33.8685 4.83351 79.4471 + vertex -34.0453 4.76767 79.0737 + vertex -34.0453 4 79.1408 + endloop + endfacet + facet normal -0.815465 0.0482546 0.576792 + outer loop + vertex -33.8656 4 79.5241 + vertex -33.6285 4.89302 79.7846 + vertex -33.8685 4 79.52 + endloop + endfacet + facet normal -0.818112 0.0501011 0.572872 + outer loop + vertex -33.6285 4.89302 79.7846 + vertex -33.8656 4 79.5241 + vertex -33.6285 4 79.8627 + endloop + endfacet + facet normal -0.818083 0.0501078 0.572913 + outer loop + vertex -33.6285 4.89302 79.7846 + vertex -33.8685 4.83351 79.4471 + vertex -33.8685 4 79.52 + endloop + endfacet + facet normal -0.705758 0.0617284 0.705758 + outer loop + vertex -33.3327 4 80.1585 + vertex -33.3327 4.94439 80.0759 + vertex -33.6285 4 79.8627 + endloop + endfacet + facet normal -0.70575 0.0617236 0.705767 + outer loop + vertex -33.3327 4.94439 80.0759 + vertex -33.6285 4.89302 79.7846 + vertex -33.6285 4 79.8627 + endloop + endfacet + facet normal -0.57242 0.0714904 0.816838 + outer loop + vertex -32.99 4.98605 80.3122 + vertex -33.0545 4 80.3533 + vertex -32.99 4 80.3985 + endloop + endfacet + facet normal -0.572114 0.0714794 0.817054 + outer loop + vertex -33.0545 4 80.3533 + vertex -32.99 4.98605 80.3122 + vertex -33.3327 4 80.1585 + endloop + endfacet + facet normal -0.572082 0.0714647 0.817077 + outer loop + vertex -32.99 4.98605 80.3122 + vertex -33.3327 4.94439 80.0759 + vertex -33.3327 4 80.1585 + endloop + endfacet + facet normal -0.421361 0.0790806 0.903438 + outer loop + vertex -32.6108 5.01676 80.4863 + vertex -32.8831 4 80.4483 + vertex -32.6108 4 80.5753 + endloop + endfacet + facet normal -0.420963 0.0789667 0.903634 + outer loop + vertex -32.8831 4 80.4483 + vertex -32.6108 5.01676 80.4863 + vertex -32.99 4 80.3985 + endloop + endfacet + facet normal -0.421224 0.0790754 0.903503 + outer loop + vertex -32.99 4 80.3985 + vertex -32.6108 5.01676 80.4863 + vertex -32.99 4.98605 80.3122 + endloop + endfacet + facet normal -0.257785 0.0842103 0.962526 + outer loop + vertex -32.2068 4 80.6835 + vertex -32.2068 5.03556 80.5929 + vertex -32.6108 4 80.5753 + endloop + endfacet + facet normal -0.257886 0.08425 0.962495 + outer loop + vertex -32.2068 5.03556 80.5929 + vertex -32.6108 5.01676 80.4863 + vertex -32.6108 4 80.5753 + endloop + endfacet + facet normal -0.0869083 0.0868699 0.992422 + outer loop + vertex -31.79 4 80.72 + vertex -31.79 5.04189 80.6288 + vertex -32.2068 4 80.6835 + endloop + endfacet + facet normal -0.0867995 0.086827 0.992435 + outer loop + vertex -31.79 5.04189 80.6288 + vertex -32.2068 5.03556 80.5929 + vertex -32.2068 4 80.6835 + endloop + endfacet + facet normal -0.996157 0.0226661 0.0845989 + outer loop + vertex -34.1535 4.6975 78.6757 + vertex -34.1535 5.37381 78.4945 + vertex -34.19 4.62513 78.2653 + endloop + endfacet + facet normal -0.996157 0.0226661 0.0845988 + outer loop + vertex -34.1535 5.37381 78.4945 + vertex -34.19 5.23127 78.1029 + vertex -34.19 4.62513 78.2653 + endloop + endfacet + facet normal -0.965731 0.0671915 0.250697 + outer loop + vertex -34.0453 4.76767 79.0737 + vertex -34.0453 5.51202 78.8742 + vertex -34.1535 4.6975 78.6757 + endloop + endfacet + facet normal -0.965721 0.0671795 0.25074 + outer loop + vertex -34.0453 5.51202 78.8742 + vertex -34.1535 5.37381 78.4945 + vertex -34.1535 4.6975 78.6757 + endloop + endfacet + facet normal -0.905693 0.109744 0.409483 + outer loop + vertex -33.8685 4.83351 79.4471 + vertex -33.8685 5.6417 79.2305 + vertex -34.0453 4.76767 79.0737 + endloop + endfacet + facet normal -0.905697 0.109747 0.409473 + outer loop + vertex -33.8685 5.6417 79.2305 + vertex -34.0453 5.51202 78.8742 + vertex -34.0453 4.76767 79.0737 + endloop + endfacet + facet normal -0.818082 0.1489 0.555492 + outer loop + vertex -33.6285 4.89302 79.7846 + vertex -33.6285 5.7589 79.5525 + vertex -33.8685 4.83351 79.4471 + endloop + endfacet + facet normal -0.818053 0.148888 0.555538 + outer loop + vertex -33.6285 5.7589 79.5525 + vertex -33.8685 5.6417 79.2305 + vertex -33.8685 4.83351 79.4471 + endloop + endfacet + facet normal -0.705751 0.183392 0.684312 + outer loop + vertex -33.3327 4.94439 80.0759 + vertex -33.3327 5.86008 79.8305 + vertex -33.6285 4.89302 79.7846 + endloop + endfacet + facet normal -0.705809 0.183413 0.684247 + outer loop + vertex -33.3327 5.86008 79.8305 + vertex -33.6285 5.7589 79.5525 + vertex -33.6285 4.89302 79.7846 + endloop + endfacet + facet normal -0.572081 0.212293 0.792247 + outer loop + vertex -32.99 4.98605 80.3122 + vertex -32.99 5.94215 80.056 + vertex -33.3327 4.94439 80.0759 + endloop + endfacet + facet normal -0.572126 0.212308 0.79221 + outer loop + vertex -32.99 5.94215 80.056 + vertex -33.3327 5.86008 79.8305 + vertex -33.3327 4.94439 80.0759 + endloop + endfacet + facet normal -0.421226 0.23477 0.876043 + outer loop + vertex -32.6108 5.01676 80.4863 + vertex -32.6108 6.00262 80.2221 + vertex -32.99 4.98605 80.3122 + endloop + endfacet + facet normal -0.421178 0.234755 0.87607 + outer loop + vertex -32.6108 6.00262 80.2221 + vertex -32.99 5.94215 80.056 + vertex -32.99 4.98605 80.3122 + endloop + endfacet + facet normal -0.257887 0.250025 0.933264 + outer loop + vertex -32.2068 5.03556 80.5929 + vertex -32.2068 6.03965 80.3239 + vertex -32.6108 5.01676 80.4863 + endloop + endfacet + facet normal -0.25807 0.250087 0.933197 + outer loop + vertex -32.2068 6.03965 80.3239 + vertex -32.6108 6.00262 80.2221 + vertex -32.6108 5.01676 80.4863 + endloop + endfacet + facet normal -0.0868001 0.257762 0.962302 + outer loop + vertex -31.79 5.04189 80.6288 + vertex -31.79 6.05212 80.3582 + vertex -32.2068 5.03556 80.5929 + endloop + endfacet + facet normal -0.0869027 0.2578 0.962282 + outer loop + vertex -31.79 6.05212 80.3582 + vertex -32.2068 6.03965 80.3239 + vertex -32.2068 5.03556 80.5929 + endloop + endfacet + facet normal -0.996157 0.0370133 0.0793766 + outer loop + vertex -34.1535 5.37381 78.4945 + vertex -34.1535 6.00838 78.1986 + vertex -34.19 5.23127 78.1029 + endloop + endfacet + facet normal -0.996157 0.0370133 0.0793763 + outer loop + vertex -34.1535 6.00838 78.1986 + vertex -34.19 5.8 77.8377 + vertex -34.19 5.23127 78.1029 + endloop + endfacet + facet normal -0.965721 0.109684 0.235269 + outer loop + vertex -34.0453 5.51202 78.8742 + vertex -34.0453 6.21042 78.5486 + vertex -34.1535 5.37381 78.4945 + endloop + endfacet + facet normal -0.96573 0.109688 0.23523 + outer loop + vertex -34.0453 6.21042 78.5486 + vertex -34.1535 6.00838 78.1986 + vertex -34.1535 5.37381 78.4945 + endloop + endfacet + facet normal -0.905696 0.179159 0.384209 + outer loop + vertex -33.8685 5.6417 79.2305 + vertex -33.8685 6.4 78.8769 + vertex -34.0453 5.51202 78.8742 + endloop + endfacet + facet normal -0.905668 0.179153 0.384277 + outer loop + vertex -33.8685 6.4 78.8769 + vertex -34.0453 6.21042 78.5486 + vertex -34.0453 5.51202 78.8742 + endloop + endfacet + facet normal -0.818053 0.243041 0.521268 + outer loop + vertex -33.6285 5.7589 79.5525 + vertex -33.6285 6.57134 79.1737 + vertex -33.8685 5.6417 79.2305 + endloop + endfacet + facet normal -0.818085 0.243046 0.521216 + outer loop + vertex -33.6285 6.57134 79.1737 + vertex -33.8685 6.4 78.8769 + vertex -33.8685 5.6417 79.2305 + endloop + endfacet + facet normal -0.705806 0.299362 0.642044 + outer loop + vertex -33.3327 5.86008 79.8305 + vertex -33.3327 6.71925 79.4299 + vertex -33.6285 5.7589 79.5525 + endloop + endfacet + facet normal -0.705794 0.29936 0.642059 + outer loop + vertex -33.3327 6.71925 79.4299 + vertex -33.6285 6.57134 79.1737 + vertex -33.6285 5.7589 79.5525 + endloop + endfacet + facet normal -0.572124 0.346607 0.743329 + outer loop + vertex -32.99 5.94215 80.056 + vertex -32.99 6.83923 79.6377 + vertex -33.3327 5.86008 79.8305 + endloop + endfacet + facet normal -0.572089 0.346601 0.743358 + outer loop + vertex -32.99 6.83923 79.6377 + vertex -33.3327 6.71925 79.4299 + vertex -33.3327 5.86008 79.8305 + endloop + endfacet + facet normal -0.421184 0.383276 0.822012 + outer loop + vertex -32.6108 6.00262 80.2221 + vertex -32.6108 6.92763 79.7908 + vertex -32.99 5.94215 80.056 + endloop + endfacet + facet normal -0.421225 0.383285 0.821987 + outer loop + vertex -32.6108 6.92763 79.7908 + vertex -32.99 6.83923 79.6377 + vertex -32.99 5.94215 80.056 + endloop + endfacet + facet normal -0.258061 0.40829 0.875616 + outer loop + vertex -32.2068 6.03965 80.3239 + vertex -32.2068 6.98177 79.8846 + vertex -32.6108 6.00262 80.2221 + endloop + endfacet + facet normal -0.258017 0.408278 0.875635 + outer loop + vertex -32.2068 6.98177 79.8846 + vertex -32.6108 6.92763 79.7908 + vertex -32.6108 6.00262 80.2221 + endloop + endfacet + facet normal -0.0868975 0.421017 0.902881 + outer loop + vertex -31.79 6.05212 80.3582 + vertex -31.79 7 79.9162 + vertex -32.2068 6.03965 80.3239 + endloop + endfacet + facet normal -0.0868671 0.421007 0.902888 + outer loop + vertex -31.79 7 79.9162 + vertex -32.2068 6.98177 79.8846 + vertex -32.2068 6.03965 80.3239 + endloop + endfacet + facet normal -0.996157 0.0502349 0.0717423 + outer loop + vertex -34.1535 6.00838 78.1986 + vertex -34.1535 6.58192 77.797 + vertex -34.19 5.8 77.8377 + endloop + endfacet + facet normal -0.996157 0.0502352 0.0717488 + outer loop + vertex -34.1535 6.58192 77.797 + vertex -34.19 6.31403 77.4778 + vertex -34.19 5.8 77.8377 + endloop + endfacet + facet normal -0.96573 0.14887 0.212612 + outer loop + vertex -34.0453 6.21042 78.5486 + vertex -34.0453 6.84167 78.1066 + vertex -34.1535 6.00838 78.1986 + endloop + endfacet + facet normal -0.965731 0.14887 0.212607 + outer loop + vertex -34.0453 6.84167 78.1066 + vertex -34.1535 6.58192 77.797 + vertex -34.1535 6.00838 78.1986 + endloop + endfacet + facet normal -0.905672 0.243182 0.347305 + outer loop + vertex -33.8685 6.4 78.8769 + vertex -33.8685 7.08538 78.397 + vertex -34.0453 6.21042 78.5486 + endloop + endfacet + facet normal -0.905672 0.243182 0.347304 + outer loop + vertex -33.8685 7.08538 78.397 + vertex -34.0453 6.84167 78.1066 + vertex -34.0453 6.21042 78.5486 + endloop + endfacet + facet normal -0.818082 0.329875 0.471088 + outer loop + vertex -33.6285 6.57134 79.1737 + vertex -33.6285 7.30566 78.6595 + vertex -33.8685 6.4 78.8769 + endloop + endfacet + facet normal -0.818062 0.329877 0.471122 + outer loop + vertex -33.6285 7.30566 78.6595 + vertex -33.8685 7.08538 78.397 + vertex -33.8685 6.4 78.8769 + endloop + endfacet + facet normal -0.705792 0.406359 0.580284 + outer loop + vertex -33.3327 6.71925 79.4299 + vertex -33.3327 7.4958 78.8861 + vertex -33.6285 6.57134 79.1737 + endloop + endfacet + facet normal -0.705765 0.406361 0.580317 + outer loop + vertex -33.3327 7.4958 78.8861 + vertex -33.6285 7.30566 78.6595 + vertex -33.6285 6.57134 79.1737 + endloop + endfacet + facet normal -0.572093 0.470421 0.671873 + outer loop + vertex -32.99 6.83923 79.6377 + vertex -32.99 7.65004 79.07 + vertex -33.3327 6.71925 79.4299 + endloop + endfacet + facet normal -0.572211 0.470425 0.67177 + outer loop + vertex -32.99 7.65004 79.07 + vertex -33.3327 7.4958 78.8861 + vertex -33.3327 6.71925 79.4299 + endloop + endfacet + facet normal -0.421228 0.520199 0.74294 + outer loop + vertex -32.6108 6.92763 79.7908 + vertex -32.6108 7.76369 79.2054 + vertex -32.99 6.83923 79.6377 + endloop + endfacet + facet normal -0.421195 0.520195 0.742962 + outer loop + vertex -32.6108 7.76369 79.2054 + vertex -32.99 7.65004 79.07 + vertex -32.99 6.83923 79.6377 + endloop + endfacet + facet normal -0.258011 0.554193 0.791391 + outer loop + vertex -32.2068 6.98177 79.8846 + vertex -32.2068 7.83329 79.2883 + vertex -32.6108 6.92763 79.7908 + endloop + endfacet + facet normal -0.257875 0.554167 0.791454 + outer loop + vertex -32.2068 7.83329 79.2883 + vertex -32.6108 7.76369 79.2054 + vertex -32.6108 6.92763 79.7908 + endloop + endfacet + facet normal -0.086862 0.57142 0.816048 + outer loop + vertex -31.79 7 79.9162 + vertex -31.79 7.85672 79.3163 + vertex -32.2068 6.98177 79.8846 + endloop + endfacet + facet normal -0.0869423 0.571443 0.816024 + outer loop + vertex -31.79 7.85672 79.3163 + vertex -32.2068 7.83329 79.2883 + vertex -32.2068 6.98177 79.8846 + endloop + endfacet + facet normal -0.996158 0.0619264 0.0619252 + outer loop + vertex -34.19 6.75776 77.034 + vertex -34.1535 6.58192 77.797 + vertex -34.1535 7.07701 77.3019 + endloop + endfacet + facet normal -0.996157 0.0619374 0.0619277 + outer loop + vertex -34.1535 6.58192 77.797 + vertex -34.19 6.75776 77.034 + vertex -34.19 6.31403 77.4778 + endloop + endfacet + facet normal -0.965731 0.183526 0.183526 + outer loop + vertex -34.1535 7.07701 77.3019 + vertex -34.0453 6.84167 78.1066 + vertex -34.0453 7.38657 77.5617 + endloop + endfacet + facet normal -0.96573 0.183531 0.183527 + outer loop + vertex -34.0453 6.84167 78.1066 + vertex -34.1535 7.07701 77.3019 + vertex -34.1535 6.58192 77.797 + endloop + endfacet + facet normal -0.905683 0.299774 0.299789 + outer loop + vertex -34.0453 7.38657 77.5617 + vertex -33.8685 7.08538 78.397 + vertex -33.8685 7.67701 77.8054 + endloop + endfacet + facet normal -0.905675 0.299795 0.299795 + outer loop + vertex -33.8685 7.08538 78.397 + vertex -34.0453 7.38657 77.5617 + vertex -34.0453 6.84167 78.1066 + endloop + endfacet + facet normal -0.818085 0.406633 0.406678 + outer loop + vertex -33.8685 7.67701 77.8054 + vertex -33.6285 7.30566 78.6595 + vertex -33.6285 7.93953 78.0257 + endloop + endfacet + facet normal -0.818064 0.406666 0.406686 + outer loop + vertex -33.6285 7.30566 78.6595 + vertex -33.8685 7.67701 77.8054 + vertex -33.8685 7.08538 78.397 + endloop + endfacet + facet normal -0.705737 0.500951 0.500981 + outer loop + vertex -33.6285 7.93953 78.0257 + vertex -33.3327 7.4958 78.8861 + vertex -33.3327 8.16614 78.2158 + endloop + endfacet + facet normal -0.705766 0.500919 0.500974 + outer loop + vertex -33.3327 7.4958 78.8861 + vertex -33.6285 7.93953 78.0257 + vertex -33.6285 7.30566 78.6595 + endloop + endfacet + facet normal -0.572192 0.579949 0.579875 + outer loop + vertex -32.99 8.34995 78.37 + vertex -33.3327 7.4958 78.8861 + vertex -32.99 7.65004 79.07 + endloop + endfacet + facet normal -0.572046 0.579967 0.580002 + outer loop + vertex -33.3327 7.4958 78.8861 + vertex -32.99 8.34995 78.37 + vertex -33.3327 8.16614 78.2158 + endloop + endfacet + facet normal -0.421205 0.641321 0.641321 + outer loop + vertex -32.6108 8.48539 78.4837 + vertex -32.99 7.65004 79.07 + vertex -32.6108 7.76369 79.2054 + endloop + endfacet + facet normal -0.421332 0.641321 0.641238 + outer loop + vertex -32.99 7.65004 79.07 + vertex -32.6108 8.48539 78.4837 + vertex -32.99 8.34995 78.37 + endloop + endfacet + facet normal -0.257965 0.683151 0.683197 + outer loop + vertex -32.6108 8.48539 78.4837 + vertex -32.2068 7.83329 79.2883 + vertex -32.2068 8.56834 78.5533 + endloop + endfacet + facet normal -0.257887 0.683189 0.683189 + outer loop + vertex -32.2068 7.83329 79.2883 + vertex -32.6108 8.48539 78.4837 + vertex -32.6108 7.76369 79.2054 + endloop + endfacet + facet normal -0.0869212 0.704454 0.704407 + outer loop + vertex -31.79 8.59627 78.5767 + vertex -32.2068 7.83329 79.2883 + vertex -31.79 7.85672 79.3163 + endloop + endfacet + facet normal -0.0867535 0.704417 0.704465 + outer loop + vertex -32.2068 7.83329 79.2883 + vertex -31.79 8.59627 78.5767 + vertex -32.2068 8.56834 78.5533 + endloop + endfacet + facet normal -0.996158 0.0717367 0.0502345 + outer loop + vertex -34.1535 7.07701 77.3019 + vertex -34.1535 7.47861 76.7284 + vertex -34.19 6.75776 77.034 + endloop + endfacet + facet normal -0.996158 0.0717363 0.0502336 + outer loop + vertex -34.1535 7.47861 76.7284 + vertex -34.19 7.11769 76.52 + vertex -34.19 6.75776 77.034 + endloop + endfacet + facet normal -0.96573 0.212617 0.148862 + outer loop + vertex -34.0453 7.38657 77.5617 + vertex -34.0453 7.82857 76.9304 + vertex -34.1535 7.07701 77.3019 + endloop + endfacet + facet normal -0.96572 0.212634 0.148899 + outer loop + vertex -34.0453 7.82857 76.9304 + vertex -34.1535 7.47861 76.7284 + vertex -34.1535 7.07701 77.3019 + endloop + endfacet + facet normal -0.905684 0.347285 0.243165 + outer loop + vertex -33.8685 7.67701 77.8054 + vertex -33.8685 8.15692 77.12 + vertex -34.0453 7.38657 77.5617 + endloop + endfacet + facet normal -0.905695 0.347274 0.243141 + outer loop + vertex -33.8685 8.15692 77.12 + vertex -34.0453 7.82857 76.9304 + vertex -34.0453 7.38657 77.5617 + endloop + endfacet + facet normal -0.818083 0.471114 0.329837 + outer loop + vertex -33.6285 7.93953 78.0257 + vertex -33.6285 8.4537 77.2913 + vertex -33.8685 7.67701 77.8054 + endloop + endfacet + facet normal -0.818052 0.471135 0.329884 + outer loop + vertex -33.6285 8.4537 77.2913 + vertex -33.8685 8.15692 77.12 + vertex -33.8685 7.67701 77.8054 + endloop + endfacet + facet normal -0.705747 0.58033 0.406373 + outer loop + vertex -33.3327 8.16614 78.2158 + vertex -33.3327 8.70988 77.4393 + vertex -33.6285 7.93953 78.0257 + endloop + endfacet + facet normal -0.705838 0.580289 0.406273 + outer loop + vertex -33.3327 8.70988 77.4393 + vertex -33.6285 8.4537 77.2913 + vertex -33.6285 7.93953 78.0257 + endloop + endfacet + facet normal -0.572054 0.671877 0.470463 + outer loop + vertex -32.99 8.34995 78.37 + vertex -32.99 8.91769 77.5592 + vertex -33.3327 8.16614 78.2158 + endloop + endfacet + facet normal -0.572031 0.671883 0.470483 + outer loop + vertex -32.99 8.91769 77.5592 + vertex -33.3327 8.70988 77.4393 + vertex -33.3327 8.16614 78.2158 + endloop + endfacet + facet normal -0.421316 0.74291 0.52017 + outer loop + vertex -32.6108 8.48539 78.4837 + vertex -32.6108 9.07081 77.6476 + vertex -32.99 8.34995 78.37 + endloop + endfacet + facet normal -0.42126 0.742917 0.520207 + outer loop + vertex -32.6108 9.07081 77.6476 + vertex -32.99 8.91769 77.5592 + vertex -32.99 8.34995 78.37 + endloop + endfacet + facet normal -0.257967 0.791421 0.554171 + outer loop + vertex -32.2068 8.56834 78.5533 + vertex -32.2068 9.16458 77.7018 + vertex -32.6108 8.48539 78.4837 + endloop + endfacet + facet normal -0.258035 0.791422 0.554138 + outer loop + vertex -32.2068 9.16458 77.7018 + vertex -32.6108 9.07081 77.6476 + vertex -32.6108 8.48539 78.4837 + endloop + endfacet + facet normal -0.0867652 0.816057 0.571421 + outer loop + vertex -31.79 8.59627 78.5767 + vertex -31.79 9.19615 77.72 + vertex -32.2068 8.56834 78.5533 + endloop + endfacet + facet normal -0.086763 0.816057 0.571422 + outer loop + vertex -31.79 9.19615 77.72 + vertex -32.2068 9.16458 77.7018 + vertex -32.2068 8.56834 78.5533 + endloop + endfacet + facet normal -1 -0 -0 + outer loop + vertex -31.79 9.19616 77.72 + vertex -31.79 9.63815 76.7721 + vertex -31.79 9.19615 77.72 + endloop + endfacet + facet normal -0.996158 0.0793716 0.0370105 + outer loop + vertex -34.1535 7.47861 76.7284 + vertex -34.1535 7.77452 76.0938 + vertex -34.19 7.11769 76.52 + endloop + endfacet + facet normal -0.996157 0.079374 0.0370142 + outer loop + vertex -34.1535 7.77452 76.0938 + vertex -34.19 7.38289 75.9513 + vertex -34.19 7.11769 76.52 + endloop + endfacet + facet normal -0.965722 0.235259 0.109703 + outer loop + vertex -34.0453 7.82857 76.9304 + vertex -34.0453 8.15424 76.232 + vertex -34.1535 7.47861 76.7284 + endloop + endfacet + facet normal -0.965723 0.235255 0.109698 + outer loop + vertex -34.0453 8.15424 76.232 + vertex -34.1535 7.77452 76.0938 + vertex -34.1535 7.47861 76.7284 + endloop + endfacet + facet normal -0.905693 0.384216 0.179162 + outer loop + vertex -33.8685 8.15692 77.12 + vertex -33.8685 8.51052 76.3617 + vertex -34.0453 7.82857 76.9304 + endloop + endfacet + facet normal -0.905692 0.384217 0.179164 + outer loop + vertex -33.8685 8.51052 76.3617 + vertex -34.0453 8.15424 76.232 + vertex -34.0453 7.82857 76.9304 + endloop + endfacet + facet normal -0.818058 0.521245 0.243075 + outer loop + vertex -33.6285 8.4537 77.2913 + vertex -33.6285 8.83255 76.4789 + vertex -33.8685 8.15692 77.12 + endloop + endfacet + facet normal -0.818073 0.52123 0.243053 + outer loop + vertex -33.6285 8.83255 76.4789 + vertex -33.8685 8.51052 76.3617 + vertex -33.8685 8.15692 77.12 + endloop + endfacet + facet normal -0.705819 0.642025 0.299373 + outer loop + vertex -33.3327 8.70988 77.4393 + vertex -33.3327 9.11052 76.5801 + vertex -33.6285 8.4537 77.2913 + endloop + endfacet + facet normal -0.705782 0.642048 0.299409 + outer loop + vertex -33.3327 9.11052 76.5801 + vertex -33.6285 8.83255 76.4789 + vertex -33.6285 8.4537 77.2913 + endloop + endfacet + facet normal -0.572051 0.743372 0.346636 + outer loop + vertex -32.99 8.91769 77.5592 + vertex -32.99 9.33601 76.6621 + vertex -33.3327 8.70988 77.4393 + endloop + endfacet + facet normal -0.572061 0.743367 0.346628 + outer loop + vertex -32.99 9.33601 76.6621 + vertex -33.3327 9.11052 76.5801 + vertex -33.3327 8.70988 77.4393 + endloop + endfacet + facet normal -0.421261 0.821964 0.383293 + outer loop + vertex -32.6108 9.07081 77.6476 + vertex -32.6108 9.50215 76.7226 + vertex -32.99 8.91769 77.5592 + endloop + endfacet + facet normal -0.421279 0.82196 0.383282 + outer loop + vertex -32.6108 9.50215 76.7226 + vertex -32.99 9.33601 76.6621 + vertex -32.99 8.91769 77.5592 + endloop + endfacet + facet normal -0.258013 0.875621 0.40831 + outer loop + vertex -32.2068 9.16458 77.7018 + vertex -32.2068 9.60389 76.7597 + vertex -32.6108 9.07081 77.6476 + endloop + endfacet + facet normal -0.258005 0.875621 0.408314 + outer loop + vertex -32.2068 9.60389 76.7597 + vertex -32.6108 9.50215 76.7226 + vertex -32.6108 9.07081 77.6476 + endloop + endfacet + facet normal -0.0867726 0.902894 0.421014 + outer loop + vertex -31.79 9.19615 77.72 + vertex -31.79 9.63815 76.7721 + vertex -32.2068 9.16458 77.7018 + endloop + endfacet + facet normal -0.0867413 0.902891 0.421027 + outer loop + vertex -31.79 9.63815 76.7721 + vertex -32.2068 9.60389 76.7597 + vertex -32.2068 9.16458 77.7018 + endloop + endfacet + facet normal -0.996158 0.0845946 0.0226665 + outer loop + vertex -34.1535 7.77452 76.0938 + vertex -34.1535 7.95573 75.4175 + vertex -34.19 7.38289 75.9513 + endloop + endfacet + facet normal -0.996158 0.0845933 0.0226652 + outer loop + vertex -34.1535 7.95573 75.4175 + vertex -34.19 7.54531 75.3451 + vertex -34.19 7.38289 75.9513 + endloop + endfacet + facet normal -0.965724 0.250727 0.0671873 + outer loop + vertex -34.0453 8.15424 76.232 + vertex -34.0453 8.35369 75.4877 + vertex -34.1535 7.77452 76.0938 + endloop + endfacet + facet normal -0.965726 0.250718 0.0671782 + outer loop + vertex -34.0453 8.35369 75.4877 + vertex -34.1535 7.95573 75.4175 + vertex -34.1535 7.77452 76.0938 + endloop + endfacet + facet normal -0.90569 0.409494 0.109725 + outer loop + vertex -33.8685 8.51052 76.3617 + vertex -33.8685 8.72708 75.5535 + vertex -34.0453 8.15424 76.232 + endloop + endfacet + facet normal -0.905685 0.409504 0.109735 + outer loop + vertex -33.8685 8.72708 75.5535 + vertex -34.0453 8.35369 75.4877 + vertex -34.0453 8.15424 76.232 + endloop + endfacet + facet normal -0.818074 0.555517 0.148846 + outer loop + vertex -33.6285 8.83255 76.4789 + vertex -33.6285 9.06456 75.613 + vertex -33.8685 8.51052 76.3617 + endloop + endfacet + facet normal -0.818066 0.555526 0.148855 + outer loop + vertex -33.6285 9.06456 75.613 + vertex -33.8685 8.72708 75.5535 + vertex -33.8685 8.51052 76.3617 + endloop + endfacet + facet normal -0.705777 0.684295 0.183355 + outer loop + vertex -33.3327 9.11052 76.5801 + vertex -33.3327 9.35588 75.6644 + vertex -33.6285 8.83255 76.4789 + endloop + endfacet + facet normal -0.705785 0.684289 0.183349 + outer loop + vertex -33.3327 9.35588 75.6644 + vertex -33.6285 9.06456 75.613 + vertex -33.6285 8.83255 76.4789 + endloop + endfacet + facet normal -0.57208 0.792246 0.212299 + outer loop + vertex -32.99 9.33601 76.6621 + vertex -32.99 9.59219 75.7061 + vertex -33.3327 9.11052 76.5801 + endloop + endfacet + facet normal -0.572113 0.792228 0.212276 + outer loop + vertex -32.99 9.59219 75.7061 + vertex -33.3327 9.35588 75.6644 + vertex -33.3327 9.11052 76.5801 + endloop + endfacet + facet normal -0.42127 0.876029 0.234745 + outer loop + vertex -32.6108 9.50215 76.7226 + vertex -32.6108 9.76631 75.7368 + vertex -32.99 9.33601 76.6621 + endloop + endfacet + facet normal -0.421259 0.876032 0.234751 + outer loop + vertex -32.6108 9.76631 75.7368 + vertex -32.99 9.59219 75.7061 + vertex -32.99 9.33601 76.6621 + endloop + endfacet + facet normal -0.25798 0.933229 0.25006 + outer loop + vertex -32.2068 9.60389 76.7597 + vertex -32.2068 9.87294 75.7556 + vertex -32.6108 9.50215 76.7226 + endloop + endfacet + facet normal -0.257951 0.933233 0.250074 + outer loop + vertex -32.2068 9.87294 75.7556 + vertex -32.6108 9.76631 75.7368 + vertex -32.6108 9.50215 76.7226 + endloop + endfacet + facet normal -0.0867685 0.962279 0.257859 + outer loop + vertex -31.79 9.63815 76.7721 + vertex -31.79 9.90885 75.7619 + vertex -32.2068 9.60389 76.7597 + endloop + endfacet + facet normal -0.0868039 0.962279 0.257844 + outer loop + vertex -31.79 9.90885 75.7619 + vertex -32.2068 9.87294 75.7556 + vertex -32.2068 9.60389 76.7597 + endloop + endfacet + facet normal -0.996158 0.0872449 0.00763378 + outer loop + vertex -34.1535 7.95573 75.4175 + vertex -34.1535 8.01676 74.72 + vertex -34.19 7.54531 75.3451 + endloop + endfacet + facet normal -0.996158 0.0872439 0.00763297 + outer loop + vertex -34.1535 8.01676 74.72 + vertex -34.19 7.6 74.72 + vertex -34.19 7.54531 75.3451 + endloop + endfacet + facet normal -0.965726 0.258578 0.0226209 + outer loop + vertex -34.0453 8.35369 75.4877 + vertex -34.0453 8.42085 74.72 + vertex -34.1535 7.95573 75.4175 + endloop + endfacet + facet normal -0.965724 0.258584 0.0226257 + outer loop + vertex -34.0453 8.42085 74.72 + vertex -34.1535 8.01676 74.72 + vertex -34.1535 7.95573 75.4175 + endloop + endfacet + facet normal -0.905688 0.422332 0.0369483 + outer loop + vertex -33.8685 8.72708 75.5535 + vertex -33.8685 8.8 74.72 + vertex -34.0453 8.35369 75.4877 + endloop + endfacet + facet normal -0.90569 0.422329 0.0369462 + outer loop + vertex -33.8685 8.8 74.72 + vertex -34.0453 8.42085 74.72 + vertex -34.0453 8.35369 75.4877 + endloop + endfacet + facet normal -0.818067 0.572934 0.0501269 + outer loop + vertex -33.6285 9.06456 75.613 + vertex -33.6285 9.14269 74.72 + vertex -33.8685 8.72708 75.5535 + endloop + endfacet + facet normal -0.818071 0.572929 0.0501236 + outer loop + vertex -33.6285 9.14269 74.72 + vertex -33.8685 8.8 74.72 + vertex -33.8685 8.72708 75.5535 + endloop + endfacet + facet normal -0.705778 0.705737 0.0617482 + outer loop + vertex -33.3327 9.35588 75.6644 + vertex -33.3327 9.43851 74.72 + vertex -33.6285 9.06456 75.613 + endloop + endfacet + facet normal -0.705781 0.705734 0.0617458 + outer loop + vertex -33.3327 9.43851 74.72 + vertex -33.6285 9.14269 74.72 + vertex -33.6285 9.06456 75.613 + endloop + endfacet + facet normal -0.572104 0.81706 0.0714813 + outer loop + vertex -32.99 9.59219 75.7061 + vertex -32.99 9.67846 74.72 + vertex -33.3327 9.35588 75.6644 + endloop + endfacet + facet normal -0.572091 0.817069 0.0714892 + outer loop + vertex -32.99 9.67846 74.72 + vertex -33.3327 9.43851 74.72 + vertex -33.3327 9.35588 75.6644 + endloop + endfacet + facet normal -0.421261 0.903489 0.0790375 + outer loop + vertex -32.6108 9.76631 75.7368 + vertex -32.6108 9.85526 74.72 + vertex -32.99 9.59219 75.7061 + endloop + endfacet + facet normal -0.421249 0.903494 0.0790431 + outer loop + vertex -32.6108 9.85526 74.72 + vertex -32.99 9.67846 74.72 + vertex -32.99 9.59219 75.7061 + endloop + endfacet + facet normal -0.257952 0.962482 0.0842032 + outer loop + vertex -32.2068 9.87294 75.7556 + vertex -32.2068 9.96354 74.72 + vertex -32.6108 9.76631 75.7368 + endloop + endfacet + facet normal -0.257963 0.962479 0.084198 + outer loop + vertex -32.2068 9.96354 74.72 + vertex -32.6108 9.85526 74.72 + vertex -32.6108 9.76631 75.7368 + endloop + endfacet + facet normal -0.0868169 0.992434 0.0868225 + outer loop + vertex -31.79 9.90885 75.7619 + vertex -31.79 10 74.72 + vertex -32.2068 9.87294 75.7556 + endloop + endfacet + facet normal -0.0868142 0.992434 0.0868236 + outer loop + vertex -31.79 10 74.72 + vertex -32.2068 9.96354 74.72 + vertex -32.2068 9.87294 75.7556 + endloop + endfacet + facet normal -0.996148 0.0872493 -0.00872493 + outer loop + vertex -34.19 7.60002 74.7199 + vertex -34.1535 8.01676 74.72 + vertex -34.1535 8.01674 74.7198 + endloop + endfacet + facet normal -0.996035 0.0872331 0.0174466 + outer loop + vertex -34.1535 8.01676 74.72 + vertex -34.19 7.60002 74.7199 + vertex -34.19 7.6 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 7.60002 74.7199 + vertex -34.19 7.6 74.7199 + vertex -34.19 7.6 74.72 + endloop + endfacet + facet normal -0.965643 0.258582 -0.0258582 + outer loop + vertex -34.1535 8.01678 74.7199 + vertex -34.0453 8.42085 74.72 + vertex -34.0453 8.42084 74.7199 + endloop + endfacet + facet normal -0.964681 0.258305 0.051661 + outer loop + vertex -34.0453 8.42085 74.72 + vertex -34.1535 8.01678 74.7199 + vertex -34.1535 8.01676 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.1535 8.01676 74.7199 + vertex -34.1535 8.01676 74.72 + vertex -34.1535 8.01678 74.7199 + endloop + endfacet + facet normal -1 0 -0 + outer loop + vertex -34.1535 8.01676 74.72 + vertex -34.1535 8.01676 74.7199 + vertex -34.1535 8.01674 74.7198 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -33.8685 8.8 74.72 + vertex -34.0453 8.42086 74.72 + vertex -34.0453 8.42085 74.72 + endloop + endfacet + facet normal 0.258704 0 0.965957 + outer loop + vertex 32.2068 4 80.6835 + vertex 32.6108 3.99999 80.5753 + vertex 32.6108 4 80.5753 + endloop + endfacet + facet normal 0.258704 0 0.965957 + outer loop + vertex 32.6108 3.99999 80.5753 + vertex 32.2068 4 80.6835 + vertex 32.2068 3.99999 80.6835 + endloop + endfacet + facet normal 0.422617 0 0.906308 + outer loop + vertex 32.777 4 80.4978 + vertex 32.6108 4 80.5753 + vertex 32.6108 3.99999 80.5753 + endloop + endfacet + facet normal 0.573462 -0 0.819232 + outer loop + vertex 33.3327 3.99999 80.1585 + vertex 33.3327 4 80.1585 + vertex 33.1787 4 80.2663 + endloop + endfacet + facet normal 0.707107 0 0.707107 + outer loop + vertex 33.6285 3.99999 79.8627 + vertex 33.3327 4 80.1585 + vertex 33.3327 3.99999 80.1585 + endloop + endfacet + facet normal 0.707107 0 0.707107 + outer loop + vertex 33.3327 4 80.1585 + vertex 33.6285 3.99999 79.8627 + vertex 33.6285 4 79.8627 + endloop + endfacet + facet normal 0.819256 0 0.573428 + outer loop + vertex 33.7414 4 79.7014 + vertex 33.6285 4 79.8627 + vertex 33.6285 3.99999 79.8627 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.0453 4 79.1408 + vertex 34.0453 3.99998 79.1409 + vertex 34.0453 3.99998 79.1408 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex 34.0453 4 79.1408 + vertex 34.0453 3.99998 79.1408 + vertex 34.0453 3.99997 79.1408 + endloop + endfacet + facet normal 0.387659 0.903902 0.18078 + outer loop + vertex 34.0453 3.99998 79.1409 + vertex 34.0453 4 79.1408 + vertex 33.9601 4 79.3235 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.1535 4 78.7368 + vertex 34.1535 3.99997 78.7368 + vertex 34.1535 3.99993 78.7367 + endloop + endfacet + facet normal 0.965957 0 0.258704 + outer loop + vertex 34.1535 3.99997 78.7368 + vertex 34.0453 4 79.1408 + vertex 34.0453 3.99997 79.1408 + endloop + endfacet + facet normal 0.965957 0 0.258704 + outer loop + vertex 34.0453 4 79.1408 + vertex 34.1535 3.99997 78.7368 + vertex 34.1535 4 78.7368 + endloop + endfacet + facet normal 0.988572 -0.123497 0.0864478 + outer loop + vertex 34.1721 4 78.5241 + vertex 34.1535 4 78.7368 + vertex 34.1535 3.99993 78.7367 + endloop + endfacet + facet normal 0.0869087 0.0868262 0.992425 + outer loop + vertex 32.2068 4 80.6835 + vertex 32.2068 5.03556 80.5929 + vertex 31.79 4 80.72 + endloop + endfacet + facet normal 0.0867998 0.0868707 0.992431 + outer loop + vertex 32.2068 5.03556 80.5929 + vertex 31.79 5.04189 80.6288 + vertex 31.79 4 80.72 + endloop + endfacet + facet normal 0.257784 0.0842524 0.962522 + outer loop + vertex 32.6108 4 80.5753 + vertex 32.6108 5.01676 80.4863 + vertex 32.2068 4 80.6835 + endloop + endfacet + facet normal 0.257885 0.084208 0.962499 + outer loop + vertex 32.6108 5.01676 80.4863 + vertex 32.2068 5.03556 80.5929 + vertex 32.2068 4 80.6835 + endloop + endfacet + facet normal 0.421213 0.0790758 0.903508 + outer loop + vertex 32.99 4.98605 80.3122 + vertex 32.777 4 80.4978 + vertex 32.99 4 80.3985 + endloop + endfacet + facet normal 0.421294 0.0790515 0.903472 + outer loop + vertex 32.777 4 80.4978 + vertex 32.99 4.98605 80.3122 + vertex 32.6108 4 80.5753 + endloop + endfacet + facet normal 0.421225 0.0790862 0.903502 + outer loop + vertex 32.99 4.98605 80.3122 + vertex 32.6108 5.01676 80.4863 + vertex 32.6108 4 80.5753 + endloop + endfacet + facet normal 0.571996 0.07147 0.817137 + outer loop + vertex 33.3327 4.94439 80.0759 + vertex 33.1787 4 80.2663 + vertex 33.3327 4 80.1585 + endloop + endfacet + facet normal 0.572319 0.0713732 0.816919 + outer loop + vertex 33.1787 4 80.2663 + vertex 33.3327 4.94439 80.0759 + vertex 32.99 4 80.3985 + endloop + endfacet + facet normal 0.572084 0.0715109 0.817072 + outer loop + vertex 32.99 4 80.3985 + vertex 33.3327 4.94439 80.0759 + vertex 32.99 4.98605 80.3122 + endloop + endfacet + facet normal 0.705759 0.0617229 0.705759 + outer loop + vertex 33.6285 4 79.8627 + vertex 33.6285 4.89302 79.7846 + vertex 33.3327 4 80.1585 + endloop + endfacet + facet normal 0.70575 0.0617291 0.705767 + outer loop + vertex 33.6285 4.89302 79.7846 + vertex 33.3327 4.94439 80.0759 + vertex 33.3327 4 80.1585 + endloop + endfacet + facet normal 0.818233 0.0499614 0.572712 + outer loop + vertex 33.7414 4 79.7014 + vertex 33.8685 4.83351 79.4471 + vertex 33.6285 4 79.8627 + endloop + endfacet + facet normal 0.817948 0.0501246 0.573105 + outer loop + vertex 33.8685 4.83351 79.4471 + vertex 33.7414 4 79.7014 + vertex 33.8685 4 79.52 + endloop + endfacet + facet normal 0.818083 0.0501047 0.572913 + outer loop + vertex 33.8685 4.83351 79.4471 + vertex 33.6285 4.89302 79.7846 + vertex 33.6285 4 79.8627 + endloop + endfacet + facet normal 0.905744 0.0368662 0.422219 + outer loop + vertex 33.9601 4 79.3235 + vertex 34.0453 4.76767 79.0737 + vertex 33.8685 4 79.52 + endloop + endfacet + facet normal 0.905679 0.0369168 0.422353 + outer loop + vertex 34.0453 4.76767 79.0737 + vertex 33.9601 4 79.3235 + vertex 34.0453 4 79.1408 + endloop + endfacet + facet normal 0.905693 0.0369368 0.422321 + outer loop + vertex 33.8685 4 79.52 + vertex 34.0453 4.76767 79.0737 + vertex 33.8685 4.83351 79.4471 + endloop + endfacet + facet normal 0.965709 0.0226563 0.258638 + outer loop + vertex 34.1535 4 78.7368 + vertex 34.1535 4.6975 78.6757 + vertex 34.0453 4 79.1408 + endloop + endfacet + facet normal 0.965731 0.0225999 0.258558 + outer loop + vertex 34.1535 4.6975 78.6757 + vertex 34.0453 4.76767 79.0737 + vertex 34.0453 4 79.1408 + endloop + endfacet + facet normal 0.99617 0.00753954 0.0871122 + outer loop + vertex 34.1721 4 78.5241 + vertex 34.19 4.62513 78.2653 + vertex 34.1535 4 78.7368 + endloop + endfacet + facet normal 0.996147 0.00764452 0.0873642 + outer loop + vertex 34.19 4.62513 78.2653 + vertex 34.1721 4 78.5241 + vertex 34.19 4 78.32 + endloop + endfacet + facet normal 0.996157 0.00764281 0.0872481 + outer loop + vertex 34.19 4.62513 78.2653 + vertex 34.1535 4.6975 78.6757 + vertex 34.1535 4 78.7368 + endloop + endfacet + facet normal 0.0867997 0.257802 0.962291 + outer loop + vertex 32.2068 5.03556 80.5929 + vertex 32.2068 6.03965 80.3239 + vertex 31.79 5.04189 80.6288 + endloop + endfacet + facet normal 0.0869024 0.25776 0.962293 + outer loop + vertex 32.2068 6.03965 80.3239 + vertex 31.79 6.05212 80.3582 + vertex 31.79 5.04189 80.6288 + endloop + endfacet + facet normal 0.257885 0.2501 0.933244 + outer loop + vertex 32.6108 5.01676 80.4863 + vertex 32.6108 6.00262 80.2221 + vertex 32.2068 5.03556 80.5929 + endloop + endfacet + facet normal 0.258068 0.250013 0.933217 + outer loop + vertex 32.6108 6.00262 80.2221 + vertex 32.2068 6.03965 80.3239 + vertex 32.2068 5.03556 80.5929 + endloop + endfacet + facet normal 0.421227 0.234749 0.876048 + outer loop + vertex 32.99 4.98605 80.3122 + vertex 32.99 5.94215 80.056 + vertex 32.6108 5.01676 80.4863 + endloop + endfacet + facet normal 0.421179 0.234776 0.876064 + outer loop + vertex 32.99 5.94215 80.056 + vertex 32.6108 6.00262 80.2221 + vertex 32.6108 5.01676 80.4863 + endloop + endfacet + facet normal 0.57208 0.212316 0.792241 + outer loop + vertex 33.3327 4.94439 80.0759 + vertex 33.3327 5.86008 79.8305 + vertex 32.99 4.98605 80.3122 + endloop + endfacet + facet normal 0.572125 0.212285 0.792217 + outer loop + vertex 33.3327 5.86008 79.8305 + vertex 32.99 5.94215 80.056 + vertex 32.99 4.98605 80.3122 + endloop + endfacet + facet normal 0.705749 0.183428 0.684304 + outer loop + vertex 33.6285 4.89302 79.7846 + vertex 33.6285 5.7589 79.5525 + vertex 33.3327 4.94439 80.0759 + endloop + endfacet + facet normal 0.705807 0.183377 0.684258 + outer loop + vertex 33.6285 5.7589 79.5525 + vertex 33.3327 5.86008 79.8305 + vertex 33.3327 4.94439 80.0759 + endloop + endfacet + facet normal 0.818083 0.148877 0.555497 + outer loop + vertex 33.8685 4.83351 79.4471 + vertex 33.8685 5.6417 79.2305 + vertex 33.6285 4.89302 79.7846 + endloop + endfacet + facet normal 0.818054 0.14891 0.55553 + outer loop + vertex 33.8685 5.6417 79.2305 + vertex 33.6285 5.7589 79.5525 + vertex 33.6285 4.89302 79.7846 + endloop + endfacet + facet normal 0.905693 0.109749 0.409482 + outer loop + vertex 34.0453 4.76767 79.0737 + vertex 34.0453 5.51202 78.8742 + vertex 33.8685 4.83351 79.4471 + endloop + endfacet + facet normal 0.905697 0.109742 0.409475 + outer loop + vertex 34.0453 5.51202 78.8742 + vertex 33.8685 5.6417 79.2305 + vertex 33.8685 4.83351 79.4471 + endloop + endfacet + facet normal 0.965732 0.0671689 0.250701 + outer loop + vertex 34.1535 4.6975 78.6757 + vertex 34.1535 5.37381 78.4945 + vertex 34.0453 4.76767 79.0737 + endloop + endfacet + facet normal 0.965721 0.0672011 0.250733 + outer loop + vertex 34.1535 5.37381 78.4945 + vertex 34.0453 5.51202 78.8742 + vertex 34.0453 4.76767 79.0737 + endloop + endfacet + facet normal 0.996157 0.0226662 0.0845989 + outer loop + vertex 34.19 4.62513 78.2653 + vertex 34.19 5.23127 78.1029 + vertex 34.1535 4.6975 78.6757 + endloop + endfacet + facet normal 0.996157 0.0226661 0.0845989 + outer loop + vertex 34.19 5.23127 78.1029 + vertex 34.1535 5.37381 78.4945 + vertex 34.1535 4.6975 78.6757 + endloop + endfacet + facet normal 0.0868976 0.421006 0.902886 + outer loop + vertex 32.2068 6.03965 80.3239 + vertex 32.2068 6.98177 79.8846 + vertex 31.79 6.05212 80.3582 + endloop + endfacet + facet normal 0.0868672 0.421018 0.902883 + outer loop + vertex 32.2068 6.98177 79.8846 + vertex 31.79 7 79.9162 + vertex 31.79 6.05212 80.3582 + endloop + endfacet + facet normal 0.258062 0.408273 0.875624 + outer loop + vertex 32.6108 6.00262 80.2221 + vertex 32.6108 6.92763 79.7908 + vertex 32.2068 6.03965 80.3239 + endloop + endfacet + facet normal 0.258017 0.408295 0.875627 + outer loop + vertex 32.6108 6.92763 79.7908 + vertex 32.2068 6.98177 79.8846 + vertex 32.2068 6.03965 80.3239 + endloop + endfacet + facet normal 0.421183 0.383293 0.822004 + outer loop + vertex 32.99 5.94215 80.056 + vertex 32.99 6.83923 79.6377 + vertex 32.6108 6.00262 80.2221 + endloop + endfacet + facet normal 0.421224 0.383268 0.821995 + outer loop + vertex 32.99 6.83923 79.6377 + vertex 32.6108 6.92763 79.7908 + vertex 32.6108 6.00262 80.2221 + endloop + endfacet + facet normal 0.572124 0.346591 0.743336 + outer loop + vertex 33.3327 5.86008 79.8305 + vertex 33.3327 6.71925 79.4299 + vertex 32.99 5.94215 80.056 + endloop + endfacet + facet normal 0.57209 0.346617 0.74335 + outer loop + vertex 33.3327 6.71925 79.4299 + vertex 32.99 6.83923 79.6377 + vertex 32.99 5.94215 80.056 + endloop + endfacet + facet normal 0.705807 0.299354 0.642047 + outer loop + vertex 33.6285 5.7589 79.5525 + vertex 33.6285 6.57134 79.1737 + vertex 33.3327 5.86008 79.8305 + endloop + endfacet + facet normal 0.705794 0.299367 0.642055 + outer loop + vertex 33.6285 6.57134 79.1737 + vertex 33.3327 6.71925 79.4299 + vertex 33.3327 5.86008 79.8305 + endloop + endfacet + facet normal 0.818052 0.243066 0.521258 + outer loop + vertex 33.8685 5.6417 79.2305 + vertex 33.8685 6.4 78.8769 + vertex 33.6285 5.7589 79.5525 + endloop + endfacet + facet normal 0.818084 0.243023 0.521228 + outer loop + vertex 33.8685 6.4 78.8769 + vertex 33.6285 6.57134 79.1737 + vertex 33.6285 5.7589 79.5525 + endloop + endfacet + facet normal 0.905697 0.179127 0.384221 + outer loop + vertex 34.0453 5.51202 78.8742 + vertex 34.0453 6.21042 78.5486 + vertex 33.8685 5.6417 79.2305 + endloop + endfacet + facet normal 0.905669 0.179183 0.384261 + outer loop + vertex 34.0453 6.21042 78.5486 + vertex 33.8685 6.4 78.8769 + vertex 33.8685 5.6417 79.2305 + endloop + endfacet + facet normal 0.965721 0.109703 0.235262 + outer loop + vertex 34.1535 5.37381 78.4945 + vertex 34.1535 6.00838 78.1986 + vertex 34.0453 5.51202 78.8742 + endloop + endfacet + facet normal 0.96573 0.109671 0.23524 + outer loop + vertex 34.1535 6.00838 78.1986 + vertex 34.0453 6.21042 78.5486 + vertex 34.0453 5.51202 78.8742 + endloop + endfacet + facet normal 0.996157 0.0370134 0.0793765 + outer loop + vertex 34.19 5.23127 78.1029 + vertex 34.19 5.8 77.8377 + vertex 34.1535 5.37381 78.4945 + endloop + endfacet + facet normal 0.996157 0.0370132 0.0793764 + outer loop + vertex 34.19 5.8 77.8377 + vertex 34.1535 6.00838 78.1986 + vertex 34.1535 5.37381 78.4945 + endloop + endfacet + facet normal 0.0868618 0.571447 0.816029 + outer loop + vertex 32.2068 6.98177 79.8846 + vertex 32.2068 7.83329 79.2883 + vertex 31.79 7 79.9162 + endloop + endfacet + facet normal 0.0869421 0.571416 0.816042 + outer loop + vertex 32.2068 7.83329 79.2883 + vertex 31.79 7.85672 79.3163 + vertex 31.79 7 79.9162 + endloop + endfacet + facet normal 0.258012 0.554146 0.791424 + outer loop + vertex 32.6108 6.92763 79.7908 + vertex 32.6108 7.76369 79.2054 + vertex 32.2068 6.98177 79.8846 + endloop + endfacet + facet normal 0.257876 0.554214 0.791421 + outer loop + vertex 32.6108 7.76369 79.2054 + vertex 32.2068 7.83329 79.2883 + vertex 32.2068 6.98177 79.8846 + endloop + endfacet + facet normal 0.421229 0.520186 0.742949 + outer loop + vertex 32.99 6.83923 79.6377 + vertex 32.99 7.65004 79.07 + vertex 32.6108 6.92763 79.7908 + endloop + endfacet + facet normal 0.421196 0.520207 0.742953 + outer loop + vertex 32.99 7.65004 79.07 + vertex 32.6108 7.76369 79.2054 + vertex 32.6108 6.92763 79.7908 + endloop + endfacet + facet normal 0.572091 0.470473 0.671839 + outer loop + vertex 33.3327 6.71925 79.4299 + vertex 33.3327 7.4958 78.8861 + vertex 32.99 6.83923 79.6377 + endloop + endfacet + facet normal 0.572209 0.470375 0.671807 + outer loop + vertex 33.3327 7.4958 78.8861 + vertex 32.99 7.65004 79.07 + vertex 32.99 6.83923 79.6377 + endloop + endfacet + facet normal 0.705793 0.406344 0.580293 + outer loop + vertex 33.6285 6.57134 79.1737 + vertex 33.6285 7.30566 78.6595 + vertex 33.3327 6.71925 79.4299 + endloop + endfacet + facet normal 0.705765 0.406375 0.580306 + outer loop + vertex 33.6285 7.30566 78.6595 + vertex 33.3327 7.4958 78.8861 + vertex 33.3327 6.71925 79.4299 + endloop + endfacet + facet normal 0.818083 0.32986 0.471097 + outer loop + vertex 33.8685 6.4 78.8769 + vertex 33.8685 7.08538 78.397 + vertex 33.6285 6.57134 79.1737 + endloop + endfacet + facet normal 0.818063 0.329891 0.471111 + outer loop + vertex 33.8685 7.08538 78.397 + vertex 33.6285 7.30566 78.6595 + vertex 33.6285 6.57134 79.1737 + endloop + endfacet + facet normal 0.905672 0.243182 0.347305 + outer loop + vertex 34.0453 6.21042 78.5486 + vertex 34.0453 6.84167 78.1066 + vertex 33.8685 6.4 78.8769 + endloop + endfacet + facet normal 0.905672 0.243181 0.347305 + outer loop + vertex 34.0453 6.84167 78.1066 + vertex 33.8685 7.08538 78.397 + vertex 33.8685 6.4 78.8769 + endloop + endfacet + facet normal 0.96573 0.148873 0.212611 + outer loop + vertex 34.1535 6.00838 78.1986 + vertex 34.1535 6.58192 77.797 + vertex 34.0453 6.21042 78.5486 + endloop + endfacet + facet normal 0.965731 0.148868 0.212609 + outer loop + vertex 34.1535 6.58192 77.797 + vertex 34.0453 6.84167 78.1066 + vertex 34.0453 6.21042 78.5486 + endloop + endfacet + facet normal 0.996157 0.0502319 0.0717441 + outer loop + vertex 34.19 5.8 77.8377 + vertex 34.19 6.31403 77.4778 + vertex 34.1535 6.00838 78.1986 + endloop + endfacet + facet normal 0.996157 0.0502379 0.0717466 + outer loop + vertex 34.19 6.31403 77.4778 + vertex 34.1535 6.58192 77.797 + vertex 34.1535 6.00838 78.1986 + endloop + endfacet + facet normal 0.0869212 0.704454 0.704407 + outer loop + vertex 32.2068 7.83329 79.2883 + vertex 31.79 8.59627 78.5767 + vertex 31.79 7.85672 79.3163 + endloop + endfacet + facet normal 0.0867535 0.704417 0.704465 + outer loop + vertex 31.79 8.59627 78.5767 + vertex 32.2068 7.83329 79.2883 + vertex 32.2068 8.56834 78.5533 + endloop + endfacet + facet normal 0.257965 0.683151 0.683197 + outer loop + vertex 32.2068 7.83329 79.2883 + vertex 32.6108 8.48539 78.4837 + vertex 32.2068 8.56834 78.5533 + endloop + endfacet + facet normal 0.257887 0.683189 0.683189 + outer loop + vertex 32.6108 8.48539 78.4837 + vertex 32.2068 7.83329 79.2883 + vertex 32.6108 7.76369 79.2054 + endloop + endfacet + facet normal 0.421205 0.641321 0.641321 + outer loop + vertex 32.99 7.65004 79.07 + vertex 32.6108 8.48539 78.4837 + vertex 32.6108 7.76369 79.2054 + endloop + endfacet + facet normal 0.421332 0.641321 0.641238 + outer loop + vertex 32.6108 8.48539 78.4837 + vertex 32.99 7.65004 79.07 + vertex 32.99 8.34995 78.37 + endloop + endfacet + facet normal 0.572192 0.579949 0.579875 + outer loop + vertex 33.3327 7.4958 78.8861 + vertex 32.99 8.34995 78.37 + vertex 32.99 7.65004 79.07 + endloop + endfacet + facet normal 0.572046 0.579967 0.580002 + outer loop + vertex 32.99 8.34995 78.37 + vertex 33.3327 7.4958 78.8861 + vertex 33.3327 8.16614 78.2158 + endloop + endfacet + facet normal 0.705737 0.500951 0.500981 + outer loop + vertex 33.3327 7.4958 78.8861 + vertex 33.6285 7.93953 78.0257 + vertex 33.3327 8.16614 78.2158 + endloop + endfacet + facet normal 0.705766 0.500919 0.500974 + outer loop + vertex 33.6285 7.93953 78.0257 + vertex 33.3327 7.4958 78.8861 + vertex 33.6285 7.30566 78.6595 + endloop + endfacet + facet normal 0.818085 0.406633 0.406678 + outer loop + vertex 33.6285 7.30566 78.6595 + vertex 33.8685 7.67701 77.8054 + vertex 33.6285 7.93953 78.0257 + endloop + endfacet + facet normal 0.818064 0.406666 0.406686 + outer loop + vertex 33.8685 7.67701 77.8054 + vertex 33.6285 7.30566 78.6595 + vertex 33.8685 7.08538 78.397 + endloop + endfacet + facet normal 0.905683 0.299774 0.299789 + outer loop + vertex 33.8685 7.08538 78.397 + vertex 34.0453 7.38657 77.5617 + vertex 33.8685 7.67701 77.8054 + endloop + endfacet + facet normal 0.905675 0.299795 0.299795 + outer loop + vertex 34.0453 7.38657 77.5617 + vertex 33.8685 7.08538 78.397 + vertex 34.0453 6.84167 78.1066 + endloop + endfacet + facet normal 0.965731 0.183526 0.183526 + outer loop + vertex 34.0453 6.84167 78.1066 + vertex 34.1535 7.07701 77.3019 + vertex 34.0453 7.38657 77.5617 + endloop + endfacet + facet normal 0.96573 0.183531 0.183527 + outer loop + vertex 34.1535 7.07701 77.3019 + vertex 34.0453 6.84167 78.1066 + vertex 34.1535 6.58192 77.797 + endloop + endfacet + facet normal 0.996158 0.0619264 0.0619252 + outer loop + vertex 34.1535 6.58192 77.797 + vertex 34.19 6.75776 77.034 + vertex 34.1535 7.07701 77.3019 + endloop + endfacet + facet normal 0.996157 0.0619374 0.0619277 + outer loop + vertex 34.19 6.75776 77.034 + vertex 34.1535 6.58192 77.797 + vertex 34.19 6.31403 77.4778 + endloop + endfacet + facet normal 0.0867652 0.816057 0.571422 + outer loop + vertex 32.2068 8.56834 78.5533 + vertex 32.2068 9.16458 77.7018 + vertex 31.79 8.59627 78.5767 + endloop + endfacet + facet normal 0.086763 0.816058 0.571421 + outer loop + vertex 32.2068 9.16458 77.7018 + vertex 31.79 9.19615 77.72 + vertex 31.79 8.59627 78.5767 + endloop + endfacet + facet normal 0.257966 0.791437 0.554148 + outer loop + vertex 32.6108 8.48539 78.4837 + vertex 32.6108 9.07081 77.6476 + vertex 32.2068 8.56834 78.5533 + endloop + endfacet + facet normal 0.258034 0.791406 0.554161 + outer loop + vertex 32.6108 9.07081 77.6476 + vertex 32.2068 9.16458 77.7018 + vertex 32.2068 8.56834 78.5533 + endloop + endfacet + facet normal 0.421317 0.742895 0.520191 + outer loop + vertex 32.99 8.34995 78.37 + vertex 32.99 8.91769 77.5592 + vertex 32.6108 8.48539 78.4837 + endloop + endfacet + facet normal 0.421261 0.742931 0.520185 + outer loop + vertex 32.99 8.91769 77.5592 + vertex 32.6108 9.07081 77.6476 + vertex 32.6108 8.48539 78.4837 + endloop + endfacet + facet normal 0.572055 0.671869 0.470473 + outer loop + vertex 33.3327 8.16614 78.2158 + vertex 33.3327 8.70988 77.4393 + vertex 32.99 8.34995 78.37 + endloop + endfacet + facet normal 0.572031 0.67189 0.470472 + outer loop + vertex 33.3327 8.70988 77.4393 + vertex 32.99 8.91769 77.5592 + vertex 32.99 8.34995 78.37 + endloop + endfacet + facet normal 0.705744 0.580365 0.406327 + outer loop + vertex 33.6285 7.93953 78.0257 + vertex 33.6285 8.4537 77.2913 + vertex 33.3327 8.16614 78.2158 + endloop + endfacet + facet normal 0.705835 0.580258 0.406322 + outer loop + vertex 33.6285 8.4537 77.2913 + vertex 33.3327 8.70988 77.4393 + vertex 33.3327 8.16614 78.2158 + endloop + endfacet + facet normal 0.818084 0.471097 0.329858 + outer loop + vertex 33.8685 7.67701 77.8054 + vertex 33.8685 8.15692 77.12 + vertex 33.6285 7.93953 78.0257 + endloop + endfacet + facet normal 0.818053 0.471148 0.329862 + outer loop + vertex 33.8685 8.15692 77.12 + vertex 33.6285 8.4537 77.2913 + vertex 33.6285 7.93953 78.0257 + endloop + endfacet + facet normal 0.905684 0.347294 0.243155 + outer loop + vertex 34.0453 7.38657 77.5617 + vertex 34.0453 7.82857 76.9304 + vertex 33.8685 7.67701 77.8054 + endloop + endfacet + facet normal 0.905695 0.347267 0.243153 + outer loop + vertex 34.0453 7.82857 76.9304 + vertex 33.8685 8.15692 77.12 + vertex 33.8685 7.67701 77.8054 + endloop + endfacet + facet normal 0.96573 0.212603 0.148878 + outer loop + vertex 34.1535 7.07701 77.3019 + vertex 34.1535 7.47861 76.7284 + vertex 34.0453 7.38657 77.5617 + endloop + endfacet + facet normal 0.965721 0.212644 0.148881 + outer loop + vertex 34.1535 7.47861 76.7284 + vertex 34.0453 7.82857 76.9304 + vertex 34.0453 7.38657 77.5617 + endloop + endfacet + facet normal 0.996158 0.071737 0.0502341 + outer loop + vertex 34.19 6.75776 77.034 + vertex 34.19 7.11769 76.52 + vertex 34.1535 7.07701 77.3019 + endloop + endfacet + facet normal 0.996158 0.0717361 0.050234 + outer loop + vertex 34.19 7.11769 76.52 + vertex 34.1535 7.47861 76.7284 + vertex 34.1535 7.07701 77.3019 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 31.79 8.59627 78.5767 + vertex 31.79 9.19615 77.72 + vertex 31.79 9.19614 77.72 + endloop + endfacet + facet normal 0.0867727 0.902889 0.421025 + outer loop + vertex 32.2068 9.16458 77.7018 + vertex 32.2068 9.60389 76.7597 + vertex 31.79 9.19615 77.72 + endloop + endfacet + facet normal 0.0867414 0.902897 0.421015 + outer loop + vertex 32.2068 9.60389 76.7597 + vertex 31.79 9.63815 76.7721 + vertex 31.79 9.19615 77.72 + endloop + endfacet + facet normal 0.258013 0.875619 0.408313 + outer loop + vertex 32.6108 9.07081 77.6476 + vertex 32.6108 9.50215 76.7226 + vertex 32.2068 9.16458 77.7018 + endloop + endfacet + facet normal 0.258005 0.875623 0.408311 + outer loop + vertex 32.6108 9.50215 76.7226 + vertex 32.2068 9.60389 76.7597 + vertex 32.2068 9.16458 77.7018 + endloop + endfacet + facet normal 0.421261 0.821968 0.383286 + outer loop + vertex 32.99 8.91769 77.5592 + vertex 32.99 9.33601 76.6621 + vertex 32.6108 9.07081 77.6476 + endloop + endfacet + facet normal 0.421279 0.821957 0.38329 + outer loop + vertex 32.99 9.33601 76.6621 + vertex 32.6108 9.50215 76.7226 + vertex 32.6108 9.07081 77.6476 + endloop + endfacet + facet normal 0.57205 0.743374 0.346631 + outer loop + vertex 33.3327 8.70988 77.4393 + vertex 33.3327 9.11052 76.5801 + vertex 32.99 8.91769 77.5592 + endloop + endfacet + facet normal 0.572061 0.743365 0.346633 + outer loop + vertex 33.3327 9.11052 76.5801 + vertex 32.99 9.33601 76.6621 + vertex 32.99 8.91769 77.5592 + endloop + endfacet + facet normal 0.70582 0.642014 0.299393 + outer loop + vertex 33.6285 8.4537 77.2913 + vertex 33.6285 8.83255 76.4789 + vertex 33.3327 8.70988 77.4393 + endloop + endfacet + facet normal 0.705783 0.642057 0.299388 + outer loop + vertex 33.6285 8.83255 76.4789 + vertex 33.3327 9.11052 76.5801 + vertex 33.3327 8.70988 77.4393 + endloop + endfacet + facet normal 0.818057 0.521252 0.243063 + outer loop + vertex 33.8685 8.15692 77.12 + vertex 33.8685 8.51052 76.3617 + vertex 33.6285 8.4537 77.2913 + endloop + endfacet + facet normal 0.818073 0.521225 0.243065 + outer loop + vertex 33.8685 8.51052 76.3617 + vertex 33.6285 8.83255 76.4789 + vertex 33.6285 8.4537 77.2913 + endloop + endfacet + facet normal 0.905693 0.384215 0.179163 + outer loop + vertex 34.0453 7.82857 76.9304 + vertex 34.0453 8.15424 76.232 + vertex 33.8685 8.15692 77.12 + endloop + endfacet + facet normal 0.905692 0.384217 0.179163 + outer loop + vertex 34.0453 8.15424 76.232 + vertex 33.8685 8.51052 76.3617 + vertex 33.8685 8.15692 77.12 + endloop + endfacet + facet normal 0.965722 0.23526 0.1097 + outer loop + vertex 34.1535 7.47861 76.7284 + vertex 34.1535 7.77452 76.0938 + vertex 34.0453 7.82857 76.9304 + endloop + endfacet + facet normal 0.965723 0.235254 0.109701 + outer loop + vertex 34.1535 7.77452 76.0938 + vertex 34.0453 8.15424 76.232 + vertex 34.0453 7.82857 76.9304 + endloop + endfacet + facet normal 0.996158 0.0793704 0.0370125 + outer loop + vertex 34.19 7.11769 76.52 + vertex 34.19 7.38289 75.9513 + vertex 34.1535 7.47861 76.7284 + endloop + endfacet + facet normal 0.996157 0.0793748 0.037012 + outer loop + vertex 34.19 7.38289 75.9513 + vertex 34.1535 7.77452 76.0938 + vertex 34.1535 7.47861 76.7284 + endloop + endfacet + facet normal 0.0867684 0.962282 0.257845 + outer loop + vertex 32.2068 9.60389 76.7597 + vertex 32.2068 9.87294 75.7556 + vertex 31.79 9.63815 76.7721 + endloop + endfacet + facet normal 0.0868038 0.962276 0.257858 + outer loop + vertex 32.2068 9.87294 75.7556 + vertex 31.79 9.90885 75.7619 + vertex 31.79 9.63815 76.7721 + endloop + endfacet + facet normal 0.25798 0.933226 0.250072 + outer loop + vertex 32.6108 9.50215 76.7226 + vertex 32.6108 9.76631 75.7368 + vertex 32.2068 9.60389 76.7597 + endloop + endfacet + facet normal 0.257951 0.933236 0.250062 + outer loop + vertex 32.6108 9.76631 75.7368 + vertex 32.2068 9.87294 75.7556 + vertex 32.2068 9.60389 76.7597 + endloop + endfacet + facet normal 0.42127 0.876028 0.23475 + outer loop + vertex 32.99 9.33601 76.6621 + vertex 32.99 9.59219 75.7061 + vertex 32.6108 9.50215 76.7226 + endloop + endfacet + facet normal 0.42126 0.876033 0.234746 + outer loop + vertex 32.99 9.59219 75.7061 + vertex 32.6108 9.76631 75.7368 + vertex 32.6108 9.50215 76.7226 + endloop + endfacet + facet normal 0.57208 0.792251 0.212282 + outer loop + vertex 33.3327 9.11052 76.5801 + vertex 33.3327 9.35588 75.6644 + vertex 32.99 9.33601 76.6621 + endloop + endfacet + facet normal 0.572113 0.792224 0.212293 + outer loop + vertex 33.3327 9.35588 75.6644 + vertex 32.99 9.59219 75.7061 + vertex 32.99 9.33601 76.6621 + endloop + endfacet + facet normal 0.705777 0.684296 0.183351 + outer loop + vertex 33.6285 8.83255 76.4789 + vertex 33.6285 9.06456 75.613 + vertex 33.3327 9.11052 76.5801 + endloop + endfacet + facet normal 0.705785 0.684288 0.183354 + outer loop + vertex 33.6285 9.06456 75.613 + vertex 33.3327 9.35588 75.6644 + vertex 33.3327 9.11052 76.5801 + endloop + endfacet + facet normal 0.818075 0.555515 0.148852 + outer loop + vertex 33.8685 8.51052 76.3617 + vertex 33.8685 8.72708 75.5535 + vertex 33.6285 8.83255 76.4789 + endloop + endfacet + facet normal 0.818067 0.555528 0.148849 + outer loop + vertex 33.8685 8.72708 75.5535 + vertex 33.6285 9.06456 75.613 + vertex 33.6285 8.83255 76.4789 + endloop + endfacet + facet normal 0.90569 0.409492 0.109732 + outer loop + vertex 34.0453 8.15424 76.232 + vertex 34.0453 8.35369 75.4877 + vertex 33.8685 8.51052 76.3617 + endloop + endfacet + facet normal 0.905685 0.409505 0.109728 + outer loop + vertex 34.0453 8.35369 75.4877 + vertex 33.8685 8.72708 75.5535 + vertex 33.8685 8.51052 76.3617 + endloop + endfacet + facet normal 0.965723 0.250729 0.0671811 + outer loop + vertex 34.1535 7.77452 76.0938 + vertex 34.1535 7.95573 75.4175 + vertex 34.0453 8.15424 76.232 + endloop + endfacet + facet normal 0.965726 0.250717 0.0671845 + outer loop + vertex 34.1535 7.95573 75.4175 + vertex 34.0453 8.35369 75.4877 + vertex 34.0453 8.15424 76.232 + endloop + endfacet + facet normal 0.996158 0.0845949 0.0226656 + outer loop + vertex 34.19 7.38289 75.9513 + vertex 34.19 7.54531 75.3451 + vertex 34.1535 7.77452 76.0938 + endloop + endfacet + facet normal 0.996158 0.0845932 0.0226662 + outer loop + vertex 34.19 7.54531 75.3451 + vertex 34.1535 7.95573 75.4175 + vertex 34.1535 7.77452 76.0938 + endloop + endfacet + facet normal 0.0868169 0.992434 0.0868236 + outer loop + vertex 32.2068 9.87294 75.7556 + vertex 32.2068 9.96354 74.72 + vertex 31.79 9.90885 75.7619 + endloop + endfacet + facet normal 0.0868142 0.992434 0.0868225 + outer loop + vertex 32.2068 9.96354 74.72 + vertex 31.79 10 74.72 + vertex 31.79 9.90885 75.7619 + endloop + endfacet + facet normal 0.257951 0.962482 0.0841982 + outer loop + vertex 32.6108 9.76631 75.7368 + vertex 32.6108 9.85526 74.72 + vertex 32.2068 9.87294 75.7556 + endloop + endfacet + facet normal 0.257963 0.962478 0.0842029 + outer loop + vertex 32.6108 9.85526 74.72 + vertex 32.2068 9.96354 74.72 + vertex 32.2068 9.87294 75.7556 + endloop + endfacet + facet normal 0.421261 0.903489 0.0790427 + outer loop + vertex 32.99 9.59219 75.7061 + vertex 32.99 9.67846 74.72 + vertex 32.6108 9.76631 75.7368 + endloop + endfacet + facet normal 0.421263 0.903487 0.0790438 + outer loop + vertex 32.808 9.76332 74.72 + vertex 32.6108 9.76631 75.7368 + vertex 32.99 9.67846 74.72 + endloop + endfacet + facet normal 0.421273 0.903483 0.0790456 + outer loop + vertex 32.7653 9.78323 74.72 + vertex 32.6108 9.76631 75.7368 + vertex 32.808 9.76332 74.72 + endloop + endfacet + facet normal 0.421226 0.903505 0.0790389 + outer loop + vertex 32.6108 9.76631 75.7368 + vertex 32.7653 9.78323 74.72 + vertex 32.6108 9.85526 74.72 + endloop + endfacet + facet normal -0 -0 1 + outer loop + vertex 32.808 9.76332 74.72 + vertex 32.99 9.67846 74.72 + vertex 32.7653 9.78323 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.6108 9.85526 74.72 + vertex 32.7653 9.78323 74.72 + vertex 32.808 9.76332 74.72 + endloop + endfacet + facet normal 0.572105 0.817059 0.0714883 + outer loop + vertex 33.3327 9.35588 75.6644 + vertex 33.3327 9.43851 74.72 + vertex 32.99 9.59219 75.7061 + endloop + endfacet + facet normal 0.572103 0.81706 0.0714875 + outer loop + vertex 33.1802 9.54529 74.72 + vertex 32.99 9.59219 75.7061 + vertex 33.3327 9.43851 74.72 + endloop + endfacet + facet normal 0.571996 0.817137 0.0714633 + outer loop + vertex 33.1545 9.56328 74.72 + vertex 32.99 9.59219 75.7061 + vertex 33.1802 9.54529 74.72 + endloop + endfacet + facet normal 0.572095 0.817066 0.0714819 + outer loop + vertex 32.99 9.59219 75.7061 + vertex 33.1545 9.56328 74.72 + vertex 32.99 9.67846 74.72 + endloop + endfacet + facet normal -0 -0 -1 + outer loop + vertex 33.1802 9.54529 74.72 + vertex 33.3327 9.43851 74.72 + vertex 33.1545 9.56328 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.99 9.67846 74.72 + vertex 33.1545 9.56328 74.72 + vertex 33.1802 9.54529 74.72 + endloop + endfacet + facet normal 0.705778 0.705737 0.0617461 + outer loop + vertex 33.6285 9.06456 75.613 + vertex 33.6285 9.14269 74.72 + vertex 33.3327 9.35588 75.6644 + endloop + endfacet + facet normal 0.705781 0.705734 0.061748 + outer loop + vertex 33.6285 9.14269 74.72 + vertex 33.3327 9.43851 74.72 + vertex 33.3327 9.35588 75.6644 + endloop + endfacet + facet normal 0.818067 0.572934 0.050124 + outer loop + vertex 33.8685 8.72708 75.5535 + vertex 33.8685 8.8 74.72 + vertex 33.6285 9.06456 75.613 + endloop + endfacet + facet normal 0.818016 0.573006 0.0501332 + outer loop + vertex 33.6285 9.06456 75.613 + vertex 33.7445 8.97709 74.72 + vertex 33.6285 9.14269 74.72 + endloop + endfacet + facet normal 0.818803 0.571862 0.0503475 + outer loop + vertex 33.6285 9.06456 75.613 + vertex 33.7533 8.96449 74.72 + vertex 33.7445 8.97709 74.72 + endloop + endfacet + facet normal 0.818069 0.572932 0.0501251 + outer loop + vertex 33.7533 8.96449 74.72 + vertex 33.6285 9.06456 75.613 + vertex 33.8685 8.8 74.72 + endloop + endfacet + facet normal -0 -0 1 + outer loop + vertex 33.7533 8.96449 74.72 + vertex 33.8685 8.8 74.72 + vertex 33.7445 8.97709 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.7533 8.96449 74.72 + vertex 33.6285 9.14269 74.72 + vertex 33.7445 8.97709 74.72 + endloop + endfacet + facet normal 0.905688 0.422332 0.0369465 + outer loop + vertex 34.0453 8.35369 75.4877 + vertex 34.0453 8.42085 74.72 + vertex 33.8685 8.72708 75.5535 + endloop + endfacet + facet normal 0.90581 0.422071 0.0369255 + outer loop + vertex 33.8685 8.72708 75.5535 + vertex 33.9533 8.61801 74.72 + vertex 33.8685 8.8 74.72 + endloop + endfacet + facet normal 0.902999 0.428146 0.0358445 + outer loop + vertex 33.8685 8.72708 75.5535 + vertex 33.9577 8.60873 74.72 + vertex 33.9533 8.61801 74.72 + endloop + endfacet + facet normal 0.905707 0.42229 0.0369658 + outer loop + vertex 33.9577 8.60873 74.72 + vertex 33.8685 8.72708 75.5535 + vertex 34.0453 8.42085 74.72 + endloop + endfacet + facet normal -0 -0 -1 + outer loop + vertex 33.9577 8.60873 74.72 + vertex 34.0453 8.42085 74.72 + vertex 33.9533 8.61801 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 33.8685 8.8 74.72 + vertex 33.9533 8.61801 74.72 + vertex 33.9577 8.60873 74.72 + endloop + endfacet + facet normal 0.965726 0.258577 0.022625 + outer loop + vertex 34.1535 7.95573 75.4175 + vertex 34.1535 8.01676 74.72 + vertex 34.0453 8.35369 75.4877 + endloop + endfacet + facet normal 0.965724 0.258584 0.0226215 + outer loop + vertex 34.1535 8.01676 74.72 + vertex 34.0453 8.42085 74.72 + vertex 34.0453 8.35369 75.4877 + endloop + endfacet + facet normal 0.996158 0.0872451 0.00763307 + outer loop + vertex 34.19 7.54531 75.3451 + vertex 34.19 7.6 74.72 + vertex 34.1535 7.95573 75.4175 + endloop + endfacet + facet normal 0.996163 0.0871874 0.00762874 + outer loop + vertex 34.1535 7.95573 75.4175 + vertex 34.1723 7.80196 74.72 + vertex 34.1535 8.01676 74.72 + endloop + endfacet + facet normal 0.994611 0.103605 0.00396745 + outer loop + vertex 34.1535 7.95573 75.4175 + vertex 34.1725 7.80004 74.72 + vertex 34.1723 7.80196 74.72 + endloop + endfacet + facet normal 0.996166 0.0871471 0.00768347 + outer loop + vertex 34.1725 7.80004 74.72 + vertex 34.1535 7.95573 75.4175 + vertex 34.19 7.6 74.72 + endloop + endfacet + facet normal -0 -0 -1 + outer loop + vertex 34.1725 7.80004 74.72 + vertex 34.19 7.6 74.72 + vertex 34.1723 7.80196 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 34.1535 8.01676 74.72 + vertex 34.1723 7.80196 74.72 + vertex 34.1725 7.80004 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.6285 9.14269 74.72 + vertex 33.3327 9.4385 74.72 + vertex 33.3327 9.43851 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.1535 8.01676 74.72 + vertex 34.0453 8.42083 74.72 + vertex 34.0453 8.42085 74.72 + endloop + endfacet + facet normal 0.99523 0.0872526 -0.0436263 + outer loop + vertex 34.1725 7.80004 74.72 + vertex 34.1535 8.01671 74.7199 + vertex 34.1535 8.01676 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.1535 8.01676 74.72 + vertex 34.1535 8.01671 74.7199 + vertex 34.1535 8.01675 74.7199 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 3.37487 71.1747 + vertex -34.19 4 74.72 + vertex -34.19 4 71.12 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 2.76873 71.3371 + vertex -34.19 4 74.72 + vertex -34.19 3.37487 71.1747 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 3.37487 78.2653 + vertex -34.19 4 78.32 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 2.2 71.6023 + vertex -34.19 4 74.72 + vertex -34.19 2.76873 71.3371 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 2.76873 78.1029 + vertex -34.19 3.37487 78.2653 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 1.68597 71.9622 + vertex -34.19 4 74.72 + vertex -34.19 2.2 71.6023 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 2.2 77.8377 + vertex -34.19 2.76873 78.1029 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 1.24224 72.406 + vertex -34.19 4 74.72 + vertex -34.19 1.68597 71.9622 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 1.68597 77.4778 + vertex -34.19 2.2 77.8377 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.882309 72.92 + vertex -34.19 4 74.72 + vertex -34.19 1.24224 72.406 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 1.24224 77.034 + vertex -34.19 1.68597 77.4778 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.617107 73.4887 + vertex -34.19 4 74.72 + vertex -34.19 0.882309 72.92 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 0.882309 76.52 + vertex -34.19 1.24224 77.034 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.454693 74.0949 + vertex -34.19 4 74.72 + vertex -34.19 0.617107 73.4887 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 0.617107 75.9513 + vertex -34.19 0.882309 76.52 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.400001 74.72 + vertex -34.19 4 74.72 + vertex -34.19 0.454693 74.0949 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 0.454693 75.3451 + vertex -34.19 0.617107 75.9513 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 0.400001 74.72 + vertex -34.19 0.454693 75.3451 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 4 74.72 + vertex -34.19 7.54531 75.3451 + vertex -34.19 7.6 74.72 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 7.54531 75.3451 + vertex -34.19 4 74.72 + vertex -34.19 7.38289 75.9513 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 7.38289 75.9513 + vertex -34.19 4 74.72 + vertex -34.19 7.11769 76.52 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 7.11769 76.52 + vertex -34.19 4 74.72 + vertex -34.19 6.75776 77.034 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 6.75776 77.034 + vertex -34.19 4 74.72 + vertex -34.19 6.31403 77.4778 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 6.31403 77.4778 + vertex -34.19 4 74.72 + vertex -34.19 5.8 77.8377 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 5.8 77.8377 + vertex -34.19 4 74.72 + vertex -34.19 5.23127 78.1029 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 5.23127 78.1029 + vertex -34.19 4 74.72 + vertex -34.19 4.62513 78.2653 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 4.62513 78.2653 + vertex -34.19 4 74.72 + vertex -34.19 4 78.32 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.54531 75.3451 + vertex 34.19 7.59999 74.72 + vertex 34.19 7.6 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.54531 75.3451 + vertex 34.19 7.59999 74.7199 + vertex 34.19 7.59999 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.54531 75.3451 + vertex 34.19 7.54531 74.0949 + vertex 34.19 7.59999 74.7199 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.38289 75.9513 + vertex 34.19 7.54531 74.0949 + vertex 34.19 7.54531 75.3451 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.38289 75.9513 + vertex 34.19 7.38289 73.4887 + vertex 34.19 7.54531 74.0949 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.11769 76.52 + vertex 34.19 7.38289 73.4887 + vertex 34.19 7.38289 75.9513 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.11769 76.52 + vertex 34.19 7.11769 72.92 + vertex 34.19 7.38289 73.4887 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 6.75776 77.034 + vertex 34.19 7.11769 72.92 + vertex 34.19 7.11769 76.52 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 6.75776 77.034 + vertex 34.19 6.75776 72.406 + vertex 34.19 7.11769 72.92 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 6.31403 77.4778 + vertex 34.19 6.75776 72.406 + vertex 34.19 6.75776 77.034 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 6.31403 77.4778 + vertex 34.19 6.31403 71.9622 + vertex 34.19 6.75776 72.406 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 5.8 77.8377 + vertex 34.19 6.31403 71.9622 + vertex 34.19 6.31403 77.4778 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 5.8 77.8377 + vertex 34.19 5.8 71.6023 + vertex 34.19 6.31403 71.9622 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 5.23127 78.1029 + vertex 34.19 5.8 71.6023 + vertex 34.19 5.8 77.8377 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 5.23127 78.1029 + vertex 34.19 5.23127 71.3371 + vertex 34.19 5.8 71.6023 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 4.62513 78.2653 + vertex 34.19 5.23127 71.3371 + vertex 34.19 5.23127 78.1029 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 4.62513 78.2653 + vertex 34.19 4.62513 71.1747 + vertex 34.19 5.23127 71.3371 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 4.62513 78.2653 + vertex 34.19 4 78.32 + vertex 34.19 4.62513 71.1747 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 4.62513 71.1747 + vertex 34.19 4 78.32 + vertex 34.19 4 71.12 + endloop + endfacet + facet normal 0 0.996195 0.0871515 + outer loop + vertex 31.79 10 74.72 + vertex -31.79 9.90885 75.7619 + vertex 31.79 9.90885 75.7619 + endloop + endfacet + facet normal 0 0.996195 0.0871515 + outer loop + vertex -31.79 9.90885 75.7619 + vertex 31.79 10 74.72 + vertex -31.79 10 74.72 + endloop + endfacet + facet normal -0 0.0871998 0.996191 + outer loop + vertex -31.79 5.04189 80.6288 + vertex 31.79 4 80.72 + vertex 31.79 5.04189 80.6288 + endloop + endfacet + facet normal 0 0.0871998 0.996191 + outer loop + vertex 31.79 4 80.72 + vertex -31.79 5.04189 80.6288 + vertex -31.79 4 80.72 + endloop + endfacet + facet normal 0 0.707131 0.707083 + outer loop + vertex 31.79 8.59627 78.5767 + vertex -31.79 7.85672 79.3163 + vertex 31.79 7.85672 79.3163 + endloop + endfacet + facet normal 0 0.707131 0.707083 + outer loop + vertex -31.79 7.85672 79.3163 + vertex 31.79 8.59627 78.5767 + vertex -31.79 8.59627 78.5767 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 9.19615 77.72 + vertex -31.79 9.19616 77.72 + vertex -31.79 9.19615 77.72 + endloop + endfacet + facet normal 1.42547e-07 0.906313 0.422608 + outer loop + vertex 31.79 9.63815 76.7721 + vertex -31.79 9.19616 77.72 + vertex 31.79 9.19615 77.72 + endloop + endfacet + facet normal 0 0.906316 0.4226 + outer loop + vertex -31.79 9.19616 77.72 + vertex 31.79 9.63815 76.7721 + vertex -31.79 9.63815 76.7721 + endloop + endfacet + facet normal 0 0.965922 0.258835 + outer loop + vertex 31.79 9.90885 75.7619 + vertex -31.79 9.63815 76.7721 + vertex 31.79 9.63815 76.7721 + endloop + endfacet + facet normal 0 0.965922 0.258835 + outer loop + vertex -31.79 9.63815 76.7721 + vertex 31.79 9.90885 75.7619 + vertex -31.79 9.90885 75.7619 + endloop + endfacet + facet normal 0 0.819151 0.573578 + outer loop + vertex 31.79 9.19614 77.72 + vertex -31.79 8.59627 78.5767 + vertex 31.79 8.59627 78.5767 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.79 9.19615 77.72 + vertex 31.79 9.19614 77.72 + vertex 31.79 9.19615 77.72 + endloop + endfacet + facet normal 1.28837e-07 0.819147 0.573584 + outer loop + vertex 31.79 9.19614 77.72 + vertex -31.79 9.19615 77.72 + vertex -31.79 8.59627 78.5767 + endloop + endfacet + facet normal -0 0.422615 0.906309 + outer loop + vertex -31.79 7 79.9162 + vertex 31.79 6.05212 80.3582 + vertex 31.79 7 79.9162 + endloop + endfacet + facet normal 0 0.422615 0.906309 + outer loop + vertex 31.79 6.05212 80.3582 + vertex -31.79 7 79.9162 + vertex -31.79 6.05212 80.3582 + endloop + endfacet + facet normal -0 0.258738 0.965947 + outer loop + vertex -31.79 6.05212 80.3582 + vertex 31.79 5.04189 80.6288 + vertex 31.79 6.05212 80.3582 + endloop + endfacet + facet normal 0 0.258738 0.965947 + outer loop + vertex 31.79 5.04189 80.6288 + vertex -31.79 6.05212 80.3582 + vertex -31.79 5.04189 80.6288 + endloop + endfacet + facet normal -0 0.573588 0.819144 + outer loop + vertex -31.79 7.85672 79.3163 + vertex 31.79 7 79.9162 + vertex 31.79 7.85672 79.3163 + endloop + endfacet + facet normal 0 0.573588 0.819144 + outer loop + vertex 31.79 7 79.9162 + vertex -31.79 7.85672 79.3163 + vertex -31.79 7 79.9162 + endloop + endfacet + facet normal 0.0871432 0.996196 -0 + outer loop + vertex -31.3732 9.96354 3.05176e-07 + vertex -31.79 10 6.86645e-07 + vertex -31.3732 9.96354 6.86645e-07 + endloop + endfacet + facet normal 0.0871432 0.996196 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex -31.3732 9.96354 3.05176e-07 + vertex -31.79 10 3.05176e-07 + endloop + endfacet + facet normal 0.258883 0.965909 -0 + outer loop + vertex -30.9692 9.85526 3.05176e-07 + vertex -31.3732 9.96354 6.86645e-07 + vertex -30.9692 9.85526 6.86645e-07 + endloop + endfacet + facet normal 0.258883 0.965909 0 + outer loop + vertex -31.3732 9.96354 6.86645e-07 + vertex -30.9692 9.85526 3.05176e-07 + vertex -31.3732 9.96354 3.05176e-07 + endloop + endfacet + facet normal 0.906308 0.422617 0 + outer loop + vertex -29.5347 8.42085 6.86645e-07 + vertex -29.7115 8.8 3.05176e-07 + vertex -29.7115 8.8 6.86645e-07 + endloop + endfacet + facet normal 0.906308 0.422617 0 + outer loop + vertex -29.7115 8.8 3.05176e-07 + vertex -29.5347 8.42085 6.86645e-07 + vertex -29.5347 8.42085 3.05176e-07 + endloop + endfacet + facet normal -0.0871432 0.996196 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex -32.2068 9.96354 74.72 + vertex -31.79 10 74.72 + endloop + endfacet + facet normal -0.0871432 0.996196 0 + outer loop + vertex -32.2068 9.96354 3.05176e-07 + vertex -31.79 10 6.86645e-07 + vertex -31.79 10 3.05176e-07 + endloop + endfacet + facet normal -0.0871432 0.996196 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex -32.2068 9.96354 3.05176e-07 + vertex -32.2068 9.96354 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.1535 8.01676 3.05176e-07 + vertex -34.1535 8.01674 74.7198 + vertex -34.1535 8.01676 74.7199 + endloop + endfacet + facet normal -0.996186 0.0872547 2.33552e-08 + outer loop + vertex -34.1535 8.01676 3.05176e-07 + vertex -34.19 7.60002 74.7199 + vertex -34.1535 8.01674 74.7198 + endloop + endfacet + facet normal -0.996187 0.0872464 0 + outer loop + vertex -34.19 7.6 6.86645e-07 + vertex -34.1535 8.01676 3.05176e-07 + vertex -34.19 7.6 3.05176e-07 + endloop + endfacet + facet normal -0.996187 0.0872464 -2.33529e-08 + outer loop + vertex -34.1535 8.01676 3.05176e-07 + vertex -34.19 7.6 6.86645e-07 + vertex -34.19 7.60002 74.7199 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 7.60002 74.7199 + vertex -34.19 7.6 6.86645e-07 + vertex -34.19 7.6 74.7199 + endloop + endfacet + facet normal -0.906304 0.422626 0 + outer loop + vertex -33.8685 8.8 3.05176e-07 + vertex -34.0453 8.42086 74.72 + vertex -33.8685 8.8 74.72 + endloop + endfacet + facet normal -0.906308 0.422617 -5.65601e-08 + outer loop + vertex -34.0453 8.42085 3.05176e-07 + vertex -34.0453 8.42086 74.72 + vertex -33.8685 8.8 3.05176e-07 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.0453 8.42086 74.72 + vertex -34.0453 8.42085 3.05176e-07 + vertex -34.0453 8.42085 74.72 + endloop + endfacet + facet normal 0.422571 0.90633 -0 + outer loop + vertex -30.59 9.67846 3.05176e-07 + vertex -30.9692 9.85526 6.86645e-07 + vertex -30.59 9.67846 6.86645e-07 + endloop + endfacet + facet normal 0.422571 0.90633 0 + outer loop + vertex -30.9692 9.85526 6.86645e-07 + vertex -30.59 9.67846 3.05176e-07 + vertex -30.9692 9.85526 3.05176e-07 + endloop + endfacet + facet normal 0.573559 0.819165 -0 + outer loop + vertex -30.2473 9.43851 3.05176e-07 + vertex -30.59 9.67846 6.86645e-07 + vertex -30.2473 9.43851 6.86645e-07 + endloop + endfacet + facet normal 0.573559 0.819165 0 + outer loop + vertex -30.59 9.67846 6.86645e-07 + vertex -30.2473 9.43851 3.05176e-07 + vertex -30.59 9.67846 3.05176e-07 + endloop + endfacet + facet normal 0.966011 0.258502 0 + outer loop + vertex -29.4979 8.28333 6.86645e-07 + vertex -29.5347 8.42085 3.05176e-07 + vertex -29.5347 8.42085 6.86645e-07 + endloop + endfacet + facet normal 0.966011 0.258502 0 + outer loop + vertex -29.5347 8.42085 3.05176e-07 + vertex -29.4979 8.28333 6.86645e-07 + vertex -29.4979 8.28333 3.05176e-07 + endloop + endfacet + facet normal -0.819101 0.57365 0 + outer loop + vertex -33.6285 9.14269 3.05176e-07 + vertex -33.8685 8.8 74.72 + vertex -33.6285 9.14269 74.72 + endloop + endfacet + facet normal -0.819101 0.57365 0 + outer loop + vertex -33.8685 8.8 3.05176e-07 + vertex -33.8685 8.8 74.72 + vertex -33.6285 9.14269 3.05176e-07 + endloop + endfacet + facet normal -0.819101 -0.57365 0 + outer loop + vertex -33.6285 6.05731 3.05176e-07 + vertex -33.8685 6.4 6.86645e-07 + vertex -33.8685 6.4 3.05176e-07 + endloop + endfacet + facet normal -0.819101 -0.57365 0 + outer loop + vertex -33.8685 6.4 6.86645e-07 + vertex -33.6285 6.05731 3.05176e-07 + vertex -33.6285 6.05731 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.3327 5.76149 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -33.2453 5.70034 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.6285 6.05731 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -33.3327 5.76149 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.4979 8.28333 3.05176e-07 + vertex -29.7115 8.8 3.05176e-07 + vertex -29.5347 8.42085 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -29.7115 8.8 3.05176e-07 + vertex -29.4979 8.28333 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -29.7115 8.8 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -29.9515 9.14269 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -29.9515 9.14269 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -30.2473 9.43851 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -30.2473 9.43851 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -30.59 9.67846 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -30.59 9.67846 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -30.9692 9.85526 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -30.9692 9.85526 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -31.3732 9.96354 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -31.3732 9.96354 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -31.79 10 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.79 10 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -32.2068 9.96354 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.2068 9.96354 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -32.6108 9.85526 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.6108 9.85526 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -32.99 9.67846 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -33.3327 9.43851 3.05176e-07 + vertex -32.99 9.67846 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.8685 6.4 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -33.6285 6.05731 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -34.0453 6.77915 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -33.8685 6.4 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -33.6285 9.14269 3.05176e-07 + vertex -33.3327 9.43851 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -34.1535 7.18324 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -34.0453 6.77915 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -33.8685 8.8 3.05176e-07 + vertex -33.6285 9.14269 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -34.19 7.6 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -34.1535 7.18324 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -34.0453 8.42085 3.05176e-07 + vertex -33.8685 8.8 3.05176e-07 + endloop + endfacet + facet normal 0 -0 -1 + outer loop + vertex -34.1535 8.01676 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -34.19 7.6 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.2453 8.23333 3.05176e-07 + vertex -34.0453 8.42085 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + endloop + endfacet + facet normal 0 -0 -1 + outer loop + vertex -34.0453 8.42085 3.05176e-07 + vertex -33.2453 8.23333 3.05176e-07 + vertex -34.1535 8.01676 3.05176e-07 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.0453 8.42085 3.05176e-07 + vertex -34.0453 8.42084 74.7199 + vertex -34.0453 8.42085 74.72 + endloop + endfacet + facet normal -0.965966 0.258668 3.46184e-08 + outer loop + vertex -34.0453 8.42085 3.05176e-07 + vertex -34.1535 8.01678 74.7199 + vertex -34.0453 8.42084 74.7199 + endloop + endfacet + facet normal -0.965971 0.25865 -6.9232e-08 + outer loop + vertex -34.1535 8.01676 3.05176e-07 + vertex -34.1535 8.01678 74.7199 + vertex -34.0453 8.42085 3.05176e-07 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.1535 8.01678 74.7199 + vertex -34.1535 8.01676 3.05176e-07 + vertex -34.1535 8.01676 74.7199 + endloop + endfacet + facet normal 0.707131 0.707083 0 + outer loop + vertex -29.9515 9.14269 6.86645e-07 + vertex -30.2473 9.43851 3.05176e-07 + vertex -30.2473 9.43851 6.86645e-07 + endloop + endfacet + facet normal 0.707131 0.707083 0 + outer loop + vertex -30.2473 9.43851 3.05176e-07 + vertex -29.9515 9.14269 6.86645e-07 + vertex -29.9515 9.14269 3.05176e-07 + endloop + endfacet + facet normal 0.819101 0.57365 0 + outer loop + vertex -29.7115 8.8 6.86645e-07 + vertex -29.9515 9.14269 3.05176e-07 + vertex -29.9515 9.14269 6.86645e-07 + endloop + endfacet + facet normal 0.819101 0.57365 0 + outer loop + vertex -29.9515 9.14269 3.05176e-07 + vertex -29.7115 8.8 6.86645e-07 + vertex -29.7115 8.8 3.05176e-07 + endloop + endfacet + facet normal -0.422571 0.90633 0 + outer loop + vertex -32.6108 9.85526 3.05176e-07 + vertex -32.99 9.67846 74.72 + vertex -32.6108 9.85526 74.72 + endloop + endfacet + facet normal -0.422571 0.90633 0 + outer loop + vertex -32.99 9.67846 74.72 + vertex -32.6108 9.85526 3.05176e-07 + vertex -32.99 9.67846 3.05176e-07 + endloop + endfacet + facet normal -0.258883 0.965909 0 + outer loop + vertex -32.2068 9.96354 3.05176e-07 + vertex -32.6108 9.85526 74.72 + vertex -32.2068 9.96354 74.72 + endloop + endfacet + facet normal -0.258883 0.965909 0 + outer loop + vertex -32.6108 9.85526 74.72 + vertex -32.2068 9.96354 3.05176e-07 + vertex -32.6108 9.85526 3.05176e-07 + endloop + endfacet + facet normal -0.906308 -0.422617 0 + outer loop + vertex -33.8685 6.4 3.05176e-07 + vertex -34.0453 6.77915 6.86645e-07 + vertex -34.0453 6.77915 3.05176e-07 + endloop + endfacet + facet normal -0.906308 -0.422617 0 + outer loop + vertex -34.0453 6.77915 6.86645e-07 + vertex -33.8685 6.4 3.05176e-07 + vertex -33.8685 6.4 6.86645e-07 + endloop + endfacet + facet normal -0.965971 -0.25865 0 + outer loop + vertex -34.0453 6.77915 3.05176e-07 + vertex -34.1535 7.18324 6.86645e-07 + vertex -34.1535 7.18324 3.05176e-07 + endloop + endfacet + facet normal -0.965971 -0.25865 0 + outer loop + vertex -34.1535 7.18324 6.86645e-07 + vertex -34.0453 6.77915 3.05176e-07 + vertex -34.0453 6.77915 6.86645e-07 + endloop + endfacet + facet normal -0.996187 -0.0872464 0 + outer loop + vertex -34.1535 7.18324 3.05176e-07 + vertex -34.19 7.6 6.86645e-07 + vertex -34.19 7.6 3.05176e-07 + endloop + endfacet + facet normal -0.996187 -0.0872464 0 + outer loop + vertex -34.19 7.6 6.86645e-07 + vertex -34.1535 7.18324 3.05176e-07 + vertex -34.1535 7.18324 6.86645e-07 + endloop + endfacet + facet normal -0.707131 -0.707083 0 + outer loop + vertex -33.3327 5.76149 3.05176e-07 + vertex -33.6285 6.05731 6.86645e-07 + vertex -33.6285 6.05731 3.05176e-07 + endloop + endfacet + facet normal -0.707131 -0.707083 0 + outer loop + vertex -33.6285 6.05731 6.86645e-07 + vertex -33.3327 5.76149 3.05176e-07 + vertex -33.3327 5.76149 6.86645e-07 + endloop + endfacet + facet normal -0.573274 -0.819364 0 + outer loop + vertex -33.3327 5.76149 3.05176e-07 + vertex -33.2453 5.70034 6.86645e-07 + vertex -33.3327 5.76149 6.86645e-07 + endloop + endfacet + facet normal -0.573274 -0.819364 -0 + outer loop + vertex -33.2453 5.70034 6.86645e-07 + vertex -33.3327 5.76149 3.05176e-07 + vertex -33.2453 5.70034 3.05176e-07 + endloop + endfacet + facet normal -0.707131 0.707083 0 + outer loop + vertex -33.3327 9.43851 3.05176e-07 + vertex -33.6285 9.14269 74.72 + vertex -33.3327 9.43851 74.72 + endloop + endfacet + facet normal -0.707131 0.707083 0 + outer loop + vertex -33.6285 9.14269 3.05176e-07 + vertex -33.6285 9.14269 74.72 + vertex -33.3327 9.43851 3.05176e-07 + endloop + endfacet + facet normal -0.573559 0.819165 0 + outer loop + vertex -32.99 9.67846 3.05176e-07 + vertex -33.3327 9.43851 74.72 + vertex -32.99 9.67846 74.72 + endloop + endfacet + facet normal -0.573559 0.819165 0 + outer loop + vertex -33.3327 9.43851 74.72 + vertex -32.99 9.67846 3.05176e-07 + vertex -33.3327 9.43851 3.05176e-07 + endloop + endfacet + facet normal -0.0872381 0 0.996187 + outer loop + vertex -32.2068 3.99999 80.6835 + vertex -31.79 0.0078125 80.72 + vertex -31.79 4 80.72 + endloop + endfacet + facet normal -0.0872381 0 0.996187 + outer loop + vertex -31.79 0.0078125 80.72 + vertex -32.2068 3.99999 80.6835 + vertex -32.2068 0.0078125 80.6835 + endloop + endfacet + facet normal -0.996187 4.37064e-06 0.0872381 + outer loop + vertex -34.1535 0.0078125 78.7368 + vertex -34.1535 3.99982 78.7366 + vertex -34.19 0.0078125 78.32 + endloop + endfacet + facet normal -0.996184 0 0.0872797 + outer loop + vertex -34.19 0.0078125 78.32 + vertex -34.1535 3.99982 78.7366 + vertex -34.19 3.99996 78.32 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.1535 3.99982 78.7366 + vertex -34.1535 0.0078125 78.7368 + vertex -34.1535 3.99982 78.7367 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.1535 3.99982 78.7367 + vertex -34.1535 0.0078125 78.7368 + vertex -34.1535 3.99994 78.7368 + endloop + endfacet + facet normal -0.707107 0 0.707107 + outer loop + vertex -33.3327 0.0078125 80.1585 + vertex -33.3327 3.99998 80.1585 + vertex -33.6285 0.0078125 79.8627 + endloop + endfacet + facet normal -0.707107 0 0.707107 + outer loop + vertex -33.6285 3.99998 79.8627 + vertex -33.6285 0.0078125 79.8627 + vertex -33.3327 3.99998 80.1585 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.6109 3.99999 80.5753 + vertex -32.6108 0.0078125 80.5753 + vertex -32.6108 3.99999 80.5753 + endloop + endfacet + facet normal -0.422813 -1.0591e-05 0.906217 + outer loop + vertex -32.8831 4 80.4483 + vertex -32.6108 0.0078125 80.5753 + vertex -32.6109 3.99999 80.5753 + endloop + endfacet + facet normal -0.422282 0 0.906465 + outer loop + vertex -32.99 0.0078125 80.3985 + vertex -32.8831 4 80.4483 + vertex -32.99 4 80.3985 + endloop + endfacet + facet normal -0.422571 9.43546e-06 0.90633 + outer loop + vertex -32.8831 4 80.4483 + vertex -32.99 0.0078125 80.3985 + vertex -32.6108 0.0078125 80.5753 + endloop + endfacet + facet normal -0.258704 0 0.965957 + outer loop + vertex -32.6108 3.99999 80.5753 + vertex -32.2068 0.0078125 80.6835 + vertex -32.2068 3.99999 80.6835 + endloop + endfacet + facet normal -0.258704 0 0.965957 + outer loop + vertex -32.2068 0.0078125 80.6835 + vertex -32.6108 3.99999 80.5753 + vertex -32.6108 0.0078125 80.5753 + endloop + endfacet + facet normal -0.573888 0 0.818934 + outer loop + vertex -33.0545 4 80.3533 + vertex -32.99 0.0078125 80.3985 + vertex -32.99 4 80.3985 + endloop + endfacet + facet normal -0.573581 0 0.819149 + outer loop + vertex -33.3327 0.0078125 80.1585 + vertex -33.0545 4 80.3533 + vertex -33.3327 3.99998 80.1585 + endloop + endfacet + facet normal -0.573639 6.01105e-06 0.819108 + outer loop + vertex -33.0545 4 80.3533 + vertex -33.3327 0.0078125 80.1585 + vertex -32.99 0.0078125 80.3985 + endloop + endfacet + facet normal -0.90633 0 0.422571 + outer loop + vertex -34.0453 0.0078125 79.1408 + vertex -33.8685 4 79.52 + vertex -34.0453 3.99996 79.1408 + endloop + endfacet + facet normal -0.90633 0 0.422571 + outer loop + vertex -33.8685 4 79.52 + vertex -34.0453 0.0078125 79.1408 + vertex -33.8685 0.0078125 79.52 + endloop + endfacet + facet normal -0.965957 0 0.258704 + outer loop + vertex -34.1535 0.0078125 78.7368 + vertex -34.0453 3.99994 79.1408 + vertex -34.1535 3.99994 78.7368 + endloop + endfacet + facet normal -0.965957 0 0.258704 + outer loop + vertex -34.0453 0.0078125 79.1408 + vertex -34.0453 3.99994 79.1408 + vertex -34.1535 0.0078125 78.7368 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -34.0453 0.0078125 79.1408 + vertex -34.0453 3.99996 79.1408 + vertex -34.0453 3.99994 79.1408 + endloop + endfacet + facet normal -0.816416 0 0.577465 + outer loop + vertex -33.8685 0.0078125 79.52 + vertex -33.8656 4 79.5241 + vertex -33.8685 4 79.52 + endloop + endfacet + facet normal -0.819141 5.95682e-06 0.573592 + outer loop + vertex -33.8656 4 79.5241 + vertex -33.8685 0.0078125 79.52 + vertex -33.6285 3.99997 79.8627 + endloop + endfacet + facet normal -0.819108 0 0.573639 + outer loop + vertex -33.6285 0.0078125 79.8627 + vertex -33.6285 3.99997 79.8627 + vertex -33.8685 0.0078125 79.52 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -33.6285 0.0078125 79.8627 + vertex -33.6285 3.99998 79.8627 + vertex -33.6285 3.99997 79.8627 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -34.19 0.0078125 78.32 + vertex -34.19 3.99996 78.32 + vertex -34.19 3.99994 78.32 + endloop + endfacet + facet normal 0.0871432 0.996196 -0 + outer loop + vertex 32.2068 9.96354 3.05176e-07 + vertex 31.79 10 74.72 + vertex 32.2068 9.96354 74.72 + endloop + endfacet + facet normal 0.0871432 0.996196 0 + outer loop + vertex 31.79 10 74.72 + vertex 32.2068 9.96354 3.05176e-07 + vertex 31.79 10 3.05176e-07 + endloop + endfacet + facet normal 0.258883 0.965909 -0 + outer loop + vertex 32.6108 9.85526 3.05176e-07 + vertex 32.2068 9.96354 74.72 + vertex 32.6108 9.85526 74.72 + endloop + endfacet + facet normal 0.258883 0.965909 0 + outer loop + vertex 32.2068 9.96354 74.72 + vertex 32.6108 9.85526 3.05176e-07 + vertex 32.2068 9.96354 3.05176e-07 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.59999 74.7199 + vertex 34.19 7.6 74.72 + vertex 34.19 7.59999 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 7.6 74.72 + vertex 34.19 7.59999 74.7199 + vertex 34.19 7.6 3.05176e-07 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 7.6 3.05176e-07 + vertex 34.19 7.59999 74.7199 + endloop + endfacet + facet normal 0.996187 -0.0872485 0 + outer loop + vertex 34.1535 7.18324 3.05176e-07 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.1535 7.18324 6.86645e-07 + endloop + endfacet + facet normal 0.399085 -0.034952 -0.916248 + outer loop + vertex 34.19 7.59999 6.86645e-07 + vertex 34.1535 7.18324 3.05176e-07 + vertex 34.19 7.6 3.05176e-07 + endloop + endfacet + facet normal 0.573274 -0.819364 0 + outer loop + vertex 33.2453 5.70034 3.05176e-07 + vertex 33.3327 5.76149 6.86645e-07 + vertex 33.2453 5.70034 6.86645e-07 + endloop + endfacet + facet normal 0.573274 -0.819364 0 + outer loop + vertex 33.3327 5.76149 6.86645e-07 + vertex 33.2453 5.70034 3.05176e-07 + vertex 33.3327 5.76149 3.05176e-07 + endloop + endfacet + facet normal 0.906308 -0.422617 0 + outer loop + vertex 33.8685 6.4 6.86645e-07 + vertex 34.0453 6.77915 3.05176e-07 + vertex 34.0453 6.77915 6.86645e-07 + endloop + endfacet + facet normal 0.906308 -0.422617 0 + outer loop + vertex 34.0453 6.77915 3.05176e-07 + vertex 33.8685 6.4 6.86645e-07 + vertex 33.8685 6.4 3.05176e-07 + endloop + endfacet + facet normal 0.906429 0.422359 0 + outer loop + vertex 33.9533 8.61801 74.72 + vertex 33.8685 8.8 3.05176e-07 + vertex 33.8685 8.8 74.72 + endloop + endfacet + facet normal 0.906197 0.422855 1.4709e-06 + outer loop + vertex 34.0453 8.42085 74.72 + vertex 33.8685 8.8 3.05176e-07 + vertex 33.9533 8.61801 74.72 + endloop + endfacet + facet normal 0.906308 0.422617 0 + outer loop + vertex 33.8685 8.8 3.05176e-07 + vertex 34.0453 8.42085 74.72 + vertex 34.0453 8.42085 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.9533 8.61801 74.72 + vertex 33.8685 8.8 74.72 + vertex 33.9577 8.60873 74.72 + endloop + endfacet + facet normal 0.965971 -0.25865 0 + outer loop + vertex 34.0453 6.77915 6.86645e-07 + vertex 34.1535 7.18324 3.05176e-07 + vertex 34.1535 7.18324 6.86645e-07 + endloop + endfacet + facet normal 0.965971 -0.25865 0 + outer loop + vertex 34.1535 7.18324 3.05176e-07 + vertex 34.0453 6.77915 6.86645e-07 + vertex 34.0453 6.77915 3.05176e-07 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex 31.79 10 74.72 + vertex 31.79 10 3.05176e-07 + vertex 31.79 10 6.86645e-07 + endloop + endfacet + facet normal -0.0871432 0.996196 0 + outer loop + vertex 31.3732 9.96354 3.05176e-07 + vertex 31.79 10 6.86645e-07 + vertex 31.79 10 3.05176e-07 + endloop + endfacet + facet normal -0.0871432 0.996196 0 + outer loop + vertex 31.79 10 6.86645e-07 + vertex 31.3732 9.96354 3.05176e-07 + vertex 31.3732 9.96354 6.86645e-07 + endloop + endfacet + facet normal -0.906308 0.422617 0 + outer loop + vertex 29.5347 8.42085 3.05176e-07 + vertex 29.7115 8.8 6.86645e-07 + vertex 29.7115 8.8 3.05176e-07 + endloop + endfacet + facet normal -0.906308 0.422617 0 + outer loop + vertex 29.7115 8.8 6.86645e-07 + vertex 29.5347 8.42085 3.05176e-07 + vertex 29.5347 8.42085 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.99 9.67846 74.72 + vertex 32.6108 9.85526 74.72 + vertex 32.808 9.76332 74.72 + endloop + endfacet + facet normal 0.422571 0.90633 -0 + outer loop + vertex 32.99 9.67846 3.05176e-07 + vertex 32.6108 9.85526 74.72 + vertex 32.99 9.67846 74.72 + endloop + endfacet + facet normal 0.422571 0.90633 0 + outer loop + vertex 32.6108 9.85526 74.72 + vertex 32.99 9.67846 3.05176e-07 + vertex 32.6108 9.85526 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.808 9.76332 74.72 + vertex 32.7653 9.78323 74.72 + vertex 32.99 9.67846 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 33.3327 9.43851 74.72 + vertex 32.99 9.67846 74.72 + vertex 33.1545 9.56328 74.72 + endloop + endfacet + facet normal 0.573559 0.819165 -0 + outer loop + vertex 33.3327 9.43851 3.05176e-07 + vertex 32.99 9.67846 74.72 + vertex 33.3327 9.43851 74.72 + endloop + endfacet + facet normal 0.573559 0.819165 0 + outer loop + vertex 32.99 9.67846 74.72 + vertex 33.3327 9.43851 3.05176e-07 + vertex 32.99 9.67846 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.1545 9.56328 74.72 + vertex 32.99 9.67846 74.72 + vertex 33.1802 9.54529 74.72 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.1535 8.01675 74.7199 + vertex 34.1535 8.01676 3.05176e-07 + vertex 34.1535 8.01676 74.72 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.1535 8.01671 74.7199 + vertex 34.1535 8.01676 3.05176e-07 + vertex 34.1535 8.01675 74.7199 + endloop + endfacet + facet normal 0.996177 0.0873557 5.84555e-08 + outer loop + vertex 34.1725 7.80004 74.72 + vertex 34.1535 8.01676 3.05176e-07 + vertex 34.1535 8.01671 74.7199 + endloop + endfacet + facet normal 0.996195 0.0871497 -5.43852e-07 + outer loop + vertex 34.19 7.6 74.72 + vertex 34.1535 8.01676 3.05176e-07 + vertex 34.1725 7.80004 74.72 + endloop + endfacet + facet normal 0.996187 0.0872464 0 + outer loop + vertex 34.1535 8.01676 3.05176e-07 + vertex 34.19 7.6 74.72 + vertex 34.19 7.6 3.05176e-07 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 34.1723 7.80196 74.72 + vertex 34.19 7.6 74.72 + vertex 34.1725 7.80004 74.72 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.0453 8.42083 74.72 + vertex 34.0453 8.42085 3.05176e-07 + vertex 34.0453 8.42085 74.72 + endloop + endfacet + facet normal 0.965968 0.258662 6.92351e-08 + outer loop + vertex 34.1535 8.01676 74.72 + vertex 34.0453 8.42085 3.05176e-07 + vertex 34.0453 8.42083 74.72 + endloop + endfacet + facet normal 0.965971 0.25865 0 + outer loop + vertex 34.0453 8.42085 3.05176e-07 + vertex 34.1535 8.01676 74.72 + vertex 34.1535 8.01676 3.05176e-07 + endloop + endfacet + facet normal 0.819101 -0.57365 0 + outer loop + vertex 33.6285 6.05731 6.86645e-07 + vertex 33.8685 6.4 3.05176e-07 + vertex 33.8685 6.4 6.86645e-07 + endloop + endfacet + facet normal 0.819101 -0.57365 0 + outer loop + vertex 33.8685 6.4 3.05176e-07 + vertex 33.6285 6.05731 6.86645e-07 + vertex 33.6285 6.05731 3.05176e-07 + endloop + endfacet + facet normal -0.819101 0.57365 0 + outer loop + vertex 29.7115 8.8 3.05176e-07 + vertex 29.9515 9.14269 6.86645e-07 + vertex 29.9515 9.14269 3.05176e-07 + endloop + endfacet + facet normal -0.819101 0.57365 0 + outer loop + vertex 29.9515 9.14269 6.86645e-07 + vertex 29.7115 8.8 3.05176e-07 + vertex 29.7115 8.8 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 34.19 7.6 3.05176e-07 + vertex 34.1535 7.18324 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 34.1535 7.18324 3.05176e-07 + vertex 34.0453 6.77915 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 7.6 3.05176e-07 + vertex 33.2453 8.23333 3.05176e-07 + vertex 34.1535 8.01676 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 34.0453 6.77915 3.05176e-07 + vertex 33.8685 6.4 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.1535 8.01676 3.05176e-07 + vertex 33.2453 8.23333 3.05176e-07 + vertex 34.0453 8.42085 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 33.8685 6.4 3.05176e-07 + vertex 33.6285 6.05731 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 34.0453 8.42085 3.05176e-07 + vertex 33.2453 8.23333 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 33.6285 6.05731 3.05176e-07 + vertex 33.3327 5.76149 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 34.0453 8.42085 3.05176e-07 + vertex 33.2453 8.28333 3.05176e-07 + vertex 33.8685 8.8 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 33.3327 5.76149 3.05176e-07 + vertex 33.2453 5.70034 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 33.8685 8.8 3.05176e-07 + vertex 33.2453 8.28333 3.05176e-07 + vertex 33.6285 9.14269 3.05176e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 33.6285 9.14269 3.05176e-07 + vertex 33.2453 8.28333 3.05176e-07 + vertex 33.3327 9.43851 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 32.99 9.67846 3.05176e-07 + vertex 33.3327 9.43851 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 32.6108 9.85526 3.05176e-07 + vertex 32.99 9.67846 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 32.2068 9.96354 3.05176e-07 + vertex 32.6108 9.85526 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 31.79 10 3.05176e-07 + vertex 32.2068 9.96354 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 31.3732 9.96354 3.05176e-07 + vertex 31.79 10 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 30.9692 9.85526 3.05176e-07 + vertex 31.3732 9.96354 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 30.59 9.67846 3.05176e-07 + vertex 30.9692 9.85526 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 30.2473 9.43851 3.05176e-07 + vertex 30.59 9.67846 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 29.9515 9.14269 3.05176e-07 + vertex 30.2473 9.43851 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 29.7115 8.8 3.05176e-07 + vertex 29.9515 9.14269 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.4979 8.28333 3.05176e-07 + vertex 29.7115 8.8 3.05176e-07 + vertex 33.2453 8.28333 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.7115 8.8 3.05176e-07 + vertex 29.4979 8.28333 3.05176e-07 + vertex 29.5347 8.42085 3.05176e-07 + endloop + endfacet + facet normal -0.966011 0.258502 0 + outer loop + vertex 29.4979 8.28333 3.05176e-07 + vertex 29.5347 8.42085 6.86645e-07 + vertex 29.5347 8.42085 3.05176e-07 + endloop + endfacet + facet normal -0.966011 0.258502 0 + outer loop + vertex 29.5347 8.42085 6.86645e-07 + vertex 29.4979 8.28333 3.05176e-07 + vertex 29.4979 8.28333 6.86645e-07 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 33.3327 9.4385 74.72 + vertex 33.3327 9.43851 3.05176e-07 + vertex 33.3327 9.43851 74.72 + endloop + endfacet + facet normal 0.707119 0.707095 9.46326e-08 + outer loop + vertex 33.6285 9.14269 74.72 + vertex 33.3327 9.43851 3.05176e-07 + vertex 33.3327 9.4385 74.72 + endloop + endfacet + facet normal 0.707131 0.707083 0 + outer loop + vertex 33.3327 9.43851 3.05176e-07 + vertex 33.6285 9.14269 74.72 + vertex 33.6285 9.14269 3.05176e-07 + endloop + endfacet + facet normal 0.819102 0.573647 0 + outer loop + vertex 33.7533 8.96449 74.72 + vertex 33.6285 9.14269 3.05176e-07 + vertex 33.6285 9.14269 74.72 + endloop + endfacet + facet normal 0.819099 0.573653 1.91934e-08 + outer loop + vertex 33.8685 8.8 74.72 + vertex 33.6285 9.14269 3.05176e-07 + vertex 33.7533 8.96449 74.72 + endloop + endfacet + facet normal 0.819101 0.57365 0 + outer loop + vertex 33.6285 9.14269 3.05176e-07 + vertex 33.8685 8.8 74.72 + vertex 33.8685 8.8 3.05176e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.8685 8.8 74.72 + vertex 33.7533 8.96449 74.72 + vertex 33.7445 8.97709 74.72 + endloop + endfacet + facet normal 0.707131 -0.707083 0 + outer loop + vertex 33.3327 5.76149 6.86645e-07 + vertex 33.6285 6.05731 3.05176e-07 + vertex 33.6285 6.05731 6.86645e-07 + endloop + endfacet + facet normal 0.707131 -0.707083 0 + outer loop + vertex 33.6285 6.05731 3.05176e-07 + vertex 33.3327 5.76149 6.86645e-07 + vertex 33.3327 5.76149 3.05176e-07 + endloop + endfacet + facet normal -0.422571 0.90633 0 + outer loop + vertex 30.9692 9.85526 3.05176e-07 + vertex 30.59 9.67846 6.86645e-07 + vertex 30.9692 9.85526 6.86645e-07 + endloop + endfacet + facet normal -0.422571 0.90633 0 + outer loop + vertex 30.59 9.67846 6.86645e-07 + vertex 30.9692 9.85526 3.05176e-07 + vertex 30.59 9.67846 3.05176e-07 + endloop + endfacet + facet normal -0.258883 0.965909 0 + outer loop + vertex 31.3732 9.96354 3.05176e-07 + vertex 30.9692 9.85526 6.86645e-07 + vertex 31.3732 9.96354 6.86645e-07 + endloop + endfacet + facet normal -0.258883 0.965909 0 + outer loop + vertex 30.9692 9.85526 6.86645e-07 + vertex 31.3732 9.96354 3.05176e-07 + vertex 30.9692 9.85526 3.05176e-07 + endloop + endfacet + facet normal -0.707131 0.707083 0 + outer loop + vertex 29.9515 9.14269 3.05176e-07 + vertex 30.2473 9.43851 6.86645e-07 + vertex 30.2473 9.43851 3.05176e-07 + endloop + endfacet + facet normal -0.707131 0.707083 0 + outer loop + vertex 30.2473 9.43851 6.86645e-07 + vertex 29.9515 9.14269 3.05176e-07 + vertex 29.9515 9.14269 6.86645e-07 + endloop + endfacet + facet normal -0.573559 0.819165 0 + outer loop + vertex 30.59 9.67846 3.05176e-07 + vertex 30.2473 9.43851 6.86645e-07 + vertex 30.59 9.67846 6.86645e-07 + endloop + endfacet + facet normal -0.573559 0.819165 0 + outer loop + vertex 30.2473 9.43851 6.86645e-07 + vertex 30.59 9.67846 3.05176e-07 + vertex 30.2473 9.43851 3.05176e-07 + endloop + endfacet + facet normal 0.707107 -0 0.707107 + outer loop + vertex 33.3327 0.0078125 80.1585 + vertex 33.6285 3.99999 79.8627 + vertex 33.3327 3.99999 80.1585 + endloop + endfacet + facet normal 0.707107 -0 0.707107 + outer loop + vertex 33.6285 0.0078125 79.8627 + vertex 33.6285 3.99999 79.8627 + vertex 33.3327 0.0078125 80.1585 + endloop + endfacet + facet normal 0.90636 -0 0.422507 + outer loop + vertex 33.8685 0.0078125 79.52 + vertex 33.9601 4 79.3235 + vertex 33.8685 4 79.52 + endloop + endfacet + facet normal 0.90633 3.87956e-06 0.422571 + outer loop + vertex 34.0453 0.0078125 79.1408 + vertex 33.9601 4 79.3235 + vertex 33.8685 0.0078125 79.52 + endloop + endfacet + facet normal 0.906208 -1.05915e-05 0.422831 + outer loop + vertex 33.9601 4 79.3235 + vertex 34.0453 0.0078125 79.1408 + vertex 34.0453 3.99998 79.1409 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.0453 3.99998 79.1409 + vertex 34.0453 0.0078125 79.1408 + vertex 34.0453 3.99998 79.1408 + endloop + endfacet + facet normal 0.996195 2.18319e-06 0.0871553 + outer loop + vertex 34.1535 0.0078125 78.7368 + vertex 34.1721 4 78.5241 + vertex 34.1535 3.99993 78.7367 + endloop + endfacet + facet normal 0.996187 6.6275e-06 0.0872381 + outer loop + vertex 34.19 0.0078125 78.32 + vertex 34.1721 4 78.5241 + vertex 34.1535 0.0078125 78.7368 + endloop + endfacet + facet normal 0.996176 0 0.0873668 + outer loop + vertex 34.1721 4 78.5241 + vertex 34.19 0.0078125 78.32 + vertex 34.19 4 78.32 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.1535 3.99997 78.7368 + vertex 34.1535 0.0078125 78.7368 + vertex 34.1535 3.99993 78.7367 + endloop + endfacet + facet normal 0.819256 -0 0.573428 + outer loop + vertex 33.6285 0.0078125 79.8627 + vertex 33.7414 4 79.7014 + vertex 33.6285 3.99999 79.8627 + endloop + endfacet + facet normal 0.819108 1.26747e-05 0.573639 + outer loop + vertex 33.8685 0.0078125 79.52 + vertex 33.7414 4 79.7014 + vertex 33.6285 0.0078125 79.8627 + endloop + endfacet + facet normal 0.818977 0 0.573826 + outer loop + vertex 33.7414 4 79.7014 + vertex 33.8685 0.0078125 79.52 + vertex 33.8685 4 79.52 + endloop + endfacet + facet normal 0.573462 0 0.819232 + outer loop + vertex 33.1787 4 80.2663 + vertex 33.3327 0.0078125 80.1585 + vertex 33.3327 3.99999 80.1585 + endloop + endfacet + facet normal 0.573783 -0 0.819008 + outer loop + vertex 32.99 0.0078125 80.3985 + vertex 33.1787 4 80.2663 + vertex 32.99 4 80.3985 + endloop + endfacet + facet normal 0.573639 1.01421e-05 0.819108 + outer loop + vertex 33.1787 4 80.2663 + vertex 32.99 0.0078125 80.3985 + vertex 33.3327 0.0078125 80.1585 + endloop + endfacet + facet normal 0.422536 0 0.906346 + outer loop + vertex 32.777 4 80.4978 + vertex 32.99 0.0078125 80.3985 + vertex 32.99 4 80.3985 + endloop + endfacet + facet normal 0.422617 -0 0.906308 + outer loop + vertex 32.6108 0.0078125 80.5753 + vertex 32.777 4 80.4978 + vertex 32.6108 3.99999 80.5753 + endloop + endfacet + facet normal 0.422571 2.299e-06 0.90633 + outer loop + vertex 32.777 4 80.4978 + vertex 32.6108 0.0078125 80.5753 + vertex 32.99 0.0078125 80.3985 + endloop + endfacet + facet normal 0.258704 0 0.965957 + outer loop + vertex 32.2068 3.99999 80.6835 + vertex 32.6108 0.0078125 80.5753 + vertex 32.6108 3.99999 80.5753 + endloop + endfacet + facet normal 0.258704 0 0.965957 + outer loop + vertex 32.6108 0.0078125 80.5753 + vertex 32.2068 3.99999 80.6835 + vertex 32.2068 0.0078125 80.6835 + endloop + endfacet + facet normal 0 0 -0 + outer loop + vertex 32.2068 3.99999 80.6835 + vertex 32.2068 4 80.6835 + vertex 32.2068 0.0078125 80.6835 + endloop + endfacet + facet normal 0.0872381 0 0.996187 + outer loop + vertex 31.79 4 80.72 + vertex 32.2068 0.0078125 80.6835 + vertex 32.2068 4 80.6835 + endloop + endfacet + facet normal 0.0872381 0 0.996187 + outer loop + vertex 32.2068 0.0078125 80.6835 + vertex 31.79 4 80.72 + vertex 31.79 0.0078125 80.72 + endloop + endfacet + facet normal 0.965957 -0 0.258704 + outer loop + vertex 34.0453 0.0078125 79.1408 + vertex 34.1535 3.99997 78.7368 + vertex 34.0453 3.99997 79.1408 + endloop + endfacet + facet normal 0.965957 -0 0.258704 + outer loop + vertex 34.1535 0.0078125 78.7368 + vertex 34.1535 3.99997 78.7368 + vertex 34.0453 0.0078125 79.1408 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex 34.0453 0.0078125 79.1408 + vertex 34.0453 3.99997 79.1408 + vertex 34.0453 3.99998 79.1408 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 0 10 46.485 + vertex -31.79 10 74.72 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 0.54265 10 46.4375 + vertex 0 10 46.485 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 1.06881 10 46.2965 + vertex 0.54265 10 46.4375 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 1.5625 10 46.0663 + vertex 1.06881 10 46.2965 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 2.00871 10 45.7539 + vertex 1.5625 10 46.0663 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 2.39389 10 45.3687 + vertex 2.00871 10 45.7539 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 2.70633 10 44.9225 + vertex 2.39389 10 45.3687 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 2.93654 10 44.4288 + vertex 2.70633 10 44.9225 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 3.07752 10 43.9026 + vertex 2.93654 10 44.4288 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 3.125 10 43.36 + vertex 3.07752 10 43.9026 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 31.79 10 74.72 + vertex 3.07752 10 42.8173 + vertex 3.125 10 43.36 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex 31.79 10 6.86645e-07 + vertex 3.07752 10 42.8173 + vertex 31.79 10 74.72 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 3.07752 10 42.8173 + vertex 31.79 10 6.86645e-07 + vertex 2.93654 10 42.2912 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 2.93654 10 42.2912 + vertex 31.79 10 6.86645e-07 + vertex 2.70633 10 41.7975 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 0.54265 10 40.2825 + vertex 31.79 10 6.86645e-07 + vertex 0 10 40.235 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 1.06881 10 40.4235 + vertex 31.79 10 6.86645e-07 + vertex 0.54265 10 40.2825 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 1.5625 10 40.6537 + vertex 31.79 10 6.86645e-07 + vertex 1.06881 10 40.4235 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 2.00871 10 40.9661 + vertex 31.79 10 6.86645e-07 + vertex 1.5625 10 40.6537 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 2.39389 10 41.3513 + vertex 31.79 10 6.86645e-07 + vertex 2.00871 10 40.9661 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 2.70633 10 41.7975 + vertex 31.79 10 6.86645e-07 + vertex 2.39389 10 41.3513 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -0.54265 10 46.4375 + vertex -31.79 10 74.72 + vertex 0 10 46.485 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -1.06881 10 46.2965 + vertex -31.79 10 74.72 + vertex -0.54265 10 46.4375 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -1.5625 10 46.0663 + vertex -31.79 10 74.72 + vertex -1.06881 10 46.2965 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -2.00871 10 45.7539 + vertex -31.79 10 74.72 + vertex -1.5625 10 46.0663 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -2.39389 10 45.3687 + vertex -31.79 10 74.72 + vertex -2.00871 10 45.7539 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -2.70633 10 44.9225 + vertex -31.79 10 74.72 + vertex -2.39389 10 45.3687 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -2.93654 10 44.4288 + vertex -31.79 10 74.72 + vertex -2.70633 10 44.9225 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -3.07752 10 43.9026 + vertex -31.79 10 74.72 + vertex -2.93654 10 44.4288 + endloop + endfacet + facet normal 0 1 -0 + outer loop + vertex -3.125 10 43.36 + vertex -31.79 10 74.72 + vertex -3.07752 10 43.9026 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -3.07752 10 42.8173 + vertex -31.79 10 74.72 + vertex -3.125 10 43.36 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex -3.07752 10 42.8173 + vertex -2.93654 10 42.2912 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex -2.93654 10 42.2912 + vertex -2.70633 10 41.7975 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex -2.70633 10 41.7975 + vertex -2.39389 10 41.3513 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -31.79 10 6.86645e-07 + vertex 0 10 40.235 + vertex 31.79 10 6.86645e-07 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex 0 10 40.235 + vertex -31.79 10 6.86645e-07 + vertex -0.54265 10 40.2825 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -0.54265 10 40.2825 + vertex -31.79 10 6.86645e-07 + vertex -1.06881 10 40.4235 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -1.06881 10 40.4235 + vertex -31.79 10 6.86645e-07 + vertex -1.5625 10 40.6537 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -1.5625 10 40.6537 + vertex -31.79 10 6.86645e-07 + vertex -2.00871 10 40.9661 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -2.00871 10 40.9661 + vertex -31.79 10 6.86645e-07 + vertex -2.39389 10 41.3513 + endloop + endfacet + facet normal 0 1 0 + outer loop + vertex -3.07752 10 42.8173 + vertex -31.79 10 6.86645e-07 + vertex -31.79 10 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -34.19 0.0078125 6.86645e-07 + vertex -33.2453 5.70034 6.86645e-07 + vertex -33.2453 0.0078125 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -33.2453 5.70034 6.86645e-07 + vertex -34.19 0.0078125 6.86645e-07 + vertex -33.3327 5.76149 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -33.3327 5.76149 6.86645e-07 + vertex -34.19 0.0078125 6.86645e-07 + vertex -33.6285 6.05731 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -33.6285 6.05731 6.86645e-07 + vertex -34.19 0.0078125 6.86645e-07 + vertex -33.8685 6.4 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -33.8685 6.4 6.86645e-07 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.0453 6.77915 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -34.0453 6.77915 6.86645e-07 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.1535 7.18324 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -34.1535 7.18324 6.86645e-07 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 7.6 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.4979 8.28333 6.86645e-07 + vertex 29.5347 8.42085 6.86645e-07 + vertex 29.4979 8.28333 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.5347 8.42085 6.86645e-07 + vertex 29.5347 8.42085 6.86645e-07 + vertex -29.4979 8.28333 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.5347 8.42085 6.86645e-07 + vertex -29.5347 8.42085 6.86645e-07 + vertex 29.7115 8.8 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.7115 8.8 6.86645e-07 + vertex 29.7115 8.8 6.86645e-07 + vertex -29.5347 8.42085 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.7115 8.8 6.86645e-07 + vertex -29.7115 8.8 6.86645e-07 + vertex 29.9515 9.14269 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.9515 9.14269 6.86645e-07 + vertex 29.9515 9.14269 6.86645e-07 + vertex -29.7115 8.8 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.9515 9.14269 6.86645e-07 + vertex -29.9515 9.14269 6.86645e-07 + vertex 30.2473 9.43851 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -30.2473 9.43851 6.86645e-07 + vertex 30.2473 9.43851 6.86645e-07 + vertex -29.9515 9.14269 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 30.2473 9.43851 6.86645e-07 + vertex -30.2473 9.43851 6.86645e-07 + vertex 30.59 9.67846 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -30.59 9.67846 6.86645e-07 + vertex 30.59 9.67846 6.86645e-07 + vertex -30.2473 9.43851 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 30.59 9.67846 6.86645e-07 + vertex -30.59 9.67846 6.86645e-07 + vertex 30.9692 9.85526 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -30.9692 9.85526 6.86645e-07 + vertex 30.9692 9.85526 6.86645e-07 + vertex -30.59 9.67846 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 30.9692 9.85526 6.86645e-07 + vertex -30.9692 9.85526 6.86645e-07 + vertex 31.3732 9.96354 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.3732 9.96354 6.86645e-07 + vertex 31.3732 9.96354 6.86645e-07 + vertex -30.9692 9.85526 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.3732 9.96354 6.86645e-07 + vertex -31.3732 9.96354 6.86645e-07 + vertex 31.79 10 6.86645e-07 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 31.79 10 6.86645e-07 + vertex -31.3732 9.96354 6.86645e-07 + vertex -31.79 10 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 34.1535 7.18324 6.86645e-07 + vertex 34.19 7.59999 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 34.0453 6.77915 6.86645e-07 + vertex 34.1535 7.18324 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 33.8685 6.4 6.86645e-07 + vertex 34.0453 6.77915 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 33.6285 6.05731 6.86645e-07 + vertex 33.8685 6.4 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 33.3327 5.76149 6.86645e-07 + vertex 33.6285 6.05731 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 33.2453 5.70034 6.86645e-07 + vertex 34.19 0.0078125 6.86645e-07 + vertex 33.2453 0.0078125 6.86645e-07 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 33.2453 5.70034 6.86645e-07 + vertex 33.3327 5.76149 6.86645e-07 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 7.6 6.86645e-07 + vertex -34.19 4 71.12 + vertex -34.19 7.6 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 4 71.12 + vertex -34.19 7.6 6.86645e-07 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 4 71.12 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 3.37487 71.1747 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 3.37487 71.1747 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 2.76873 71.3371 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 2.76873 71.3371 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 2.2 71.6023 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 2.2 71.6023 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 1.68597 71.9622 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 1.68597 71.9622 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 1.24224 72.406 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 1.24224 72.406 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 0.882309 72.92 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 0.882309 72.92 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 0.617107 73.4887 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.0078125 78.32 + vertex -34.19 0.454693 74.0949 + vertex -34.19 0.0078125 6.86645e-07 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 0.617107 73.4887 + vertex -34.19 0.0078125 6.86645e-07 + vertex -34.19 0.454693 74.0949 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -34.19 7.6 74.72 + vertex -34.19 4 71.12 + vertex -34.19 4 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 3.37487 78.2653 + vertex -34.19 0.0078125 78.32 + vertex -34.19 4 78.32 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 2.76873 78.1029 + vertex -34.19 0.0078125 78.32 + vertex -34.19 3.37487 78.2653 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 2.2 77.8377 + vertex -34.19 0.0078125 78.32 + vertex -34.19 2.76873 78.1029 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 1.68597 77.4778 + vertex -34.19 0.0078125 78.32 + vertex -34.19 2.2 77.8377 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 1.24224 77.034 + vertex -34.19 0.0078125 78.32 + vertex -34.19 1.68597 77.4778 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.882309 76.52 + vertex -34.19 0.0078125 78.32 + vertex -34.19 1.24224 77.034 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.617107 75.9513 + vertex -34.19 0.0078125 78.32 + vertex -34.19 0.882309 76.52 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.454693 75.3451 + vertex -34.19 0.0078125 78.32 + vertex -34.19 0.617107 75.9513 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.400001 74.72 + vertex -34.19 0.0078125 78.32 + vertex -34.19 0.454693 75.3451 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -34.19 0.454693 74.0949 + vertex -34.19 0.0078125 78.32 + vertex -34.19 0.400001 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 4 78.32 + vertex 34.19 0.0078125 78.32 + vertex 34.19 4 71.12 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 7.54531 74.0949 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 7.59999 74.7199 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 7.38289 73.4887 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 7.54531 74.0949 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 7.11769 72.92 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 7.38289 73.4887 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 6.75776 72.406 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 7.11769 72.92 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 6.31403 71.9622 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 6.75776 72.406 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 5.8 71.6023 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 6.31403 71.9622 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 5.23127 71.3371 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 5.8 71.6023 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 4.62513 71.1747 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 5.23127 71.3371 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 34.19 4 71.12 + vertex 34.19 7.59999 6.86645e-07 + vertex 34.19 4.62513 71.1747 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 34.19 4 71.12 + vertex 34.19 0.0078125 78.32 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 34.19 4 71.12 + vertex 34.19 0.0078125 6.86645e-07 + vertex 34.19 7.59999 6.86645e-07 + endloop + endfacet + facet normal 0 0 -0 + outer loop + vertex -34.19 3.99994 78.32 + vertex -34.19 4 78.32 + vertex -34.19 0.0078125 78.32 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -34.19 7.6 6.86645e-07 + vertex -34.19 7.6 74.72 + vertex -34.19 7.6 74.7199 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.79 4 80.72 + vertex 31.79 0.0078125 80.72 + vertex 31.79 4 80.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 0.0078125 80.72 + vertex -31.79 4 80.72 + vertex -31.79 0.0078125 80.72 + endloop + endfacet + facet normal 0.99617 0 -0.0874337 + outer loop + vertex -32.1839 3.99996 78.3895 + vertex -32.19 4 78.32 + vertex -32.1839 4 78.3895 + endloop + endfacet + facet normal 0.99617 0 -0.0874337 + outer loop + vertex -32.19 4 78.32 + vertex -32.1839 3.99996 78.3895 + vertex -32.19 3.99996 78.32 + endloop + endfacet + facet normal -0 0 0 + outer loop + vertex -32.19 3.99996 78.32 + vertex -32.19 3.99994 78.32 + vertex -32.19 4 78.32 + endloop + endfacet + facet normal 0.966044 0 -0.258377 + outer loop + vertex -32.1659 3.99994 78.4568 + vertex -32.1839 4 78.3895 + vertex -32.1659 4 78.4568 + endloop + endfacet + facet normal 0.966044 0 -0.258377 + outer loop + vertex -32.1839 4 78.3895 + vertex -32.1659 3.99994 78.4568 + vertex -32.1839 3.99999 78.3895 + endloop + endfacet + facet normal -0 0 0 + outer loop + vertex -32.1839 3.99999 78.3895 + vertex -32.1839 3.99996 78.3895 + vertex -32.1839 4 78.3895 + endloop + endfacet + facet normal 0.906147 0 -0.422964 + outer loop + vertex -32.1659 3.99996 78.4568 + vertex -32.1659 4 78.4568 + vertex -32.1364 4 78.52 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -32.1659 4 78.4568 + vertex -32.1659 3.99996 78.4568 + vertex -32.1659 3.99994 78.4568 + endloop + endfacet + facet normal 0.818885 0 -0.573957 + outer loop + vertex -32.0964 3.99999 78.5771 + vertex -32.1353 4 78.5216 + vertex -32.0964 4 78.5771 + endloop + endfacet + facet normal 0.707107 0 -0.707107 + outer loop + vertex -32.0471 3.99999 78.6264 + vertex -32.0964 4 78.5771 + vertex -32.0471 4 78.6264 + endloop + endfacet + facet normal 0.707107 0 -0.707107 + outer loop + vertex -32.0964 3.99999 78.5771 + vertex -32.0964 4 78.5771 + vertex -32.0471 3.99999 78.6264 + endloop + endfacet + facet normal 0.423104 0 -0.906081 + outer loop + vertex -31.9268 3.99999 78.6959 + vertex -31.9722 4 78.6747 + vertex -31.9268 4 78.6959 + endloop + endfacet + facet normal 0.257955 0 -0.966157 + outer loop + vertex -31.9268 3.99999 78.6959 + vertex -31.9268 4 78.6959 + vertex -31.8856 4 78.7069 + endloop + endfacet + facet normal 0.996141 -0.00765652 -0.0874311 + outer loop + vertex -32.1839 4 78.3895 + vertex -32.19 4 78.32 + vertex -32.1839 4.63719 78.3337 + endloop + endfacet + facet normal 0.996136 -0.00765527 -0.087487 + outer loop + vertex -32.1839 4.63719 78.3337 + vertex -32.19 4 78.32 + vertex -32.19 4.62513 78.2653 + endloop + endfacet + facet normal 0.965797 -0.022611 -0.258311 + outer loop + vertex -32.1659 4 78.4568 + vertex -32.1839 4 78.3895 + vertex -32.1659 4.64889 78.4 + endloop + endfacet + facet normal 0.965821 -0.0226131 -0.258223 + outer loop + vertex -32.1659 4.64889 78.4 + vertex -32.1839 4 78.3895 + vertex -32.1839 4.63719 78.3337 + endloop + endfacet + facet normal 0.905527 -0.0369599 -0.422675 + outer loop + vertex -32.1364 4 78.52 + vertex -32.1659 4 78.4568 + vertex -32.1364 4.65986 78.4623 + endloop + endfacet + facet normal 0.905681 -0.0369695 -0.422344 + outer loop + vertex -32.1364 4.65986 78.4623 + vertex -32.1659 4 78.4568 + vertex -32.1659 4.64889 78.4 + endloop + endfacet + facet normal 0.817855 -0.0501531 -0.573235 + outer loop + vertex -32.1353 4 78.5216 + vertex -32.0964 4.66978 78.5185 + vertex -32.0964 4 78.5771 + endloop + endfacet + facet normal 0.822994 -0.0504173 -0.565808 + outer loop + vertex -32.0964 4.66978 78.5185 + vertex -32.1353 4 78.5216 + vertex -32.1364 4 78.52 + endloop + endfacet + facet normal 0.817846 -0.0501265 -0.573249 + outer loop + vertex -32.0964 4.66978 78.5185 + vertex -32.1364 4 78.52 + vertex -32.1364 4.65986 78.4623 + endloop + endfacet + facet normal 0.70576 -0.061697 -0.70576 + outer loop + vertex -32.0471 4 78.6264 + vertex -32.0964 4 78.5771 + vertex -32.0471 4.67834 78.5671 + endloop + endfacet + facet normal 0.706109 -0.0617172 -0.705409 + outer loop + vertex -32.0471 4.67834 78.5671 + vertex -32.0964 4 78.5771 + vertex -32.0964 4.66978 78.5185 + endloop + endfacet + facet normal 0.572629 -0.0713864 -0.816701 + outer loop + vertex -32.0161 4 78.6481 + vertex -31.99 4.68529 78.6065 + vertex -31.99 4 78.6664 + endloop + endfacet + facet normal 0.571999 -0.0713892 -0.817142 + outer loop + vertex -31.99 4.68529 78.6065 + vertex -32.0161 4 78.6481 + vertex -32.0471 4 78.6264 + endloop + endfacet + facet normal 0.572359 -0.0714118 -0.816888 + outer loop + vertex -31.99 4.68529 78.6065 + vertex -32.0471 4 78.6264 + vertex -32.0471 4.67834 78.5671 + endloop + endfacet + facet normal 0.421781 -0.0790211 -0.903248 + outer loop + vertex -31.9722 4 78.6747 + vertex -31.9268 4.6904 78.6355 + vertex -31.9268 4 78.6959 + endloop + endfacet + facet normal 0.421286 -0.0790018 -0.90348 + outer loop + vertex -31.9268 4.6904 78.6355 + vertex -31.9722 4 78.6747 + vertex -31.99 4 78.6664 + endloop + endfacet + facet normal 0.421016 -0.0789828 -0.903608 + outer loop + vertex -31.99 4 78.6664 + vertex -31.99 4.68529 78.6065 + vertex -31.9268 4.6904 78.6355 + endloop + endfacet + facet normal 0.258124 -0.084234 -0.962433 + outer loop + vertex -31.8856 4 78.7069 + vertex -31.8595 4.69354 78.6532 + vertex -31.8595 4 78.7139 + endloop + endfacet + facet normal 0.257038 -0.0842158 -0.962725 + outer loop + vertex -31.8595 4.69354 78.6532 + vertex -31.8856 4 78.7069 + vertex -31.9268 4 78.6959 + endloop + endfacet + facet normal 0.257122 -0.0842225 -0.962702 + outer loop + vertex -31.8595 4.69354 78.6532 + vertex -31.9268 4 78.6959 + vertex -31.9268 4.6904 78.6355 + endloop + endfacet + facet normal 0.0854511 -0.0868813 -0.992547 + outer loop + vertex -31.8202 4 78.7174 + vertex -31.79 4.69459 78.6592 + vertex -31.79 4 78.72 + endloop + endfacet + facet normal 0.0883712 -0.086986 -0.992282 + outer loop + vertex -31.79 4.69459 78.6592 + vertex -31.8202 4 78.7174 + vertex -31.8595 4 78.7139 + endloop + endfacet + facet normal 0.0869884 -0.0868582 -0.992416 + outer loop + vertex -31.8595 4 78.7139 + vertex -31.8595 4.69354 78.6532 + vertex -31.79 4.69459 78.6592 + endloop + endfacet + facet normal 0.996136 -0.0227234 -0.0848302 + outer loop + vertex -32.1839 4.63719 78.3337 + vertex -32.19 4.62513 78.2653 + vertex -32.1839 5.25503 78.1682 + endloop + endfacet + facet normal 0.99614 -0.022717 -0.0847886 + outer loop + vertex -32.1839 5.25503 78.1682 + vertex -32.19 4.62513 78.2653 + vertex -32.19 5.23127 78.1029 + endloop + endfacet + facet normal 0.965822 -0.0670553 -0.250381 + outer loop + vertex -32.1659 4.64889 78.4 + vertex -32.1839 4.63719 78.3337 + vertex -32.1659 5.27806 78.2315 + endloop + endfacet + facet normal 0.965855 -0.0670371 -0.250261 + outer loop + vertex -32.1659 5.27806 78.2315 + vertex -32.1839 4.63719 78.3337 + vertex -32.1839 5.25503 78.1682 + endloop + endfacet + facet normal 0.905673 -0.10977 -0.409521 + outer loop + vertex -32.1364 4.65986 78.4623 + vertex -32.1659 4.64889 78.4 + vertex -32.1364 5.29968 78.2908 + endloop + endfacet + facet normal 0.905306 -0.109884 -0.410301 + outer loop + vertex -32.1364 5.29968 78.2908 + vertex -32.1659 4.64889 78.4 + vertex -32.1659 5.27806 78.2315 + endloop + endfacet + facet normal 0.817857 -0.148919 -0.555818 + outer loop + vertex -32.0964 4.66978 78.5185 + vertex -32.1364 4.65986 78.4623 + vertex -32.0964 5.31921 78.3445 + endloop + endfacet + facet normal 0.818203 -0.148852 -0.555326 + outer loop + vertex -32.0964 5.31921 78.3445 + vertex -32.1364 4.65986 78.4623 + vertex -32.1364 5.29968 78.2908 + endloop + endfacet + facet normal 0.706094 -0.183334 -0.683973 + outer loop + vertex -32.0471 4.67834 78.5671 + vertex -32.0964 4.66978 78.5185 + vertex -32.0471 5.33607 78.3908 + endloop + endfacet + facet normal 0.705567 -0.183396 -0.684501 + outer loop + vertex -32.0471 5.33607 78.3908 + vertex -32.0964 4.66978 78.5185 + vertex -32.0964 5.31921 78.3445 + endloop + endfacet + facet normal 0.572363 -0.212297 -0.792042 + outer loop + vertex -31.99 4.68529 78.6065 + vertex -32.0471 4.67834 78.5671 + vertex -31.99 5.34975 78.4284 + endloop + endfacet + facet normal 0.572399 -0.212295 -0.792016 + outer loop + vertex -31.99 5.34975 78.4284 + vertex -32.0471 4.67834 78.5671 + vertex -32.0471 5.33607 78.3908 + endloop + endfacet + facet normal 0.42101 -0.234796 -0.87614 + outer loop + vertex -31.9268 4.6904 78.6355 + vertex -31.99 4.68529 78.6065 + vertex -31.9268 5.35983 78.4561 + endloop + endfacet + facet normal 0.421376 -0.234791 -0.875966 + outer loop + vertex -31.9268 5.35983 78.4561 + vertex -31.99 4.68529 78.6065 + vertex -31.99 5.34975 78.4284 + endloop + endfacet + facet normal 0.257168 -0.250004 -0.933468 + outer loop + vertex -31.8595 4.69354 78.6532 + vertex -31.9268 4.6904 78.6355 + vertex -31.8595 5.366 78.4731 + endloop + endfacet + facet normal 0.258615 -0.250049 -0.933056 + outer loop + vertex -31.8595 5.366 78.4731 + vertex -31.9268 4.6904 78.6355 + vertex -31.9268 5.35983 78.4561 + endloop + endfacet + facet normal 0.0869694 -0.257757 -0.962288 + outer loop + vertex -31.79 4.69459 78.6592 + vertex -31.8595 4.69354 78.6532 + vertex -31.79 5.36808 78.4788 + endloop + endfacet + facet normal 0.0866379 -0.257732 -0.962324 + outer loop + vertex -31.79 5.36808 78.4788 + vertex -31.8595 4.69354 78.6532 + vertex -31.8595 5.366 78.4731 + endloop + endfacet + facet normal 0.99614 -0.0371072 -0.0795526 + outer loop + vertex -32.1839 5.25503 78.1682 + vertex -32.19 5.23127 78.1029 + vertex -32.1839 5.83473 77.8978 + endloop + endfacet + facet normal 0.996131 -0.0371381 -0.0796438 + outer loop + vertex -32.1839 5.83473 77.8978 + vertex -32.19 5.23127 78.1029 + vertex -32.19 5.8 77.8377 + endloop + endfacet + facet normal 0.965854 -0.109502 -0.234811 + outer loop + vertex -32.1659 5.27806 78.2315 + vertex -32.1839 5.25503 78.1682 + vertex -32.1659 5.8684 77.9562 + endloop + endfacet + facet normal 0.965908 -0.109437 -0.234617 + outer loop + vertex -32.1659 5.8684 77.9562 + vertex -32.1839 5.25503 78.1682 + vertex -32.1839 5.83473 77.8978 + endloop + endfacet + facet normal 0.905324 -0.179477 -0.384937 + outer loop + vertex -32.1364 5.29968 78.2908 + vertex -32.1659 5.27806 78.2315 + vertex -32.1364 5.9 78.0109 + endloop + endfacet + facet normal 0.90545 -0.179392 -0.38468 + outer loop + vertex -32.1364 5.9 78.0109 + vertex -32.1659 5.27806 78.2315 + vertex -32.1659 5.8684 77.9562 + endloop + endfacet + facet normal 0.81819 -0.242952 -0.521094 + outer loop + vertex -32.0964 5.31921 78.3445 + vertex -32.1364 5.29968 78.2908 + vertex -32.0964 5.92856 78.0604 + endloop + endfacet + facet normal 0.818234 -0.242933 -0.521034 + outer loop + vertex -32.0964 5.92856 78.0604 + vertex -32.1364 5.29968 78.2908 + vertex -32.1364 5.9 78.0109 + endloop + endfacet + facet normal 0.70558 -0.299414 -0.642268 + outer loop + vertex -32.0471 5.33607 78.3908 + vertex -32.0964 5.31921 78.3445 + vertex -32.0471 5.95321 78.1031 + endloop + endfacet + facet normal 0.705788 -0.299354 -0.642067 + outer loop + vertex -32.0471 5.95321 78.1031 + vertex -32.0964 5.31921 78.3445 + vertex -32.0964 5.92856 78.0604 + endloop + endfacet + facet normal 0.572389 -0.34652 -0.743165 + outer loop + vertex -31.99 5.34975 78.4284 + vertex -32.0471 5.33607 78.3908 + vertex -31.99 5.9732 78.1377 + endloop + endfacet + facet normal 0.57188 -0.346613 -0.743513 + outer loop + vertex -31.99 5.9732 78.1377 + vertex -32.0471 5.33607 78.3908 + vertex -32.0471 5.95321 78.1031 + endloop + endfacet + facet normal 0.421369 -0.383276 -0.821917 + outer loop + vertex -31.9268 5.35983 78.4561 + vertex -31.99 5.34975 78.4284 + vertex -31.9268 5.98794 78.1632 + endloop + endfacet + facet normal 0.42108 -0.383304 -0.822052 + outer loop + vertex -31.9268 5.98794 78.1632 + vertex -31.99 5.34975 78.4284 + vertex -31.99 5.9732 78.1377 + endloop + endfacet + facet normal 0.258573 -0.408335 -0.875444 + outer loop + vertex -31.8595 5.366 78.4731 + vertex -31.9268 5.35983 78.4561 + vertex -31.8595 5.99696 78.1788 + endloop + endfacet + facet normal 0.257713 -0.408351 -0.87569 + outer loop + vertex -31.8595 5.99696 78.1788 + vertex -31.9268 5.35983 78.4561 + vertex -31.9268 5.98794 78.1632 + endloop + endfacet + facet normal 0.086651 -0.421065 -0.902882 + outer loop + vertex -31.79 5.36808 78.4788 + vertex -31.8595 5.366 78.4731 + vertex -31.79 6 78.1841 + endloop + endfacet + facet normal 0.0872664 -0.421098 -0.902807 + outer loop + vertex -31.79 6 78.1841 + vertex -31.8595 5.366 78.4731 + vertex -31.8595 5.99696 78.1788 + endloop + endfacet + facet normal 0.996132 -0.0503936 -0.071984 + outer loop + vertex -32.1839 5.83473 77.8978 + vertex -32.19 5.8 77.8377 + vertex -32.1839 6.35868 77.531 + endloop + endfacet + facet normal 0.996136 -0.0503711 -0.0719429 + outer loop + vertex -32.1839 6.35868 77.531 + vertex -32.19 5.8 77.8377 + vertex -32.19 6.31403 77.4778 + endloop + endfacet + facet normal 0.965901 -0.148504 -0.212091 + outer loop + vertex -32.1659 5.8684 77.9562 + vertex -32.1839 5.83473 77.8978 + vertex -32.1659 6.40197 77.5826 + endloop + endfacet + facet normal 0.96585 -0.148596 -0.212259 + outer loop + vertex -32.1659 6.40197 77.5826 + vertex -32.1839 5.83473 77.8978 + vertex -32.1839 6.35868 77.531 + endloop + endfacet + facet normal 0.905458 -0.243436 -0.347686 + outer loop + vertex -32.1364 5.9 78.0109 + vertex -32.1659 5.8684 77.9562 + vertex -32.1364 6.44259 77.631 + endloop + endfacet + facet normal 0.905491 -0.243402 -0.347623 + outer loop + vertex -32.1364 6.44259 77.631 + vertex -32.1659 5.8684 77.9562 + vertex -32.1659 6.40197 77.5826 + endloop + endfacet + facet normal 0.81822 -0.329787 -0.470911 + outer loop + vertex -32.0964 5.92856 78.0604 + vertex -32.1364 5.9 78.0109 + vertex -32.0964 6.47931 77.6747 + endloop + endfacet + facet normal 0.817885 -0.330011 -0.471336 + outer loop + vertex -32.0964 6.47931 77.6747 + vertex -32.1364 5.9 78.0109 + vertex -32.1364 6.44259 77.631 + endloop + endfacet + facet normal 0.705786 -0.406358 -0.580293 + outer loop + vertex -32.0471 5.95321 78.1031 + vertex -32.0964 5.92856 78.0604 + vertex -32.0471 6.511 77.7125 + endloop + endfacet + facet normal 0.705963 -0.406278 -0.580133 + outer loop + vertex -32.0471 6.511 77.7125 + vertex -32.0964 5.92856 78.0604 + vertex -32.0964 6.47931 77.6747 + endloop + endfacet + facet normal 0.571902 -0.470544 -0.67195 + outer loop + vertex -31.99 5.9732 78.1377 + vertex -32.0471 5.95321 78.1031 + vertex -31.99 6.5367 77.7431 + endloop + endfacet + facet normal 0.571891 -0.470547 -0.671957 + outer loop + vertex -31.99 6.5367 77.7431 + vertex -32.0471 5.95321 78.1031 + vertex -32.0471 6.511 77.7125 + endloop + endfacet + facet normal 0.421112 -0.520232 -0.742983 + outer loop + vertex -31.9268 5.98794 78.1632 + vertex -31.99 5.9732 78.1377 + vertex -31.9268 6.55564 77.7657 + endloop + endfacet + facet normal 0.421508 -0.520162 -0.742807 + outer loop + vertex -31.9268 6.55564 77.7657 + vertex -31.99 5.9732 78.1377 + vertex -31.99 6.5367 77.7431 + endloop + endfacet + facet normal 0.25774 -0.554184 -0.791486 + outer loop + vertex -31.8595 5.99696 78.1788 + vertex -31.9268 5.98794 78.1632 + vertex -31.8595 6.56724 77.7795 + endloop + endfacet + facet normal 0.257811 -0.55418 -0.791466 + outer loop + vertex -31.8595 6.56724 77.7795 + vertex -31.9268 5.98794 78.1632 + vertex -31.9268 6.55564 77.7657 + endloop + endfacet + facet normal 0.087223 -0.571368 -0.816046 + outer loop + vertex -31.79 6 78.1841 + vertex -31.8595 5.99696 78.1788 + vertex -31.79 6.57115 77.7842 + endloop + endfacet + facet normal 0.0873297 -0.571371 -0.816032 + outer loop + vertex -31.79 6.57115 77.7842 + vertex -31.8595 5.99696 78.1788 + vertex -31.8595 6.56724 77.7795 + endloop + endfacet + facet normal 0.996141 -0.0620623 -0.0620609 + outer loop + vertex -32.1839 6.35868 77.531 + vertex -32.19 6.75776 77.034 + vertex -32.1839 6.81097 77.0787 + endloop + endfacet + facet normal 0.996136 -0.0621048 -0.062095 + outer loop + vertex -32.19 6.75776 77.034 + vertex -32.1839 6.35868 77.531 + vertex -32.19 6.31403 77.4778 + endloop + endfacet + facet normal 0.965849 -0.183217 -0.183213 + outer loop + vertex -32.1659 6.40197 77.5826 + vertex -32.1839 6.81097 77.0787 + vertex -32.1659 6.86256 77.122 + endloop + endfacet + facet normal 0.965849 -0.183217 -0.183213 + outer loop + vertex -32.1839 6.81097 77.0787 + vertex -32.1659 6.40197 77.5826 + vertex -32.1839 6.35868 77.531 + endloop + endfacet + facet normal 0.905475 -0.300102 -0.300089 + outer loop + vertex -32.1364 6.44259 77.631 + vertex -32.1659 6.86256 77.122 + vertex -32.1364 6.91097 77.1626 + endloop + endfacet + facet normal 0.905493 -0.300072 -0.300065 + outer loop + vertex -32.1659 6.86256 77.122 + vertex -32.1364 6.44259 77.631 + vertex -32.1659 6.40197 77.5826 + endloop + endfacet + facet normal 0.818019 -0.406718 -0.406726 + outer loop + vertex -32.0964 6.47931 77.6747 + vertex -32.1364 6.91097 77.1626 + vertex -32.0964 6.95472 77.1993 + endloop + endfacet + facet normal 0.817917 -0.406832 -0.406815 + outer loop + vertex -32.1364 6.91097 77.1626 + vertex -32.0964 6.47931 77.6747 + vertex -32.1364 6.44259 77.631 + endloop + endfacet + facet normal 0.705832 -0.500905 -0.500894 + outer loop + vertex -32.0471 6.511 77.7125 + vertex -32.0964 6.95472 77.1993 + vertex -32.0471 6.99249 77.231 + endloop + endfacet + facet normal 0.705934 -0.500823 -0.500833 + outer loop + vertex -32.0964 6.95472 77.1993 + vertex -32.0471 6.511 77.7125 + vertex -32.0964 6.47931 77.6747 + endloop + endfacet + facet normal 0.571919 -0.580053 -0.580041 + outer loop + vertex -32.0471 6.99249 77.231 + vertex -31.99 6.5367 77.7431 + vertex -32.0471 6.511 77.7125 + endloop + endfacet + facet normal 0.572124 -0.579934 -0.579958 + outer loop + vertex -31.99 6.5367 77.7431 + vertex -32.0471 6.99249 77.231 + vertex -31.99 7.02312 77.2567 + endloop + endfacet + facet normal 0.421468 -0.641261 -0.641209 + outer loop + vertex -31.99 6.5367 77.7431 + vertex -31.9268 7.0457 77.2756 + vertex -31.9268 6.55564 77.7657 + endloop + endfacet + facet normal 0.420968 -0.641386 -0.641412 + outer loop + vertex -31.9268 7.0457 77.2756 + vertex -31.99 6.5367 77.7431 + vertex -31.99 7.02312 77.2567 + endloop + endfacet + facet normal 0.257847 -0.683224 -0.683169 + outer loop + vertex -31.9268 7.0457 77.2756 + vertex -31.8595 6.56724 77.7795 + vertex -31.9268 6.55564 77.7657 + endloop + endfacet + facet normal 0.258038 -0.683174 -0.683147 + outer loop + vertex -31.8595 6.56724 77.7795 + vertex -31.9268 7.0457 77.2756 + vertex -31.8595 7.05952 77.2872 + endloop + endfacet + facet normal 0.0872655 -0.704424 -0.704395 + outer loop + vertex -31.8595 7.05952 77.2872 + vertex -31.79 6.57115 77.7842 + vertex -31.8595 6.56724 77.7795 + endloop + endfacet + facet normal 0.0877683 -0.704357 -0.704399 + outer loop + vertex -31.79 6.57115 77.7842 + vertex -31.8595 7.05952 77.2872 + vertex -31.79 7.06418 77.2912 + endloop + endfacet + facet normal 0.99614 -0.0719056 -0.0503435 + outer loop + vertex -32.1839 6.81097 77.0787 + vertex -32.19 6.75776 77.034 + vertex -32.1839 7.17784 76.5547 + endloop + endfacet + facet normal 0.996134 -0.0719539 -0.0503859 + outer loop + vertex -32.1839 7.17784 76.5547 + vertex -32.19 6.75776 77.034 + vertex -32.19 7.11769 76.52 + endloop + endfacet + facet normal 0.965848 -0.212255 -0.148614 + outer loop + vertex -32.1659 6.86256 77.122 + vertex -32.1839 6.81097 77.0787 + vertex -32.1659 7.23617 76.5884 + endloop + endfacet + facet normal 0.965862 -0.212214 -0.148578 + outer loop + vertex -32.1659 7.23617 76.5884 + vertex -32.1839 6.81097 77.0787 + vertex -32.1839 7.17784 76.5547 + endloop + endfacet + facet normal 0.905482 -0.347636 -0.243416 + outer loop + vertex -32.1364 6.91097 77.1626 + vertex -32.1659 6.86256 77.122 + vertex -32.1364 7.2909 76.62 + endloop + endfacet + facet normal 0.905521 -0.347573 -0.24336 + outer loop + vertex -32.1364 7.2909 76.62 + vertex -32.1659 6.86256 77.122 + vertex -32.1659 7.23617 76.5884 + endloop + endfacet + facet normal 0.818027 -0.471146 -0.32993 + outer loop + vertex -32.0964 6.95472 77.1993 + vertex -32.1364 6.91097 77.1626 + vertex -32.0964 7.34036 76.6486 + endloop + endfacet + facet normal 0.818175 -0.470988 -0.329787 + outer loop + vertex -32.0964 7.34036 76.6486 + vertex -32.1364 6.91097 77.1626 + vertex -32.1364 7.2909 76.62 + endloop + endfacet + facet normal 0.705825 -0.580279 -0.40631 + outer loop + vertex -32.0471 6.99249 77.231 + vertex -32.0964 6.95472 77.1993 + vertex -32.0471 7.38306 76.6732 + endloop + endfacet + facet normal 0.705579 -0.580459 -0.40648 + outer loop + vertex -32.0471 7.38306 76.6732 + vertex -32.0964 6.95472 77.1993 + vertex -32.0964 7.34036 76.6486 + endloop + endfacet + facet normal 0.572127 -0.671838 -0.47043 + outer loop + vertex -31.99 7.02312 77.2567 + vertex -32.0471 6.99249 77.231 + vertex -31.99 7.41769 76.6932 + endloop + endfacet + facet normal 0.572197 -0.671803 -0.470395 + outer loop + vertex -31.99 7.41769 76.6932 + vertex -32.0471 6.99249 77.231 + vertex -32.0471 7.38306 76.6732 + endloop + endfacet + facet normal 0.421043 -0.743003 -0.520259 + outer loop + vertex -31.9268 7.0457 77.2756 + vertex -31.99 7.02312 77.2567 + vertex -31.9268 7.44321 76.7079 + endloop + endfacet + facet normal 0.421034 -0.743006 -0.520262 + outer loop + vertex -31.9268 7.44321 76.7079 + vertex -31.99 7.02312 77.2567 + vertex -31.99 7.41769 76.6932 + endloop + endfacet + facet normal 0.258033 -0.791372 -0.55421 + outer loop + vertex -31.8595 7.05952 77.2872 + vertex -31.9268 7.0457 77.2756 + vertex -31.8595 7.45884 76.717 + endloop + endfacet + facet normal 0.258684 -0.791268 -0.554055 + outer loop + vertex -31.8595 7.45884 76.717 + vertex -31.9268 7.0457 77.2756 + vertex -31.9268 7.44321 76.7079 + endloop + endfacet + facet normal 0.0875976 -0.816029 -0.571335 + outer loop + vertex -31.79 7.06418 77.2912 + vertex -31.8595 7.05952 77.2872 + vertex -31.79 7.4641 76.72 + endloop + endfacet + facet normal 0.0864297 -0.816045 -0.571489 + outer loop + vertex -31.79 7.4641 76.72 + vertex -31.8595 7.05952 77.2872 + vertex -31.8595 7.45884 76.717 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -31.79 7.46412 76.72 + vertex -31.79 7.4641 76.72 + vertex -31.79 7.75877 76.0881 + endloop + endfacet + facet normal 0.996135 -0.0796063 -0.0371212 + outer loop + vertex -32.1839 7.17784 76.5547 + vertex -32.19 7.11769 76.52 + vertex -32.1839 7.44816 75.975 + endloop + endfacet + facet normal 0.996134 -0.0796156 -0.0371269 + outer loop + vertex -32.1839 7.44816 75.975 + vertex -32.19 7.11769 76.52 + vertex -32.19 7.38289 75.9513 + endloop + endfacet + facet normal 0.965859 -0.234794 -0.109494 + outer loop + vertex -32.1659 7.23617 76.5884 + vertex -32.1839 7.17784 76.5547 + vertex -32.1659 7.51145 75.9981 + endloop + endfacet + facet normal 0.965874 -0.234746 -0.109465 + outer loop + vertex -32.1659 7.51145 75.9981 + vertex -32.1839 7.17784 76.5547 + vertex -32.1839 7.44816 75.975 + endloop + endfacet + facet normal 0.90552 -0.384548 -0.179321 + outer loop + vertex -32.1364 7.2909 76.62 + vertex -32.1659 7.23617 76.5884 + vertex -32.1364 7.57083 76.0197 + endloop + endfacet + facet normal 0.905489 -0.384605 -0.179356 + outer loop + vertex -32.1364 7.57083 76.0197 + vertex -32.1659 7.23617 76.5884 + vertex -32.1659 7.51145 75.9981 + endloop + endfacet + facet normal 0.818142 -0.52115 -0.242993 + outer loop + vertex -32.0964 7.34036 76.6486 + vertex -32.1364 7.2909 76.62 + vertex -32.0964 7.6245 76.0392 + endloop + endfacet + facet normal 0.817999 -0.521324 -0.243102 + outer loop + vertex -32.0964 7.6245 76.0392 + vertex -32.1364 7.2909 76.62 + vertex -32.1364 7.57083 76.0197 + endloop + endfacet + facet normal 0.705638 -0.64218 -0.299466 + outer loop + vertex -32.0471 7.38306 76.6732 + vertex -32.0964 7.34036 76.6486 + vertex -32.0471 7.67083 76.0561 + endloop + endfacet + facet normal 0.705896 -0.641963 -0.299323 + outer loop + vertex -32.0471 7.67083 76.0561 + vertex -32.0964 7.34036 76.6486 + vertex -32.0964 7.6245 76.0392 + endloop + endfacet + facet normal 0.572186 -0.743295 -0.346577 + outer loop + vertex -31.99 7.41769 76.6932 + vertex -32.0471 7.38306 76.6732 + vertex -31.99 7.70841 76.0697 + endloop + endfacet + facet normal 0.571886 -0.743469 -0.346699 + outer loop + vertex -31.99 7.70841 76.0697 + vertex -32.0471 7.38306 76.6732 + vertex -32.0471 7.67083 76.0561 + endloop + endfacet + facet normal 0.421095 -0.822035 -0.383324 + outer loop + vertex -31.9268 7.44321 76.7079 + vertex -31.99 7.41769 76.6932 + vertex -31.9268 7.7361 76.0798 + endloop + endfacet + facet normal 0.421363 -0.821935 -0.383244 + outer loop + vertex -31.9268 7.7361 76.0798 + vertex -31.99 7.41769 76.6932 + vertex -31.99 7.70841 76.0697 + endloop + endfacet + facet normal 0.25853 -0.875507 -0.408228 + outer loop + vertex -31.8595 7.45884 76.717 + vertex -31.9268 7.44321 76.7079 + vertex -31.8595 7.75306 76.086 + endloop + endfacet + facet normal 0.25826 -0.875561 -0.408284 + outer loop + vertex -31.8595 7.75306 76.086 + vertex -31.9268 7.44321 76.7079 + vertex -31.9268 7.7361 76.0798 + endloop + endfacet + facet normal 0.0865096 -0.902904 -0.421046 + outer loop + vertex -31.79 7.4641 76.72 + vertex -31.8595 7.45884 76.717 + vertex -31.79 7.75877 76.0881 + endloop + endfacet + facet normal 0.0869006 -0.90289 -0.420996 + outer loop + vertex -31.79 7.75877 76.0881 + vertex -31.8595 7.45884 76.717 + vertex -31.8595 7.75306 76.086 + endloop + endfacet + facet normal 0.996135 -0.0848416 -0.0227347 + outer loop + vertex -32.1839 7.44816 75.975 + vertex -32.19 7.38289 75.9513 + vertex -32.1839 7.61371 75.3572 + endloop + endfacet + facet normal 0.996137 -0.0848167 -0.0227251 + outer loop + vertex -32.1839 7.61371 75.3572 + vertex -32.19 7.38289 75.9513 + vertex -32.19 7.54531 75.3451 + endloop + endfacet + facet normal 0.965863 -0.250225 -0.0670463 + outer loop + vertex -32.1659 7.51145 75.9981 + vertex -32.1839 7.44816 75.975 + vertex -32.1659 7.68004 75.3689 + endloop + endfacet + facet normal 0.96585 -0.250273 -0.067065 + outer loop + vertex -32.1659 7.68004 75.3689 + vertex -32.1839 7.44816 75.975 + vertex -32.1839 7.61371 75.3572 + endloop + endfacet + facet normal 0.905495 -0.409897 -0.109835 + outer loop + vertex -32.1364 7.57083 76.0197 + vertex -32.1659 7.51145 75.9981 + vertex -32.1364 7.74227 75.3799 + endloop + endfacet + facet normal 0.905519 -0.409848 -0.109816 + outer loop + vertex -32.1364 7.74227 75.3799 + vertex -32.1659 7.51145 75.9981 + vertex -32.1659 7.68004 75.3689 + endloop + endfacet + facet normal 0.818026 -0.555579 -0.148879 + outer loop + vertex -32.0964 7.6245 76.0392 + vertex -32.1364 7.57083 76.0197 + vertex -32.0964 7.79852 75.3898 + endloop + endfacet + facet normal 0.818061 -0.555533 -0.14886 + outer loop + vertex -32.0964 7.79852 75.3898 + vertex -32.1364 7.57083 76.0197 + vertex -32.1364 7.74227 75.3799 + endloop + endfacet + facet normal 0.70585 -0.684228 -0.183321 + outer loop + vertex -32.0471 7.67083 76.0561 + vertex -32.0964 7.6245 76.0392 + vertex -32.0471 7.84707 75.3983 + endloop + endfacet + facet normal 0.705636 -0.684427 -0.183406 + outer loop + vertex -32.0471 7.84707 75.3983 + vertex -32.0964 7.6245 76.0392 + vertex -32.0964 7.79852 75.3898 + endloop + endfacet + facet normal 0.57201 -0.792293 -0.212312 + outer loop + vertex -31.99 7.70841 76.0697 + vertex -32.0471 7.67083 76.0561 + vertex -31.99 7.88645 75.4053 + endloop + endfacet + facet normal 0.572305 -0.792104 -0.212223 + outer loop + vertex -31.99 7.88645 75.4053 + vertex -32.0471 7.67083 76.0561 + vertex -32.0471 7.84707 75.3983 + endloop + endfacet + facet normal 0.42132 -0.876008 -0.234732 + outer loop + vertex -31.9268 7.7361 76.0798 + vertex -31.99 7.70841 76.0697 + vertex -31.9268 7.91547 75.4104 + endloop + endfacet + facet normal 0.421209 -0.876055 -0.234757 + outer loop + vertex -31.9268 7.91547 75.4104 + vertex -31.99 7.70841 76.0697 + vertex -31.99 7.88645 75.4053 + endloop + endfacet + facet normal 0.2582 -0.933175 -0.250035 + outer loop + vertex -31.8595 7.75306 76.086 + vertex -31.9268 7.7361 76.0798 + vertex -31.8595 7.93325 75.4135 + endloop + endfacet + facet normal 0.258062 -0.933207 -0.250059 + outer loop + vertex -31.8595 7.93325 75.4135 + vertex -31.9268 7.7361 76.0798 + vertex -31.9268 7.91547 75.4104 + endloop + endfacet + facet normal 0.0868498 -0.962277 -0.257836 + outer loop + vertex -31.79 7.75877 76.0881 + vertex -31.8595 7.75306 76.086 + vertex -31.79 7.93923 75.4146 + endloop + endfacet + facet normal 0.0868781 -0.962276 -0.257833 + outer loop + vertex -31.79 7.93923 75.4146 + vertex -31.8595 7.75306 76.086 + vertex -31.8595 7.93325 75.4135 + endloop + endfacet + facet normal 0.996137 -0.0874827 -0.00765405 + outer loop + vertex -32.1839 7.61371 75.3572 + vertex -32.19 7.54531 75.3451 + vertex -32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0.996137 -0.0874811 -0.00765372 + outer loop + vertex -32.1839 7.66946 74.72 + vertex -32.19 7.54531 75.3451 + vertex -32.19 7.6 74.72 + endloop + endfacet + facet normal 0.965849 -0.25812 -0.022582 + outer loop + vertex -32.1659 7.68004 75.3689 + vertex -32.1839 7.61371 75.3572 + vertex -32.1659 7.73681 74.72 + endloop + endfacet + facet normal 0.965846 -0.258132 -0.0225846 + outer loop + vertex -32.1659 7.73681 74.72 + vertex -32.1839 7.61371 75.3572 + vertex -32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0.905507 -0.422717 -0.0369805 + outer loop + vertex -32.1364 7.74227 75.3799 + vertex -32.1659 7.68004 75.3689 + vertex -32.1364 7.8 74.72 + endloop + endfacet + facet normal 0.905501 -0.42273 -0.0369831 + outer loop + vertex -32.1364 7.8 74.72 + vertex -32.1659 7.68004 75.3689 + vertex -32.1659 7.73681 74.72 + endloop + endfacet + facet normal 0.818076 -0.572923 -0.0501158 + outer loop + vertex -32.0964 7.79852 75.3898 + vertex -32.1364 7.74227 75.3799 + vertex -32.0964 7.85711 74.72 + endloop + endfacet + facet normal 0.818047 -0.572963 -0.0501245 + outer loop + vertex -32.0964 7.85711 74.72 + vertex -32.1364 7.74227 75.3799 + vertex -32.1364 7.8 74.72 + endloop + endfacet + facet normal 0.705712 -0.705802 -0.0617564 + outer loop + vertex -32.0471 7.84707 75.3983 + vertex -32.0964 7.79852 75.3898 + vertex -32.0471 7.90642 74.72 + endloop + endfacet + facet normal 0.70583 -0.705687 -0.0617291 + outer loop + vertex -32.0471 7.90642 74.72 + vertex -32.0964 7.79852 75.3898 + vertex -32.0964 7.85711 74.72 + endloop + endfacet + facet normal 0.572211 -0.816985 -0.0714817 + outer loop + vertex -31.99 7.88645 75.4053 + vertex -32.0471 7.84707 75.3983 + vertex -31.99 7.94641 74.72 + endloop + endfacet + facet normal 0.572187 -0.817002 -0.0714861 + outer loop + vertex -31.99 7.94641 74.72 + vertex -32.0471 7.84707 75.3983 + vertex -32.0471 7.90642 74.72 + endloop + endfacet + facet normal 0.421244 -0.903495 -0.0790558 + outer loop + vertex -31.9268 7.91547 75.4104 + vertex -31.99 7.88645 75.4053 + vertex -31.9268 7.97588 74.72 + endloop + endfacet + facet normal 0.421288 -0.903475 -0.0790491 + outer loop + vertex -31.9268 7.97588 74.72 + vertex -31.99 7.88645 75.4053 + vertex -31.99 7.94641 74.72 + endloop + endfacet + facet normal 0.258143 -0.962431 -0.0841971 + outer loop + vertex -31.8595 7.93325 75.4135 + vertex -31.9268 7.91547 75.4104 + vertex -31.8595 7.99392 74.72 + endloop + endfacet + facet normal 0.257993 -0.962469 -0.0842161 + outer loop + vertex -31.8595 7.99392 74.72 + vertex -31.9268 7.91547 75.4104 + vertex -31.9268 7.97588 74.72 + endloop + endfacet + facet normal 0.0867667 -0.992438 -0.0868276 + outer loop + vertex -31.79 7.93923 75.4146 + vertex -31.8595 7.93325 75.4135 + vertex -31.79 8 74.72 + endloop + endfacet + facet normal 0.0868201 -0.992433 -0.0868218 + outer loop + vertex -31.79 8 74.72 + vertex -31.8595 7.93325 75.4135 + vertex -31.8595 7.99392 74.72 + endloop + endfacet + facet normal 0.996128 -0.0874803 0.00874803 + outer loop + vertex -32.19 7.6 74.72 + vertex -32.1839 7.66945 74.7199 + vertex -32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0.996165 -0.0874961 0 + outer loop + vertex -32.1839 7.66945 74.7199 + vertex -32.19 7.6 74.72 + vertex -32.19 7.6 74.7199 + endloop + endfacet + facet normal 0.96577 -0.258112 0.0258112 + outer loop + vertex -32.1839 7.66946 74.72 + vertex -32.1659 7.7368 74.7199 + vertex -32.1659 7.73681 74.72 + endloop + endfacet + facet normal -0.259973 0 -0.965616 + outer loop + vertex 31.9268 3.99999 78.6959 + vertex 31.8982 4 78.7036 + vertex 31.9268 4 78.6959 + endloop + endfacet + facet normal -0.422169 0 -0.906517 + outer loop + vertex 31.9545 4 78.683 + vertex 31.9268 3.99999 78.6959 + vertex 31.9268 4 78.6959 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex 32.1659 4 78.4568 + vertex 32.1659 3.99997 78.4568 + vertex 32.1659 3.99998 78.4568 + endloop + endfacet + facet normal -0.906562 0 -0.422072 + outer loop + vertex 32.1659 3.99998 78.4568 + vertex 32.1517 4 78.4873 + vertex 32.1659 4 78.4568 + endloop + endfacet + facet normal -0.96609 0 -0.258204 + outer loop + vertex 32.1748 4 78.4235 + vertex 32.1659 3.99997 78.4568 + vertex 32.1659 4 78.4568 + endloop + endfacet + facet normal -0.0867823 -0.0868598 -0.992433 + outer loop + vertex 31.8149 4 78.7178 + vertex 31.8595 4.69354 78.6532 + vertex 31.8595 4 78.7139 + endloop + endfacet + facet normal -0.0876784 -0.0867953 -0.99236 + outer loop + vertex 31.8595 4.69354 78.6532 + vertex 31.8149 4 78.7178 + vertex 31.79 4 78.72 + endloop + endfacet + facet normal -0.0869885 -0.0868697 -0.992415 + outer loop + vertex 31.8595 4.69354 78.6532 + vertex 31.79 4 78.72 + vertex 31.79 4.69459 78.6592 + endloop + endfacet + facet normal -0.259051 -0.0841776 -0.962189 + outer loop + vertex 31.8982 4 78.7036 + vertex 31.9268 4.6904 78.6355 + vertex 31.9268 4 78.6959 + endloop + endfacet + facet normal -0.256279 -0.0843639 -0.962914 + outer loop + vertex 31.9268 4.6904 78.6355 + vertex 31.8982 4 78.7036 + vertex 31.8595 4 78.7139 + endloop + endfacet + facet normal -0.257122 -0.0842573 -0.962699 + outer loop + vertex 31.8595 4 78.7139 + vertex 31.8595 4.69354 78.6532 + vertex 31.9268 4.6904 78.6355 + endloop + endfacet + facet normal -0.422262 -0.0789323 -0.903031 + outer loop + vertex 31.9545 4 78.683 + vertex 31.99 4.68529 78.6065 + vertex 31.99 4 78.6664 + endloop + endfacet + facet normal -0.420847 -0.079078 -0.903678 + outer loop + vertex 31.99 4.68529 78.6065 + vertex 31.9545 4 78.683 + vertex 31.9268 4 78.6959 + endloop + endfacet + facet normal -0.421019 -0.079052 -0.903601 + outer loop + vertex 31.99 4.68529 78.6065 + vertex 31.9268 4 78.6959 + vertex 31.9268 4.6904 78.6355 + endloop + endfacet + facet normal -0.572286 -0.0714162 -0.816938 + outer loop + vertex 32.0471 4 78.6264 + vertex 31.99 4 78.6664 + vertex 32.0471 4.67834 78.5671 + endloop + endfacet + facet normal -0.572358 -0.0714028 -0.816889 + outer loop + vertex 31.99 4 78.6664 + vertex 31.99 4.68529 78.6065 + vertex 32.0471 4.67834 78.5671 + endloop + endfacet + facet normal -0.705757 -0.0617477 -0.705757 + outer loop + vertex 32.0964 4 78.5771 + vertex 32.0471 4 78.6264 + vertex 32.0964 4.66978 78.5185 + endloop + endfacet + facet normal -0.706107 -0.0616669 -0.705415 + outer loop + vertex 32.0471 4 78.6264 + vertex 32.0471 4.67834 78.5671 + vertex 32.0964 4.66978 78.5185 + endloop + endfacet + facet normal -0.818001 -0.0501074 -0.57303 + outer loop + vertex 32.1364 4 78.52 + vertex 32.0964 4 78.5771 + vertex 32.1364 4.65986 78.4623 + endloop + endfacet + facet normal -0.817848 -0.050154 -0.573245 + outer loop + vertex 32.0964 4 78.5771 + vertex 32.0964 4.66978 78.5185 + vertex 32.1364 4.65986 78.4623 + endloop + endfacet + facet normal -0.905944 -0.0369205 -0.421784 + outer loop + vertex 32.1517 4 78.4873 + vertex 32.1659 4.64889 78.4 + vertex 32.1659 4 78.4568 + endloop + endfacet + facet normal -0.905132 -0.0371694 -0.423502 + outer loop + vertex 32.1659 4.64889 78.4 + vertex 32.1517 4 78.4873 + vertex 32.1364 4 78.52 + endloop + endfacet + facet normal -0.90568 -0.0369315 -0.42235 + outer loop + vertex 32.1364 4 78.52 + vertex 32.1364 4.65986 78.4623 + vertex 32.1659 4.64889 78.4 + endloop + endfacet + facet normal -0.965751 -0.0226357 -0.25848 + outer loop + vertex 32.1748 4 78.4235 + vertex 32.1839 4.63719 78.3337 + vertex 32.1839 4 78.3895 + endloop + endfacet + facet normal -0.965844 -0.0225861 -0.258138 + outer loop + vertex 32.1839 4.63719 78.3337 + vertex 32.1748 4 78.4235 + vertex 32.1659 4 78.4568 + endloop + endfacet + facet normal -0.96582 -0.0226035 -0.258225 + outer loop + vertex 32.1839 4.63719 78.3337 + vertex 32.1659 4 78.4568 + vertex 32.1659 4.64889 78.4 + endloop + endfacet + facet normal -0.996141 -0.00765038 -0.0874311 + outer loop + vertex 32.19 4 78.32 + vertex 32.1839 4 78.3895 + vertex 32.19 4.62513 78.2653 + endloop + endfacet + facet normal -0.996136 -0.00766132 -0.0874859 + outer loop + vertex 32.1839 4 78.3895 + vertex 32.1839 4.63719 78.3337 + vertex 32.19 4.62513 78.2653 + endloop + endfacet + facet normal -0.0869696 -0.257725 -0.962296 + outer loop + vertex 31.8595 4.69354 78.6532 + vertex 31.79 4.69459 78.6592 + vertex 31.8595 5.366 78.4731 + endloop + endfacet + facet normal -0.0866381 -0.257764 -0.962316 + outer loop + vertex 31.8595 5.366 78.4731 + vertex 31.79 4.69459 78.6592 + vertex 31.79 5.36808 78.4788 + endloop + endfacet + facet normal -0.257165 -0.250149 -0.93343 + outer loop + vertex 31.9268 4.6904 78.6355 + vertex 31.8595 4.69354 78.6532 + vertex 31.9268 5.35983 78.4561 + endloop + endfacet + facet normal -0.258611 -0.249904 -0.933096 + outer loop + vertex 31.9268 5.35983 78.4561 + vertex 31.8595 4.69354 78.6532 + vertex 31.8595 5.366 78.4731 + endloop + endfacet + facet normal -0.421009 -0.234835 -0.87613 + outer loop + vertex 31.99 4.68529 78.6065 + vertex 31.9268 4.6904 78.6355 + vertex 31.99 5.34975 78.4284 + endloop + endfacet + facet normal -0.421374 -0.234752 -0.875977 + outer loop + vertex 31.99 5.34975 78.4284 + vertex 31.9268 4.6904 78.6355 + vertex 31.9268 5.35983 78.4561 + endloop + endfacet + facet normal -0.572363 -0.212301 -0.792041 + outer loop + vertex 32.0471 4.67834 78.5671 + vertex 31.99 4.68529 78.6065 + vertex 32.0471 5.33607 78.3908 + endloop + endfacet + facet normal -0.572399 -0.21229 -0.792018 + outer loop + vertex 32.0471 5.33607 78.3908 + vertex 31.99 4.68529 78.6065 + vertex 31.99 5.34975 78.4284 + endloop + endfacet + facet normal -0.706098 -0.18326 -0.68399 + outer loop + vertex 32.0964 4.66978 78.5185 + vertex 32.0471 4.67834 78.5671 + vertex 32.0964 5.31921 78.3445 + endloop + endfacet + facet normal -0.70557 -0.18347 -0.684478 + outer loop + vertex 32.0964 5.31921 78.3445 + vertex 32.0471 4.67834 78.5671 + vertex 32.0471 5.33607 78.3908 + endloop + endfacet + facet normal -0.817854 -0.14898 -0.555806 + outer loop + vertex 32.1364 4.65986 78.4623 + vertex 32.0964 4.66978 78.5185 + vertex 32.1364 5.29968 78.2908 + endloop + endfacet + facet normal -0.8182 -0.148792 -0.555346 + outer loop + vertex 32.1364 5.29968 78.2908 + vertex 32.0964 4.66978 78.5185 + vertex 32.0964 5.31921 78.3445 + endloop + endfacet + facet normal -0.905676 -0.10968 -0.409538 + outer loop + vertex 32.1659 4.64889 78.4 + vertex 32.1364 4.65986 78.4623 + vertex 32.1659 5.27806 78.2315 + endloop + endfacet + facet normal -0.905309 -0.109971 -0.410271 + outer loop + vertex 32.1659 5.27806 78.2315 + vertex 32.1364 4.65986 78.4623 + vertex 32.1364 5.29968 78.2908 + endloop + endfacet + facet normal -0.965822 -0.0670686 -0.250379 + outer loop + vertex 32.1839 4.63719 78.3337 + vertex 32.1659 4.64889 78.4 + vertex 32.1839 5.25503 78.1682 + endloop + endfacet + facet normal -0.965854 -0.0670244 -0.250266 + outer loop + vertex 32.1839 5.25503 78.1682 + vertex 32.1659 4.64889 78.4 + vertex 32.1659 5.27806 78.2315 + endloop + endfacet + facet normal -0.996136 -0.0227279 -0.0848294 + outer loop + vertex 32.19 4.62513 78.2653 + vertex 32.1839 4.63719 78.3337 + vertex 32.19 5.23127 78.1029 + endloop + endfacet + facet normal -0.99614 -0.0227126 -0.0847902 + outer loop + vertex 32.19 5.23127 78.1029 + vertex 32.1839 4.63719 78.3337 + vertex 32.1839 5.25503 78.1682 + endloop + endfacet + facet normal -0.0866505 -0.421121 -0.902856 + outer loop + vertex 31.8595 5.366 78.4731 + vertex 31.79 5.36808 78.4788 + vertex 31.8595 5.99696 78.1788 + endloop + endfacet + facet normal -0.087266 -0.421042 -0.902833 + outer loop + vertex 31.8595 5.99696 78.1788 + vertex 31.79 5.36808 78.4788 + vertex 31.79 6 78.1841 + endloop + endfacet + facet normal -0.258575 -0.408254 -0.875481 + outer loop + vertex 31.9268 5.35983 78.4561 + vertex 31.8595 5.366 78.4731 + vertex 31.9268 5.98794 78.1632 + endloop + endfacet + facet normal -0.257715 -0.408432 -0.875652 + outer loop + vertex 31.9268 5.98794 78.1632 + vertex 31.8595 5.366 78.4731 + vertex 31.8595 5.99696 78.1788 + endloop + endfacet + facet normal -0.42137 -0.383247 -0.82193 + outer loop + vertex 31.99 5.34975 78.4284 + vertex 31.9268 5.35983 78.4561 + vertex 31.99 5.9732 78.1377 + endloop + endfacet + facet normal -0.421081 -0.383333 -0.822038 + outer loop + vertex 31.99 5.9732 78.1377 + vertex 31.9268 5.35983 78.4561 + vertex 31.9268 5.98794 78.1632 + endloop + endfacet + facet normal -0.572392 -0.346462 -0.74319 + outer loop + vertex 32.0471 5.33607 78.3908 + vertex 31.99 5.34975 78.4284 + vertex 32.0471 5.95321 78.1031 + endloop + endfacet + facet normal -0.571883 -0.34667 -0.743485 + outer loop + vertex 32.0471 5.95321 78.1031 + vertex 31.99 5.34975 78.4284 + vertex 31.99 5.9732 78.1377 + endloop + endfacet + facet normal -0.705579 -0.299442 -0.642256 + outer loop + vertex 32.0964 5.31921 78.3445 + vertex 32.0471 5.33607 78.3908 + vertex 32.0964 5.92856 78.0604 + endloop + endfacet + facet normal -0.705787 -0.299327 -0.642081 + outer loop + vertex 32.0964 5.92856 78.0604 + vertex 32.0471 5.33607 78.3908 + vertex 32.0471 5.95321 78.1031 + endloop + endfacet + facet normal -0.81819 -0.242959 -0.521091 + outer loop + vertex 32.1364 5.29968 78.2908 + vertex 32.0964 5.31921 78.3445 + vertex 32.1364 5.9 78.0109 + endloop + endfacet + facet normal -0.818234 -0.242926 -0.521038 + outer loop + vertex 32.1364 5.9 78.0109 + vertex 32.0964 5.31921 78.3445 + vertex 32.0964 5.92856 78.0604 + endloop + endfacet + facet normal -0.905323 -0.179507 -0.384926 + outer loop + vertex 32.1659 5.27806 78.2315 + vertex 32.1364 5.29968 78.2908 + vertex 32.1659 5.8684 77.9562 + endloop + endfacet + facet normal -0.905449 -0.179365 -0.384695 + outer loop + vertex 32.1659 5.8684 77.9562 + vertex 32.1364 5.29968 78.2908 + vertex 32.1364 5.9 78.0109 + endloop + endfacet + facet normal -0.965853 -0.109523 -0.234803 + outer loop + vertex 32.1839 5.25503 78.1682 + vertex 32.1659 5.27806 78.2315 + vertex 32.1839 5.83473 77.8978 + endloop + endfacet + facet normal -0.965908 -0.109417 -0.234628 + outer loop + vertex 32.1839 5.83473 77.8978 + vertex 32.1659 5.27806 78.2315 + vertex 32.1659 5.8684 77.9562 + endloop + endfacet + facet normal -0.99614 -0.0370972 -0.0795562 + outer loop + vertex 32.19 5.23127 78.1029 + vertex 32.1839 5.25503 78.1682 + vertex 32.19 5.8 77.8377 + endloop + endfacet + facet normal -0.996131 -0.0371473 -0.0796386 + outer loop + vertex 32.19 5.8 77.8377 + vertex 32.1839 5.25503 78.1682 + vertex 32.1839 5.83473 77.8978 + endloop + endfacet + facet normal -0.087223 -0.571377 -0.81604 + outer loop + vertex 31.8595 5.99696 78.1788 + vertex 31.79 6 78.1841 + vertex 31.8595 6.56724 77.7795 + endloop + endfacet + facet normal -0.0873296 -0.571363 -0.816038 + outer loop + vertex 31.8595 6.56724 77.7795 + vertex 31.79 6 78.1841 + vertex 31.79 6.57115 77.7842 + endloop + endfacet + facet normal -0.25774 -0.55419 -0.791482 + outer loop + vertex 31.9268 5.98794 78.1632 + vertex 31.8595 5.99696 78.1788 + vertex 31.9268 6.55564 77.7657 + endloop + endfacet + facet normal -0.257811 -0.554173 -0.79147 + outer loop + vertex 31.9268 6.55564 77.7657 + vertex 31.8595 5.99696 78.1788 + vertex 31.8595 6.56724 77.7795 + endloop + endfacet + facet normal -0.421111 -0.520268 -0.742958 + outer loop + vertex 31.99 5.9732 78.1377 + vertex 31.9268 5.98794 78.1632 + vertex 31.99 6.5367 77.7431 + endloop + endfacet + facet normal -0.421507 -0.520127 -0.742833 + outer loop + vertex 31.99 6.5367 77.7431 + vertex 31.9268 5.98794 78.1632 + vertex 31.9268 6.55564 77.7657 + endloop + endfacet + facet normal -0.571902 -0.470542 -0.67195 + outer loop + vertex 32.0471 5.95321 78.1031 + vertex 31.99 5.9732 78.1377 + vertex 32.0471 6.511 77.7125 + endloop + endfacet + facet normal -0.571891 -0.470548 -0.671956 + outer loop + vertex 32.0471 6.511 77.7125 + vertex 31.99 5.9732 78.1377 + vertex 31.99 6.5367 77.7431 + endloop + endfacet + facet normal -0.705784 -0.40638 -0.580279 + outer loop + vertex 32.0964 5.92856 78.0604 + vertex 32.0471 5.95321 78.1031 + vertex 32.0964 6.47931 77.6747 + endloop + endfacet + facet normal -0.705962 -0.406257 -0.580149 + outer loop + vertex 32.0964 6.47931 77.6747 + vertex 32.0471 5.95321 78.1031 + vertex 32.0471 6.511 77.7125 + endloop + endfacet + facet normal -0.818222 -0.329735 -0.470943 + outer loop + vertex 32.1364 5.9 78.0109 + vertex 32.0964 5.92856 78.0604 + vertex 32.1364 6.44259 77.631 + endloop + endfacet + facet normal -0.817887 -0.330059 -0.471298 + outer loop + vertex 32.1364 6.44259 77.631 + vertex 32.0964 5.92856 78.0604 + vertex 32.0964 6.47931 77.6747 + endloop + endfacet + facet normal -0.905457 -0.243443 -0.347682 + outer loop + vertex 32.1659 5.8684 77.9562 + vertex 32.1364 5.9 78.0109 + vertex 32.1659 6.40197 77.5826 + endloop + endfacet + facet normal -0.90549 -0.243396 -0.347629 + outer loop + vertex 32.1659 6.40197 77.5826 + vertex 32.1364 5.9 78.0109 + vertex 32.1364 6.44259 77.631 + endloop + endfacet + facet normal -0.965901 -0.148485 -0.212101 + outer loop + vertex 32.1839 5.83473 77.8978 + vertex 32.1659 5.8684 77.9562 + vertex 32.1839 6.35868 77.531 + endloop + endfacet + facet normal -0.96585 -0.148612 -0.212246 + outer loop + vertex 32.1839 6.35868 77.531 + vertex 32.1659 5.8684 77.9562 + vertex 32.1659 6.40197 77.5826 + endloop + endfacet + facet normal -0.996132 -0.050398 -0.0719814 + outer loop + vertex 32.19 5.8 77.8377 + vertex 32.1839 5.83473 77.8978 + vertex 32.19 6.31403 77.4778 + endloop + endfacet + facet normal -0.996136 -0.0503671 -0.0719462 + outer loop + vertex 32.19 6.31403 77.4778 + vertex 32.1839 5.83473 77.8978 + vertex 32.1839 6.35868 77.531 + endloop + endfacet + facet normal -0.0872655 -0.704424 -0.704395 + outer loop + vertex 31.79 6.57115 77.7842 + vertex 31.8595 7.05952 77.2872 + vertex 31.8595 6.56724 77.7795 + endloop + endfacet + facet normal -0.0877683 -0.704357 -0.704399 + outer loop + vertex 31.8595 7.05952 77.2872 + vertex 31.79 6.57115 77.7842 + vertex 31.79 7.06418 77.2912 + endloop + endfacet + facet normal -0.257847 -0.683224 -0.683169 + outer loop + vertex 31.8595 6.56724 77.7795 + vertex 31.9268 7.0457 77.2756 + vertex 31.9268 6.55564 77.7657 + endloop + endfacet + facet normal -0.258038 -0.683174 -0.683147 + outer loop + vertex 31.9268 7.0457 77.2756 + vertex 31.8595 6.56724 77.7795 + vertex 31.8595 7.05952 77.2872 + endloop + endfacet + facet normal -0.421468 -0.641261 -0.641209 + outer loop + vertex 31.9268 7.0457 77.2756 + vertex 31.99 6.5367 77.7431 + vertex 31.9268 6.55564 77.7657 + endloop + endfacet + facet normal -0.420968 -0.641386 -0.641412 + outer loop + vertex 31.99 6.5367 77.7431 + vertex 31.9268 7.0457 77.2756 + vertex 31.99 7.02312 77.2567 + endloop + endfacet + facet normal -0.571919 -0.580053 -0.580041 + outer loop + vertex 31.99 6.5367 77.7431 + vertex 32.0471 6.99249 77.231 + vertex 32.0471 6.511 77.7125 + endloop + endfacet + facet normal -0.572124 -0.579934 -0.579958 + outer loop + vertex 32.0471 6.99249 77.231 + vertex 31.99 6.5367 77.7431 + vertex 31.99 7.02312 77.2567 + endloop + endfacet + facet normal -0.705832 -0.500905 -0.500894 + outer loop + vertex 32.0964 6.95472 77.1993 + vertex 32.0471 6.511 77.7125 + vertex 32.0471 6.99249 77.231 + endloop + endfacet + facet normal -0.705934 -0.500823 -0.500833 + outer loop + vertex 32.0471 6.511 77.7125 + vertex 32.0964 6.95472 77.1993 + vertex 32.0964 6.47931 77.6747 + endloop + endfacet + facet normal -0.818019 -0.406718 -0.406726 + outer loop + vertex 32.1364 6.91097 77.1626 + vertex 32.0964 6.47931 77.6747 + vertex 32.0964 6.95472 77.1993 + endloop + endfacet + facet normal -0.817917 -0.406832 -0.406815 + outer loop + vertex 32.0964 6.47931 77.6747 + vertex 32.1364 6.91097 77.1626 + vertex 32.1364 6.44259 77.631 + endloop + endfacet + facet normal -0.905475 -0.300102 -0.300089 + outer loop + vertex 32.1659 6.86256 77.122 + vertex 32.1364 6.44259 77.631 + vertex 32.1364 6.91097 77.1626 + endloop + endfacet + facet normal -0.905493 -0.300072 -0.300065 + outer loop + vertex 32.1364 6.44259 77.631 + vertex 32.1659 6.86256 77.122 + vertex 32.1659 6.40197 77.5826 + endloop + endfacet + facet normal -0.965849 -0.183217 -0.183213 + outer loop + vertex 32.1839 6.81097 77.0787 + vertex 32.1659 6.40197 77.5826 + vertex 32.1659 6.86256 77.122 + endloop + endfacet + facet normal -0.965849 -0.183217 -0.183213 + outer loop + vertex 32.1659 6.40197 77.5826 + vertex 32.1839 6.81097 77.0787 + vertex 32.1839 6.35868 77.531 + endloop + endfacet + facet normal -0.996141 -0.0620623 -0.0620609 + outer loop + vertex 32.19 6.75776 77.034 + vertex 32.1839 6.35868 77.531 + vertex 32.1839 6.81097 77.0787 + endloop + endfacet + facet normal -0.996136 -0.0621048 -0.062095 + outer loop + vertex 32.1839 6.35868 77.531 + vertex 32.19 6.75776 77.034 + vertex 32.19 6.31403 77.4778 + endloop + endfacet + facet normal -0.0875986 -0.815962 -0.571431 + outer loop + vertex 31.8595 7.05952 77.2872 + vertex 31.79 7.06418 77.2912 + vertex 31.8595 7.45884 76.717 + endloop + endfacet + facet normal -0.0864307 -0.816113 -0.571393 + outer loop + vertex 31.8595 7.45884 76.717 + vertex 31.79 7.06418 77.2912 + vertex 31.79 7.4641 76.72 + endloop + endfacet + facet normal -0.258031 -0.791411 -0.554155 + outer loop + vertex 31.9268 7.0457 77.2756 + vertex 31.8595 7.05952 77.2872 + vertex 31.9268 7.44321 76.7079 + endloop + endfacet + facet normal -0.258682 -0.79123 -0.554111 + outer loop + vertex 31.9268 7.44321 76.7079 + vertex 31.8595 7.05952 77.2872 + vertex 31.8595 7.45884 76.717 + endloop + endfacet + facet normal -0.421043 -0.743002 -0.52026 + outer loop + vertex 31.99 7.02312 77.2567 + vertex 31.9268 7.0457 77.2756 + vertex 31.99 7.41769 76.6932 + endloop + endfacet + facet normal -0.421034 -0.743006 -0.520261 + outer loop + vertex 31.99 7.41769 76.6932 + vertex 31.9268 7.0457 77.2756 + vertex 31.9268 7.44321 76.7079 + endloop + endfacet + facet normal -0.572127 -0.671843 -0.470423 + outer loop + vertex 32.0471 6.99249 77.231 + vertex 31.99 7.02312 77.2567 + vertex 32.0471 7.38306 76.6732 + endloop + endfacet + facet normal -0.572196 -0.671798 -0.470402 + outer loop + vertex 32.0471 7.38306 76.6732 + vertex 31.99 7.02312 77.2567 + vertex 31.99 7.41769 76.6932 + endloop + endfacet + facet normal -0.705826 -0.580257 -0.406338 + outer loop + vertex 32.0964 6.95472 77.1993 + vertex 32.0471 6.99249 77.231 + vertex 32.0964 7.34036 76.6486 + endloop + endfacet + facet normal -0.70558 -0.580479 -0.40645 + outer loop + vertex 32.0964 7.34036 76.6486 + vertex 32.0471 6.99249 77.231 + vertex 32.0471 7.38306 76.6732 + endloop + endfacet + facet normal -0.818026 -0.471162 -0.329909 + outer loop + vertex 32.1364 6.91097 77.1626 + vertex 32.0964 6.95472 77.1993 + vertex 32.1364 7.2909 76.62 + endloop + endfacet + facet normal -0.818174 -0.470974 -0.32981 + outer loop + vertex 32.1364 7.2909 76.62 + vertex 32.0964 6.95472 77.1993 + vertex 32.0964 7.34036 76.6486 + endloop + endfacet + facet normal -0.905482 -0.347642 -0.243408 + outer loop + vertex 32.1659 6.86256 77.122 + vertex 32.1364 6.91097 77.1626 + vertex 32.1659 7.23617 76.5884 + endloop + endfacet + facet normal -0.905521 -0.347568 -0.243368 + outer loop + vertex 32.1659 7.23617 76.5884 + vertex 32.1364 6.91097 77.1626 + vertex 32.1364 7.2909 76.62 + endloop + endfacet + facet normal -0.965847 -0.212259 -0.14861 + outer loop + vertex 32.1839 6.81097 77.0787 + vertex 32.1659 6.86256 77.122 + vertex 32.1839 7.17784 76.5547 + endloop + endfacet + facet normal -0.965862 -0.212211 -0.148583 + outer loop + vertex 32.1839 7.17784 76.5547 + vertex 32.1659 6.86256 77.122 + vertex 32.1659 7.23617 76.5884 + endloop + endfacet + facet normal -0.99614 -0.0719011 -0.0503489 + outer loop + vertex 32.19 6.75776 77.034 + vertex 32.1839 6.81097 77.0787 + vertex 32.19 7.11769 76.52 + endloop + endfacet + facet normal -0.996135 -0.0719574 -0.0503798 + outer loop + vertex 32.19 7.11769 76.52 + vertex 32.1839 6.81097 77.0787 + vertex 32.1839 7.17784 76.5547 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 31.79 7.06418 77.2912 + vertex 31.79 7.46406 76.7201 + vertex 31.79 7.4641 76.72 + endloop + endfacet + facet normal -0.0865093 -0.902921 -0.42101 + outer loop + vertex 31.8595 7.45884 76.717 + vertex 31.79 7.4641 76.72 + vertex 31.8595 7.75306 76.086 + endloop + endfacet + facet normal -0.0869004 -0.902874 -0.421031 + outer loop + vertex 31.8595 7.75306 76.086 + vertex 31.79 7.4641 76.72 + vertex 31.79 7.75877 76.0881 + endloop + endfacet + facet normal -0.25853 -0.875495 -0.408253 + outer loop + vertex 31.9268 7.44321 76.7079 + vertex 31.8595 7.45884 76.717 + vertex 31.9268 7.7361 76.0798 + endloop + endfacet + facet normal -0.25826 -0.875572 -0.408258 + outer loop + vertex 31.9268 7.7361 76.0798 + vertex 31.8595 7.45884 76.717 + vertex 31.8595 7.75306 76.086 + endloop + endfacet + facet normal -0.421094 -0.822048 -0.383297 + outer loop + vertex 31.99 7.41769 76.6932 + vertex 31.9268 7.44321 76.7079 + vertex 31.99 7.70841 76.0697 + endloop + endfacet + facet normal -0.421362 -0.821923 -0.383272 + outer loop + vertex 31.99 7.70841 76.0697 + vertex 31.9268 7.44321 76.7079 + vertex 31.9268 7.7361 76.0798 + endloop + endfacet + facet normal -0.572188 -0.743278 -0.34661 + outer loop + vertex 32.0471 7.38306 76.6732 + vertex 31.99 7.41769 76.6932 + vertex 32.0471 7.67083 76.0561 + endloop + endfacet + facet normal -0.571887 -0.743484 -0.346665 + outer loop + vertex 32.0471 7.67083 76.0561 + vertex 31.99 7.41769 76.6932 + vertex 31.99 7.70841 76.0697 + endloop + endfacet + facet normal -0.705636 -0.642198 -0.299432 + outer loop + vertex 32.0964 7.34036 76.6486 + vertex 32.0471 7.38306 76.6732 + vertex 32.0964 7.6245 76.0392 + endloop + endfacet + facet normal -0.705895 -0.641948 -0.299357 + outer loop + vertex 32.0964 7.6245 76.0392 + vertex 32.0471 7.38306 76.6732 + vertex 32.0471 7.67083 76.0561 + endloop + endfacet + facet normal -0.818143 -0.521138 -0.243015 + outer loop + vertex 32.1364 7.2909 76.62 + vertex 32.0964 7.34036 76.6486 + vertex 32.1364 7.57083 76.0197 + endloop + endfacet + facet normal -0.818 -0.521334 -0.243078 + outer loop + vertex 32.1364 7.57083 76.0197 + vertex 32.0964 7.34036 76.6486 + vertex 32.0964 7.6245 76.0392 + endloop + endfacet + facet normal -0.905521 -0.384544 -0.179328 + outer loop + vertex 32.1659 7.23617 76.5884 + vertex 32.1364 7.2909 76.62 + vertex 32.1659 7.51145 75.9981 + endloop + endfacet + facet normal -0.905489 -0.384608 -0.179349 + outer loop + vertex 32.1659 7.51145 75.9981 + vertex 32.1364 7.2909 76.62 + vertex 32.1364 7.57083 76.0197 + endloop + endfacet + facet normal -0.965859 -0.234797 -0.109488 + outer loop + vertex 32.1839 7.17784 76.5547 + vertex 32.1659 7.23617 76.5884 + vertex 32.1839 7.44816 75.975 + endloop + endfacet + facet normal -0.965873 -0.234744 -0.10947 + outer loop + vertex 32.1839 7.44816 75.975 + vertex 32.1659 7.23617 76.5884 + vertex 32.1659 7.51145 75.9981 + endloop + endfacet + facet normal -0.996135 -0.0796057 -0.0371223 + outer loop + vertex 32.19 7.11769 76.52 + vertex 32.1839 7.17784 76.5547 + vertex 32.19 7.38289 75.9513 + endloop + endfacet + facet normal -0.996134 -0.079616 -0.0371258 + outer loop + vertex 32.19 7.38289 75.9513 + vertex 32.1839 7.17784 76.5547 + vertex 32.1839 7.44816 75.975 + endloop + endfacet + facet normal -0.0868498 -0.962278 -0.257833 + outer loop + vertex 31.8595 7.75306 76.086 + vertex 31.79 7.75877 76.0881 + vertex 31.8595 7.93325 75.4135 + endloop + endfacet + facet normal -0.086878 -0.962275 -0.257835 + outer loop + vertex 31.8595 7.93325 75.4135 + vertex 31.79 7.75877 76.0881 + vertex 31.79 7.93923 75.4146 + endloop + endfacet + facet normal -0.2582 -0.933171 -0.250049 + outer loop + vertex 31.9268 7.7361 76.0798 + vertex 31.8595 7.75306 76.086 + vertex 31.9268 7.91547 75.4104 + endloop + endfacet + facet normal -0.258063 -0.93321 -0.250045 + outer loop + vertex 31.9268 7.91547 75.4104 + vertex 31.8595 7.75306 76.086 + vertex 31.8595 7.93325 75.4135 + endloop + endfacet + facet normal -0.421321 -0.876005 -0.234744 + outer loop + vertex 31.99 7.70841 76.0697 + vertex 31.9268 7.7361 76.0798 + vertex 31.99 7.88645 75.4053 + endloop + endfacet + facet normal -0.421209 -0.876058 -0.234745 + outer loop + vertex 31.99 7.88645 75.4053 + vertex 31.9268 7.7361 76.0798 + vertex 31.9268 7.91547 75.4104 + endloop + endfacet + facet normal -0.572009 -0.792303 -0.212277 + outer loop + vertex 32.0471 7.67083 76.0561 + vertex 31.99 7.70841 76.0697 + vertex 32.0471 7.84707 75.3983 + endloop + endfacet + facet normal -0.572303 -0.792095 -0.212259 + outer loop + vertex 32.0471 7.84707 75.3983 + vertex 31.99 7.70841 76.0697 + vertex 31.99 7.88645 75.4053 + endloop + endfacet + facet normal -0.705852 -0.684219 -0.18335 + outer loop + vertex 32.0964 7.6245 76.0392 + vertex 32.0471 7.67083 76.0561 + vertex 32.0964 7.79852 75.3898 + endloop + endfacet + facet normal -0.705638 -0.684433 -0.183376 + outer loop + vertex 32.0964 7.79852 75.3898 + vertex 32.0471 7.67083 76.0561 + vertex 32.0471 7.84707 75.3983 + endloop + endfacet + facet normal -0.818026 -0.555581 -0.148873 + outer loop + vertex 32.1364 7.57083 76.0197 + vertex 32.0964 7.6245 76.0392 + vertex 32.1364 7.74227 75.3799 + endloop + endfacet + facet normal -0.818061 -0.555532 -0.148866 + outer loop + vertex 32.1364 7.74227 75.3799 + vertex 32.0964 7.6245 76.0392 + vertex 32.0964 7.79852 75.3898 + endloop + endfacet + facet normal -0.905495 -0.409899 -0.10983 + outer loop + vertex 32.1659 7.51145 75.9981 + vertex 32.1364 7.57083 76.0197 + vertex 32.1659 7.68004 75.3689 + endloop + endfacet + facet normal -0.905519 -0.409847 -0.109822 + outer loop + vertex 32.1659 7.68004 75.3689 + vertex 32.1364 7.57083 76.0197 + vertex 32.1364 7.74227 75.3799 + endloop + endfacet + facet normal -0.965863 -0.250224 -0.0670517 + outer loop + vertex 32.1839 7.44816 75.975 + vertex 32.1659 7.51145 75.9981 + vertex 32.1839 7.61371 75.3572 + endloop + endfacet + facet normal -0.96585 -0.250274 -0.0670594 + outer loop + vertex 32.1839 7.61371 75.3572 + vertex 32.1659 7.51145 75.9981 + vertex 32.1659 7.68004 75.3689 + endloop + endfacet + facet normal -0.996135 -0.0848426 -0.022732 + outer loop + vertex 32.19 7.38289 75.9513 + vertex 32.1839 7.44816 75.975 + vertex 32.19 7.54531 75.3451 + endloop + endfacet + facet normal -0.996137 -0.0848162 -0.0227279 + outer loop + vertex 32.19 7.54531 75.3451 + vertex 32.1839 7.44816 75.975 + vertex 32.1839 7.61371 75.3572 + endloop + endfacet + facet normal -0.0867667 -0.992438 -0.0868222 + outer loop + vertex 31.8595 7.93325 75.4135 + vertex 31.79 7.93923 75.4146 + vertex 31.8595 7.99392 74.72 + endloop + endfacet + facet normal -0.08682 -0.992433 -0.0868272 + outer loop + vertex 31.8595 7.99392 74.72 + vertex 31.79 7.93923 75.4146 + vertex 31.79 8 74.72 + endloop + endfacet + facet normal -0.258143 -0.962429 -0.0842126 + outer loop + vertex 31.9268 7.91547 75.4104 + vertex 31.8595 7.93325 75.4135 + vertex 31.9268 7.97588 74.72 + endloop + endfacet + facet normal -0.258046 -0.962457 -0.0841993 + outer loop + vertex 31.8918 7.98526 74.72 + vertex 31.8595 7.93325 75.4135 + vertex 31.8595 7.99392 74.72 + endloop + endfacet + facet normal -0.257429 -0.962623 -0.0841831 + outer loop + vertex 31.905 7.98173 74.72 + vertex 31.8595 7.93325 75.4135 + vertex 31.8918 7.98526 74.72 + endloop + endfacet + facet normal -0.258258 -0.962398 -0.0842218 + outer loop + vertex 31.8595 7.93325 75.4135 + vertex 31.905 7.98173 74.72 + vertex 31.9268 7.97588 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.8918 7.98526 74.72 + vertex 31.8595 7.99392 74.72 + vertex 31.905 7.98173 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.9268 7.97588 74.72 + vertex 31.905 7.98173 74.72 + vertex 31.8918 7.98526 74.72 + endloop + endfacet + facet normal -0.421244 -0.903496 -0.0790509 + outer loop + vertex 31.99 7.88645 75.4053 + vertex 31.9268 7.91547 75.4104 + vertex 31.99 7.94641 74.72 + endloop + endfacet + facet normal -0.420698 -0.903748 -0.0790779 + outer loop + vertex 31.9526 7.96387 74.72 + vertex 31.9268 7.91547 75.4104 + vertex 31.9268 7.97588 74.72 + endloop + endfacet + facet normal -0.422257 -0.90302 -0.0790851 + outer loop + vertex 31.9597 7.96055 74.72 + vertex 31.9268 7.91547 75.4104 + vertex 31.9526 7.96387 74.72 + endloop + endfacet + facet normal -0.421561 -0.903346 -0.0790733 + outer loop + vertex 31.9268 7.91547 75.4104 + vertex 31.9597 7.96055 74.72 + vertex 31.99 7.94641 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.9526 7.96387 74.72 + vertex 31.9268 7.97588 74.72 + vertex 31.9597 7.96055 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.99 7.94641 74.72 + vertex 31.9597 7.96055 74.72 + vertex 31.9526 7.96387 74.72 + endloop + endfacet + facet normal -0.572211 -0.816985 -0.0714847 + outer loop + vertex 32.0471 7.84707 75.3983 + vertex 31.99 7.88645 75.4053 + vertex 32.0471 7.90642 74.72 + endloop + endfacet + facet normal -0.572396 -0.816857 -0.0714705 + outer loop + vertex 32.0174 7.92721 74.72 + vertex 31.99 7.88645 75.4053 + vertex 31.99 7.94641 74.72 + endloop + endfacet + facet normal -0.570718 -0.818029 -0.0714732 + outer loop + vertex 32.0217 7.92421 74.72 + vertex 31.99 7.88645 75.4053 + vertex 32.0174 7.92721 74.72 + endloop + endfacet + facet normal -0.572211 -0.816985 -0.0714847 + outer loop + vertex 31.99 7.88645 75.4053 + vertex 32.0217 7.92421 74.72 + vertex 32.0471 7.90642 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0174 7.92721 74.72 + vertex 32.0471 7.90642 74.72 + vertex 32.0217 7.92421 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.99 7.94641 74.72 + vertex 32.0217 7.92421 74.72 + vertex 32.0174 7.92721 74.72 + endloop + endfacet + facet normal -0.705712 -0.705804 -0.0617394 + outer loop + vertex 32.0964 7.79852 75.3898 + vertex 32.0471 7.84707 75.3983 + vertex 32.0964 7.85711 74.72 + endloop + endfacet + facet normal 0.704393 0.707112 0.0618305 + outer loop + vertex 32.0728 7.88078 74.72 + vertex 32.0702 7.88337 74.72 + vertex 32.0471 7.84707 75.3983 + endloop + endfacet + facet normal -0.706799 -0.704709 -0.0618023 + outer loop + vertex 32.0471 7.84707 75.3983 + vertex 32.0728 7.88078 74.72 + vertex 32.0964 7.85711 74.72 + endloop + endfacet + facet normal -0.704989 -0.706519 -0.0618191 + outer loop + vertex 32.0702 7.88337 74.72 + vertex 32.0471 7.84707 75.3983 + vertex 32.0471 7.90642 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0702 7.88337 74.72 + vertex 32.0964 7.85711 74.72 + vertex 32.0728 7.88078 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.0702 7.88337 74.72 + vertex 32.0471 7.90642 74.72 + vertex 32.0728 7.88078 74.72 + endloop + endfacet + facet normal -0.704393 -0.707112 -0.0618305 + outer loop + vertex 32.0728 7.88078 74.72 + vertex 32.0471 7.84707 75.3983 + vertex 32.0702 7.88337 74.72 + endloop + endfacet + facet normal -0.704393 -0.707112 -0.0618305 + outer loop + vertex 32.0728 7.88078 74.72 + vertex 32.0471 7.84707 75.3983 + vertex 32.0702 7.88337 74.72 + endloop + endfacet + facet normal -0.818076 -0.572922 -0.0501209 + outer loop + vertex 32.1364 7.74227 75.3799 + vertex 32.0964 7.79852 75.3898 + vertex 32.1364 7.8 74.72 + endloop + endfacet + facet normal -0.818048 -0.572963 -0.0501193 + outer loop + vertex 32.1364 7.8 74.72 + vertex 32.0964 7.79852 75.3898 + vertex 32.0964 7.85711 74.72 + endloop + endfacet + facet normal -0.905507 -0.422717 -0.036982 + outer loop + vertex 32.1659 7.68004 75.3689 + vertex 32.1364 7.74227 75.3799 + vertex 32.1659 7.73681 74.72 + endloop + endfacet + facet normal 0.910757 0.41131 0.0366762 + outer loop + vertex 32.1513 7.76812 74.72 + vertex 32.1506 7.76967 74.72 + vertex 32.1364 7.74227 75.3799 + endloop + endfacet + facet normal -0.905034 -0.423722 -0.0370684 + outer loop + vertex 32.1506 7.76967 74.72 + vertex 32.1364 7.74227 75.3799 + vertex 32.1364 7.8 74.72 + endloop + endfacet + facet normal -0.905688 -0.422327 -0.0369933 + outer loop + vertex 32.1364 7.74227 75.3799 + vertex 32.1513 7.76812 74.72 + vertex 32.1659 7.73681 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1506 7.76967 74.72 + vertex 32.1364 7.8 74.72 + vertex 32.1513 7.76812 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.1506 7.76967 74.72 + vertex 32.1659 7.73681 74.72 + vertex 32.1513 7.76812 74.72 + endloop + endfacet + facet normal -0.910757 -0.41131 -0.0366762 + outer loop + vertex 32.1506 7.76967 74.72 + vertex 32.1513 7.76812 74.72 + vertex 32.1364 7.74227 75.3799 + endloop + endfacet + facet normal -0.910757 -0.41131 -0.0366762 + outer loop + vertex 32.1506 7.76967 74.72 + vertex 32.1513 7.76812 74.72 + vertex 32.1364 7.74227 75.3799 + endloop + endfacet + facet normal -0.965849 -0.258119 -0.0225834 + outer loop + vertex 32.1839 7.61371 75.3572 + vertex 32.1659 7.68004 75.3689 + vertex 32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0.952385 0.303953 0.0239843 + outer loop + vertex 32.1753 7.70179 74.72 + vertex 32.175 7.70273 74.72 + vertex 32.1659 7.68004 75.3689 + endloop + endfacet + facet normal -0.965904 -0.257915 -0.022564 + outer loop + vertex 32.175 7.70273 74.72 + vertex 32.1659 7.68004 75.3689 + vertex 32.1659 7.73681 74.72 + endloop + endfacet + facet normal -0.966146 -0.257002 -0.0226099 + outer loop + vertex 32.1659 7.68004 75.3689 + vertex 32.1753 7.70179 74.72 + vertex 32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.175 7.70273 74.72 + vertex 32.1659 7.73681 74.72 + vertex 32.1753 7.70179 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.175 7.70273 74.72 + vertex 32.1839 7.66946 74.72 + vertex 32.1753 7.70179 74.72 + endloop + endfacet + facet normal -0.952385 -0.303953 -0.0239843 + outer loop + vertex 32.1659 7.68004 75.3689 + vertex 32.175 7.70273 74.72 + vertex 32.1753 7.70179 74.72 + endloop + endfacet + facet normal -0.952385 -0.303953 -0.0239843 + outer loop + vertex 32.1659 7.68004 75.3689 + vertex 32.175 7.70273 74.72 + vertex 32.1753 7.70179 74.72 + endloop + endfacet + facet normal -0.996137 -0.0874828 -0.00765387 + outer loop + vertex 32.19 7.54531 75.3451 + vertex 32.1839 7.61371 75.3572 + vertex 32.19 7.6 74.72 + endloop + endfacet + facet normal -0.996137 -0.0874811 -0.00765391 + outer loop + vertex 32.19 7.6 74.72 + vertex 32.1839 7.61371 75.3572 + vertex 32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1753 7.70179 74.72 + vertex 32.1659 7.73681 74.72 + vertex 32.1659 7.7368 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 7.54531 75.3451 + vertex -32.19 7.38289 75.9513 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 7.38289 75.9513 + vertex -32.19 7.11769 76.52 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 7.11769 76.52 + vertex -32.19 6.75776 77.034 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 6.75776 77.034 + vertex -32.19 6.31403 77.4778 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 6.31403 77.4778 + vertex -32.19 5.8 77.8377 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 5.8 77.8377 + vertex -32.19 5.23127 78.1029 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 5.23127 78.1029 + vertex -32.19 4.62513 78.2653 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 4.62513 78.2653 + vertex -32.19 4 78.32 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 7.54531 75.3451 + vertex -32.19 4 74.72 + vertex -32.19 7.6 74.72 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 3.37487 78.2653 + vertex -32.19 4 74.72 + vertex -32.19 4 78.32 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 2.76873 78.1029 + vertex -32.19 4 74.72 + vertex -32.19 3.37487 78.2653 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 3.37487 71.1747 + vertex -32.19 4 71.12 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 2.2 77.8377 + vertex -32.19 4 74.72 + vertex -32.19 2.76873 78.1029 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 2.76873 71.3371 + vertex -32.19 3.37487 71.1747 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 1.68597 77.4778 + vertex -32.19 4 74.72 + vertex -32.19 2.2 77.8377 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 2.2 71.6023 + vertex -32.19 2.76873 71.3371 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 1.24224 77.034 + vertex -32.19 4 74.72 + vertex -32.19 1.68597 77.4778 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 1.68597 71.9622 + vertex -32.19 2.2 71.6023 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 0.882309 76.52 + vertex -32.19 4 74.72 + vertex -32.19 1.24224 77.034 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 1.24224 72.406 + vertex -32.19 1.68597 71.9622 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 0.617107 75.9513 + vertex -32.19 4 74.72 + vertex -32.19 0.882309 76.52 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 0.882309 72.92 + vertex -32.19 1.24224 72.406 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 0.454693 75.3451 + vertex -32.19 4 74.72 + vertex -32.19 0.617107 75.9513 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 0.617107 73.4887 + vertex -32.19 0.882309 72.92 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 0.400001 74.72 + vertex -32.19 4 74.72 + vertex -32.19 0.454693 75.3451 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 0.454693 74.0949 + vertex -32.19 0.617107 73.4887 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 74.72 + vertex -32.19 0.400001 74.72 + vertex -32.19 0.454693 74.0949 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 32.19 7.59999 74.7199 + vertex 32.19 7.54531 74.0949 + vertex 32.19 7.59999 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 74.72 + vertex 32.19 7.54531 75.3451 + vertex 32.19 7.6 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.54531 74.0949 + vertex 32.19 7.54531 75.3451 + vertex 32.19 7.59999 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.38289 73.4887 + vertex 32.19 7.54531 75.3451 + vertex 32.19 7.54531 74.0949 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.38289 73.4887 + vertex 32.19 7.38289 75.9513 + vertex 32.19 7.54531 75.3451 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.11769 72.92 + vertex 32.19 7.38289 75.9513 + vertex 32.19 7.38289 73.4887 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.11769 72.92 + vertex 32.19 7.11769 76.52 + vertex 32.19 7.38289 75.9513 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 6.75776 72.406 + vertex 32.19 7.11769 76.52 + vertex 32.19 7.11769 72.92 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 6.75776 72.406 + vertex 32.19 6.75776 77.034 + vertex 32.19 7.11769 76.52 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 6.31403 71.9622 + vertex 32.19 6.75776 77.034 + vertex 32.19 6.75776 72.406 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 6.31403 71.9622 + vertex 32.19 6.31403 77.4778 + vertex 32.19 6.75776 77.034 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 5.8 71.6023 + vertex 32.19 6.31403 77.4778 + vertex 32.19 6.31403 71.9622 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 5.8 71.6023 + vertex 32.19 5.8 77.8377 + vertex 32.19 6.31403 77.4778 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 5.23127 71.3371 + vertex 32.19 5.8 77.8377 + vertex 32.19 5.8 71.6023 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 5.23127 71.3371 + vertex 32.19 5.23127 78.1029 + vertex 32.19 5.8 77.8377 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4.62513 71.1747 + vertex 32.19 5.23127 78.1029 + vertex 32.19 5.23127 71.3371 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4.62513 71.1747 + vertex 32.19 4.62513 78.2653 + vertex 32.19 5.23127 78.1029 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 4.62513 78.2653 + vertex 32.19 4.62513 71.1747 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 78.32 + vertex 32.19 4.62513 78.2653 + vertex 32.19 4 71.12 + endloop + endfacet + facet normal 0 -0.996195 -0.0871563 + outer loop + vertex -31.79 8 74.72 + vertex 31.79 7.93923 75.4146 + vertex -31.79 7.93923 75.4146 + endloop + endfacet + facet normal 0 -0.996195 -0.0871563 + outer loop + vertex 31.79 7.93923 75.4146 + vertex -31.79 8 74.72 + vertex 31.79 8 74.72 + endloop + endfacet + facet normal 0 -0.0872002 -0.996191 + outer loop + vertex -31.79 4 78.72 + vertex 31.79 4.69459 78.6592 + vertex 31.79 4 78.72 + endloop + endfacet + facet normal -0 -0.0872002 -0.996191 + outer loop + vertex 31.79 4.69459 78.6592 + vertex -31.79 4 78.72 + vertex -31.79 4.69459 78.6592 + endloop + endfacet + facet normal 0 -0.707085 -0.707128 + outer loop + vertex -31.79 6.57115 77.7842 + vertex 31.79 7.06418 77.2912 + vertex 31.79 6.57115 77.7842 + endloop + endfacet + facet normal -0 -0.707085 -0.707128 + outer loop + vertex 31.79 7.06418 77.2912 + vertex -31.79 6.57115 77.7842 + vertex -31.79 7.06418 77.2912 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.79 7.46412 76.72 + vertex 31.79 7.4641 76.72 + vertex -31.79 7.4641 76.72 + endloop + endfacet + facet normal -2.85094e-07 -0.906313 -0.422607 + outer loop + vertex -31.79 7.75877 76.0881 + vertex 31.79 7.4641 76.72 + vertex -31.79 7.46412 76.72 + endloop + endfacet + facet normal 0 -0.906302 -0.42263 + outer loop + vertex 31.79 7.4641 76.72 + vertex -31.79 7.75877 76.0881 + vertex 31.79 7.75877 76.0881 + endloop + endfacet + facet normal 0 -0.965927 -0.258814 + outer loop + vertex -31.79 7.93923 75.4146 + vertex 31.79 7.75877 76.0881 + vertex -31.79 7.75877 76.0881 + endloop + endfacet + facet normal 0 -0.965927 -0.258814 + outer loop + vertex 31.79 7.75877 76.0881 + vertex -31.79 7.93923 75.4146 + vertex 31.79 7.93923 75.4146 + endloop + endfacet + facet normal 0 -0.819158 -0.573568 + outer loop + vertex -31.79 7.06418 77.2912 + vertex 31.79 7.46406 76.7201 + vertex 31.79 7.06418 77.2912 + endloop + endfacet + facet normal 3.86707e-07 -0.819178 -0.573539 + outer loop + vertex -31.79 7.4641 76.72 + vertex 31.79 7.46406 76.7201 + vertex -31.79 7.06418 77.2912 + endloop + endfacet + facet normal 0 -0.928477 -0.371391 + outer loop + vertex 31.79 7.46406 76.7201 + vertex -31.79 7.4641 76.72 + vertex 31.79 7.4641 76.72 + endloop + endfacet + facet normal 0 -0.422655 -0.906291 + outer loop + vertex -31.79 5.36808 78.4788 + vertex 31.79 6 78.1841 + vertex 31.79 5.36808 78.4788 + endloop + endfacet + facet normal -0 -0.422655 -0.906291 + outer loop + vertex 31.79 6 78.1841 + vertex -31.79 5.36808 78.4788 + vertex -31.79 6 78.1841 + endloop + endfacet + facet normal 0 -0.258737 -0.965948 + outer loop + vertex -31.79 4.69459 78.6592 + vertex 31.79 5.36808 78.4788 + vertex 31.79 4.69459 78.6592 + endloop + endfacet + facet normal -0 -0.258737 -0.965948 + outer loop + vertex 31.79 5.36808 78.4788 + vertex -31.79 4.69459 78.6592 + vertex -31.79 5.36808 78.4788 + endloop + endfacet + facet normal 0 -0.573554 -0.819168 + outer loop + vertex -31.79 6 78.1841 + vertex 31.79 6.57115 77.7842 + vertex 31.79 6 78.1841 + endloop + endfacet + facet normal -0 -0.573554 -0.819168 + outer loop + vertex 31.79 6.57115 77.7842 + vertex -31.79 6 78.1841 + vertex -31.79 6.57115 77.7842 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.454693 74.0949 + vertex 32.19 -0.454693 75.3451 + vertex 32.19 -0.400001 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.617107 73.4887 + vertex 32.19 -0.454693 75.3451 + vertex 32.19 -0.454693 74.0949 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.617107 73.4887 + vertex 32.19 -0.617107 75.9513 + vertex 32.19 -0.454693 75.3451 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.882309 72.92 + vertex 32.19 -0.617107 75.9513 + vertex 32.19 -0.617107 73.4887 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.882309 72.92 + vertex 32.19 -0.882309 76.52 + vertex 32.19 -0.617107 75.9513 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.99219 71.7478 + vertex 32.19 -0.882309 72.92 + vertex 32.19 -1.24224 72.406 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 32.19 -0.882309 72.92 + vertex 32.19 -1.99219 71.7478 + vertex 32.19 -0.882309 76.52 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.99219 71.7478 + vertex 32.19 -1.24224 72.406 + vertex 32.19 -1.68597 71.9622 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.99219 77.6922 + vertex 32.19 -0.882309 76.52 + vertex 32.19 -1.99219 71.7478 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.882309 76.52 + vertex 32.19 -1.99219 77.6922 + vertex 32.19 -1.24224 77.034 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.24224 77.034 + vertex 32.19 -1.99219 77.6922 + vertex 32.19 -1.68597 77.4778 + endloop + endfacet + facet normal 0.422611 -0.906311 0 + outer loop + vertex -31.99 7.94641 2 + vertex -31.9268 7.97588 10 + vertex -31.99 7.94641 10 + endloop + endfacet + facet normal 0.422611 -0.906311 0 + outer loop + vertex -31.9268 7.97588 10 + vertex -31.99 7.94641 2 + vertex -31.9268 7.97588 2 + endloop + endfacet + facet normal 0.422611 -0.906311 0 + outer loop + vertex -31.99 7.94641 11.6 + vertex -31.9268 7.97588 74.72 + vertex -31.99 7.94641 74.72 + endloop + endfacet + facet normal 0.422611 -0.906311 0 + outer loop + vertex -31.9268 7.97588 74.72 + vertex -31.99 7.94641 11.6 + vertex -31.9268 7.97588 11.6 + endloop + endfacet + facet normal 0.0871492 -0.996195 0 + outer loop + vertex -31.8595 7.99392 10 + vertex -31.79 8 2 + vertex -31.79 8 10 + endloop + endfacet + facet normal 0.0871492 -0.996195 0 + outer loop + vertex -31.8595 7.99392 2 + vertex -31.79 8 2 + vertex -31.8595 7.99392 10 + endloop + endfacet + facet normal 0.0871492 -0.996195 0 + outer loop + vertex -31.8595 7.99392 11.6 + vertex -31.79 8 74.72 + vertex -31.8595 7.99392 74.72 + endloop + endfacet + facet normal 0.0871492 -0.996195 0 + outer loop + vertex -31.79 8 74.72 + vertex -31.8595 7.99392 11.6 + vertex -31.79 8 11.6 + endloop + endfacet + facet normal 0.966092 -0.258198 0 + outer loop + vertex -32.1839 7.66946 10 + vertex -32.1659 7.73681 2 + vertex -32.1659 7.73681 10 + endloop + endfacet + facet normal 0.966092 -0.258198 0 + outer loop + vertex -32.1659 7.73681 2 + vertex -32.1839 7.66946 10 + vertex -32.1839 7.66946 2 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.1659 7.7368 74.7199 + vertex -32.1659 7.73681 11.6 + vertex -32.1659 7.73681 74.72 + endloop + endfacet + facet normal 0.966082 -0.258234 -4.09117e-08 + outer loop + vertex -32.1839 7.66946 74.72 + vertex -32.1659 7.73681 11.6 + vertex -32.1659 7.7368 74.7199 + endloop + endfacet + facet normal 0.966092 -0.258198 0 + outer loop + vertex -32.1659 7.73681 11.6 + vertex -32.1839 7.66946 74.72 + vertex -32.1839 7.66946 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.3961 7.66946 2 + vertex -31.3961 7.53054 2 + vertex -31.39 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4141 7.73681 2 + vertex -31.3961 7.53054 2 + vertex -31.3961 7.66946 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4141 7.73681 2 + vertex -31.4141 7.46319 2 + vertex -31.3961 7.53054 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4436 7.8 2 + vertex -31.4141 7.46319 2 + vertex -31.4141 7.73681 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4436 7.8 2 + vertex -31.4436 7.4 2 + vertex -31.4141 7.46319 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4836 7.85711 2 + vertex -31.4436 7.4 2 + vertex -31.4436 7.8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4836 7.85711 2 + vertex -31.4836 7.34288 2 + vertex -31.4436 7.4 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.5329 7.90642 2 + vertex -31.4836 7.34288 2 + vertex -31.4836 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.616 7.24147 2 + vertex -31.4836 7.34288 2 + vertex -31.5329 7.90642 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.616 7.24147 2 + vertex -31.5329 7.90642 2 + vertex -31.59 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4836 7.34288 2 + vertex -31.616 7.24147 2 + vertex -31.5329 7.29358 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.5329 7.29358 2 + vertex -31.616 7.24147 2 + vertex -31.59 7.25359 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.6532 7.97588 2 + vertex -31.616 7.24147 2 + vertex -31.59 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.7205 7.99392 2 + vertex -31.616 7.24147 2 + vertex -31.6532 7.97588 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.79 8 2 + vertex -31.616 7.24147 2 + vertex -31.7205 7.99392 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.8595 7.99392 2 + vertex -31.616 7.24147 2 + vertex -31.79 8 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.9268 7.97588 2 + vertex -31.616 7.24147 2 + vertex -31.8595 7.99392 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.9268 7.97588 2 + vertex -31.964 7.24147 2 + vertex -31.616 7.24147 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.99 7.94641 2 + vertex -31.964 7.24147 2 + vertex -31.9268 7.97588 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.0471 7.90642 2 + vertex -31.964 7.24147 2 + vertex -31.99 7.94641 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.0964 7.85711 2 + vertex -31.964 7.24147 2 + vertex -32.0471 7.90642 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.964 7.24147 2 + vertex -32.0471 7.29358 2 + vertex -31.99 7.25359 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.0964 7.34288 2 + vertex -31.964 7.24147 2 + vertex -32.0964 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.964 7.24147 2 + vertex -32.0964 7.34288 2 + vertex -32.0471 7.29358 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.1364 7.8 2 + vertex -32.0964 7.34288 2 + vertex -32.0964 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.1364 7.8 2 + vertex -32.1364 7.4 2 + vertex -32.0964 7.34288 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.1659 7.73681 2 + vertex -32.1364 7.4 2 + vertex -32.1364 7.8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.1659 7.73681 2 + vertex -32.1659 7.46319 2 + vertex -32.1364 7.4 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.1839 7.66946 2 + vertex -32.1659 7.46319 2 + vertex -32.1659 7.73681 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.1839 7.66946 2 + vertex -32.1839 7.53054 2 + vertex -32.1659 7.46319 2 + endloop + endfacet + facet normal 0 -0 1 + outer loop + vertex -32.1839 7.53054 2 + vertex -32.1839 7.66946 2 + vertex -32.19 7.6 2 + endloop + endfacet + facet normal 0.258913 -0.965901 0 + outer loop + vertex -31.9268 7.97588 2 + vertex -31.8595 7.99392 10 + vertex -31.9268 7.97588 10 + endloop + endfacet + facet normal 0.258913 -0.965901 0 + outer loop + vertex -31.8595 7.99392 10 + vertex -31.9268 7.97588 2 + vertex -31.8595 7.99392 2 + endloop + endfacet + facet normal 0.258913 -0.965901 0 + outer loop + vertex -31.9268 7.97588 11.6 + vertex -31.8595 7.99392 74.72 + vertex -31.9268 7.97588 74.72 + endloop + endfacet + facet normal 0.258913 -0.965901 0 + outer loop + vertex -31.8595 7.99392 74.72 + vertex -31.9268 7.97588 11.6 + vertex -31.8595 7.99392 11.6 + endloop + endfacet + facet normal 0.996166 -0.0874836 0 + outer loop + vertex -32.19 7.6 10 + vertex -32.1839 7.66946 2 + vertex -32.1839 7.66946 10 + endloop + endfacet + facet normal 0.996166 -0.0874836 0 + outer loop + vertex -32.19 7.6 2 + vertex -32.1839 7.66946 2 + vertex -32.19 7.6 10 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.1839 7.66945 74.7199 + vertex -32.1839 7.66946 11.6 + vertex -32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0.996165 -0.0874961 -1.38619e-08 + outer loop + vertex -32.19 7.6 74.7199 + vertex -32.1839 7.66946 11.6 + vertex -32.1839 7.66945 74.7199 + endloop + endfacet + facet normal 0.996166 -0.0874836 0 + outer loop + vertex -32.1839 7.66946 11.6 + vertex -32.19 7.6 74.7199 + vertex -32.19 7.6 11.6 + endloop + endfacet + facet normal 0.707178 -0.707035 0 + outer loop + vertex -32.0964 7.85711 10 + vertex -32.0471 7.90642 2 + vertex -32.0471 7.90642 10 + endloop + endfacet + facet normal 0.707178 -0.707035 0 + outer loop + vertex -32.0471 7.90642 2 + vertex -32.0964 7.85711 10 + vertex -32.0964 7.85711 2 + endloop + endfacet + facet normal 0.707178 -0.707035 0 + outer loop + vertex -32.0964 7.85711 74.72 + vertex -32.0471 7.90642 11.6 + vertex -32.0471 7.90642 74.72 + endloop + endfacet + facet normal 0.707178 -0.707035 0 + outer loop + vertex -32.0471 7.90642 11.6 + vertex -32.0964 7.85711 74.72 + vertex -32.0964 7.85711 11.6 + endloop + endfacet + facet normal 0.906121 -0.423019 0 + outer loop + vertex -32.1659 7.73681 10 + vertex -32.1364 7.8 2 + vertex -32.1364 7.8 10 + endloop + endfacet + facet normal 0.906121 -0.423019 0 + outer loop + vertex -32.1364 7.8 2 + vertex -32.1659 7.73681 10 + vertex -32.1659 7.73681 2 + endloop + endfacet + facet normal 0.906121 -0.423019 0 + outer loop + vertex -32.1659 7.73681 74.72 + vertex -32.1364 7.8 11.6 + vertex -32.1364 7.8 74.72 + endloop + endfacet + facet normal 0.906121 -0.423019 0 + outer loop + vertex -32.1364 7.8 11.6 + vertex -32.1659 7.73681 74.72 + vertex -32.1659 7.73681 11.6 + endloop + endfacet + facet normal 0.819077 -0.573684 0 + outer loop + vertex -32.1364 7.8 10 + vertex -32.0964 7.85711 2 + vertex -32.0964 7.85711 10 + endloop + endfacet + facet normal 0.819077 -0.573684 0 + outer loop + vertex -32.0964 7.85711 2 + vertex -32.1364 7.8 10 + vertex -32.1364 7.8 2 + endloop + endfacet + facet normal 0.819077 -0.573684 0 + outer loop + vertex -32.1364 7.8 74.72 + vertex -32.0964 7.85711 11.6 + vertex -32.0964 7.85711 74.72 + endloop + endfacet + facet normal 0.819077 -0.573684 0 + outer loop + vertex -32.0964 7.85711 11.6 + vertex -32.1364 7.8 74.72 + vertex -32.1364 7.8 11.6 + endloop + endfacet + facet normal 0.573655 -0.819097 0 + outer loop + vertex -32.0471 7.90642 2 + vertex -31.99 7.94641 10 + vertex -32.0471 7.90642 10 + endloop + endfacet + facet normal 0.573655 -0.819097 0 + outer loop + vertex -31.99 7.94641 10 + vertex -32.0471 7.90642 2 + vertex -31.99 7.94641 2 + endloop + endfacet + facet normal 0.573655 -0.819097 0 + outer loop + vertex -32.0471 7.90642 11.6 + vertex -31.99 7.94641 74.72 + vertex -32.0471 7.90642 74.72 + endloop + endfacet + facet normal 0.573655 -0.819097 0 + outer loop + vertex -31.99 7.94641 74.72 + vertex -32.0471 7.90642 11.6 + vertex -31.99 7.94641 11.6 + endloop + endfacet + facet normal 0.0857754 0 -0.996314 + outer loop + vertex -31.79 -1.99219 78.72 + vertex -31.8202 4 78.7174 + vertex -31.79 4 78.72 + endloop + endfacet + facet normal 0.0874337 8.41988e-06 -0.99617 + outer loop + vertex -31.8595 -1.99219 78.7139 + vertex -31.8202 4 78.7174 + vertex -31.79 -1.99219 78.72 + endloop + endfacet + facet normal 0.0887074 0 -0.996058 + outer loop + vertex -31.8202 4 78.7174 + vertex -31.8595 -1.99219 78.7139 + vertex -31.8595 4 78.7139 + endloop + endfacet + facet normal 0.0134436 0.999867 -0.00924247 + outer loop + vertex -32.0964 3.99999 78.5771 + vertex -32.1364 4 78.52 + vertex -32.1353 4 78.5216 + endloop + endfacet + facet normal 0.81903 0 -0.573751 + outer loop + vertex -32.0964 -1.99219 78.5771 + vertex -32.1364 4 78.52 + vertex -32.0964 3.99999 78.5771 + endloop + endfacet + facet normal 0.81903 0 -0.573751 + outer loop + vertex -32.1364 4 78.52 + vertex -32.0964 -1.99219 78.5771 + vertex -32.1364 -1.99219 78.52 + endloop + endfacet + facet normal 0.423104 0 -0.906081 + outer loop + vertex -31.9268 -1.99219 78.6959 + vertex -31.9722 4 78.6747 + vertex -31.9268 3.99999 78.6959 + endloop + endfacet + facet normal 0.422964 -1.29208e-06 -0.906147 + outer loop + vertex -31.99 -1.99219 78.6664 + vertex -31.9722 4 78.6747 + vertex -31.9268 -1.99219 78.6959 + endloop + endfacet + facet normal 0.422607 0 -0.906313 + outer loop + vertex -31.9722 4 78.6747 + vertex -31.99 -1.99219 78.6664 + vertex -31.99 4 78.6664 + endloop + endfacet + facet normal 0.259044 0 -0.965865 + outer loop + vertex -31.8595 -1.99219 78.7139 + vertex -31.8856 4 78.7069 + vertex -31.8595 4 78.7139 + endloop + endfacet + facet normal 0.258377 -3.11415e-06 -0.966044 + outer loop + vertex -31.9268 -1.99219 78.6959 + vertex -31.8856 4 78.7069 + vertex -31.8595 -1.99219 78.7139 + endloop + endfacet + facet normal 0.257955 0 -0.966157 + outer loop + vertex -31.8856 4 78.7069 + vertex -31.9268 -1.99219 78.6959 + vertex -31.9268 3.99999 78.6959 + endloop + endfacet + facet normal 0.574094 0 -0.81879 + outer loop + vertex -31.99 -1.99219 78.6664 + vertex -32.0161 4 78.6481 + vertex -31.99 4 78.6664 + endloop + endfacet + facet normal 0.573751 -2.22618e-06 -0.81903 + outer loop + vertex -32.0471 -1.99219 78.6264 + vertex -32.0161 4 78.6481 + vertex -31.99 -1.99219 78.6664 + endloop + endfacet + facet normal 0.573462 0 -0.819232 + outer loop + vertex -32.0161 4 78.6481 + vertex -32.0471 -1.99219 78.6264 + vertex -32.0471 4 78.6264 + endloop + endfacet + facet normal 0.966044 0 -0.258377 + outer loop + vertex -32.1659 -1.99219 78.4568 + vertex -32.1839 3.99999 78.3895 + vertex -32.1659 3.99994 78.4568 + endloop + endfacet + facet normal 0.966044 0 -0.258377 + outer loop + vertex -32.1839 3.99999 78.3895 + vertex -32.1659 -1.99219 78.4568 + vertex -32.1839 -1.99219 78.3895 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -32.1659 -1.99219 78.4568 + vertex -32.1659 3.99994 78.4568 + vertex -32.1659 3.99996 78.4568 + endloop + endfacet + facet normal 0.99617 0 -0.0874337 + outer loop + vertex -32.1839 3.99996 78.3895 + vertex -32.1839 -1.99219 78.3895 + vertex -32.19 3.99996 78.32 + endloop + endfacet + facet normal 0.99617 0 -0.0874337 + outer loop + vertex -32.19 3.99996 78.32 + vertex -32.1839 -1.99219 78.3895 + vertex -32.19 -1.99219 78.32 + endloop + endfacet + facet normal 0 0 -0 + outer loop + vertex -32.1839 3.99996 78.3895 + vertex -32.1839 3.99999 78.3895 + vertex -32.1839 -1.99219 78.3895 + endloop + endfacet + facet normal 0.906147 0 -0.422964 + outer loop + vertex -32.1364 -1.99219 78.52 + vertex -32.1659 3.99996 78.4568 + vertex -32.1364 4 78.52 + endloop + endfacet + facet normal 0.906147 0 -0.422964 + outer loop + vertex -32.1659 3.99996 78.4568 + vertex -32.1364 -1.99219 78.52 + vertex -32.1659 -1.99219 78.4568 + endloop + endfacet + facet normal 0.707107 0 -0.707107 + outer loop + vertex -32.0471 -1.99219 78.6264 + vertex -32.0964 3.99999 78.5771 + vertex -32.0471 3.99999 78.6264 + endloop + endfacet + facet normal 0.707107 0 -0.707107 + outer loop + vertex -32.0964 3.99999 78.5771 + vertex -32.0471 -1.99219 78.6264 + vertex -32.0964 -1.99219 78.5771 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -32.0471 -1.99219 78.6264 + vertex -32.0471 3.99999 78.6264 + vertex -32.0471 4 78.6264 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 3.99994 78.32 + vertex -32.19 3.99996 78.32 + endloop + endfacet + facet normal -0.0871492 -0.996195 0 + outer loop + vertex 31.79 8 11.6 + vertex 31.8595 7.99392 74.72 + vertex 31.79 8 74.72 + endloop + endfacet + facet normal -0.0871492 -0.996195 -0 + outer loop + vertex 31.8595 7.99392 74.72 + vertex 31.79 8 11.6 + vertex 31.8595 7.99392 11.6 + endloop + endfacet + facet normal -0.0871492 -0.996195 0 + outer loop + vertex 31.79 8 2 + vertex 31.8595 7.99392 10 + vertex 31.79 8 10 + endloop + endfacet + facet normal -0.0871492 -0.996195 -0 + outer loop + vertex 31.8595 7.99392 10 + vertex 31.79 8 2 + vertex 31.8595 7.99392 2 + endloop + endfacet + facet normal -0 -0 1 + outer loop + vertex 31.8918 7.98526 74.72 + vertex 31.905 7.98173 74.72 + vertex 31.8595 7.99392 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.8595 7.99392 74.72 + vertex 31.9268 7.97588 74.72 + vertex 31.905 7.98173 74.72 + endloop + endfacet + facet normal -0.258913 -0.965901 0 + outer loop + vertex 31.8595 7.99392 11.6 + vertex 31.9268 7.97588 74.72 + vertex 31.8595 7.99392 74.72 + endloop + endfacet + facet normal -0.258913 -0.965901 -0 + outer loop + vertex 31.9268 7.97588 74.72 + vertex 31.8595 7.99392 11.6 + vertex 31.9268 7.97588 11.6 + endloop + endfacet + facet normal -0 -0 -1 + outer loop + vertex 31.905 7.98173 74.72 + vertex 31.9268 7.97588 74.72 + vertex 31.8918 7.98526 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.905 7.98173 74.72 + vertex 31.8918 7.98526 74.72 + vertex 31.8595 7.99392 74.72 + endloop + endfacet + facet normal -0.258913 -0.965901 0 + outer loop + vertex 31.8595 7.99392 2 + vertex 31.9268 7.97588 10 + vertex 31.8595 7.99392 10 + endloop + endfacet + facet normal -0.258913 -0.965901 -0 + outer loop + vertex 31.9268 7.97588 10 + vertex 31.8595 7.99392 2 + vertex 31.9268 7.97588 2 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.6 2 + vertex 32.19 7.59999 2 + vertex 32.19 7.6 10 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1839 7.53054 2 + vertex 32.19 7.59999 2 + vertex 32.19 7.6 2 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 32.19 7.6 10 + vertex 32.19 7.59999 2 + vertex 32.19 7.59999 10 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 74.7199 + vertex 32.19 7.6 11.6 + vertex 32.19 7.59999 11.6 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.6 11.6 + vertex 32.19 7.59999 74.7199 + vertex 32.19 7.6 74.72 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 32.19 7.6 74.72 + vertex 32.19 7.59999 74.7199 + vertex 32.19 7.59999 74.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1839 7.66946 2 + vertex 32.1839 7.53054 2 + vertex 32.19 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1659 7.73681 2 + vertex 32.1839 7.53054 2 + vertex 32.1839 7.66946 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1659 7.73681 2 + vertex 32.1659 7.46319 2 + vertex 32.1839 7.53054 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1364 7.8 2 + vertex 32.1659 7.46319 2 + vertex 32.1659 7.73681 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1364 7.8 2 + vertex 32.1364 7.4 2 + vertex 32.1659 7.46319 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0964 7.85711 2 + vertex 32.1364 7.4 2 + vertex 32.1364 7.8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0964 7.85711 2 + vertex 32.0964 7.34288 2 + vertex 32.1364 7.4 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0471 7.90642 2 + vertex 32.0964 7.34288 2 + vertex 32.0964 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.964 7.24147 2 + vertex 32.0964 7.34288 2 + vertex 32.0471 7.90642 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.964 7.24147 2 + vertex 32.0471 7.90642 2 + vertex 31.99 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0964 7.34288 2 + vertex 31.964 7.24147 2 + vertex 32.0471 7.29358 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0471 7.29358 2 + vertex 31.964 7.24147 2 + vertex 31.99 7.25359 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.9268 7.97588 2 + vertex 31.964 7.24147 2 + vertex 31.99 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.8595 7.99392 2 + vertex 31.964 7.24147 2 + vertex 31.9268 7.97588 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 2 + vertex 31.964 7.24147 2 + vertex 31.8595 7.99392 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.7205 7.99392 2 + vertex 31.964 7.24147 2 + vertex 31.79 8 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.6532 7.97588 2 + vertex 31.964 7.24147 2 + vertex 31.7205 7.99392 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.6532 7.97588 2 + vertex 31.616 7.24147 2 + vertex 31.964 7.24147 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.59 7.94641 2 + vertex 31.616 7.24147 2 + vertex 31.6532 7.97588 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.5329 7.90642 2 + vertex 31.616 7.24147 2 + vertex 31.59 7.94641 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.4836 7.85711 2 + vertex 31.616 7.24147 2 + vertex 31.5329 7.90642 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.616 7.24147 2 + vertex 31.5329 7.29358 2 + vertex 31.59 7.25359 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.4836 7.34288 2 + vertex 31.616 7.24147 2 + vertex 31.4836 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.616 7.24147 2 + vertex 31.4836 7.34288 2 + vertex 31.5329 7.29358 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.4436 7.8 2 + vertex 31.4836 7.34288 2 + vertex 31.4836 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4436 7.8 2 + vertex 31.4436 7.4 2 + vertex 31.4836 7.34288 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.4141 7.73681 2 + vertex 31.4436 7.4 2 + vertex 31.4436 7.8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4141 7.73681 2 + vertex 31.4141 7.46319 2 + vertex 31.4436 7.4 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.3961 7.66946 2 + vertex 31.4141 7.46319 2 + vertex 31.4141 7.73681 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.3961 7.66946 2 + vertex 31.3961 7.53054 2 + vertex 31.4141 7.46319 2 + endloop + endfacet + facet normal 0 -0 1 + outer loop + vertex 31.3961 7.53054 2 + vertex 31.3961 7.66946 2 + vertex 31.39 7.6 2 + endloop + endfacet + facet normal -0.422611 -0.906311 0 + outer loop + vertex 31.9268 7.97588 2 + vertex 31.99 7.94641 10 + vertex 31.9268 7.97588 10 + endloop + endfacet + facet normal -0.422611 -0.906311 -0 + outer loop + vertex 31.99 7.94641 10 + vertex 31.9268 7.97588 2 + vertex 31.99 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.9268 7.97588 74.72 + vertex 31.99 7.94641 74.72 + vertex 31.9597 7.96055 74.72 + endloop + endfacet + facet normal -0.422611 -0.906311 0 + outer loop + vertex 31.9268 7.97588 11.6 + vertex 31.99 7.94641 74.72 + vertex 31.9268 7.97588 74.72 + endloop + endfacet + facet normal -0.422611 -0.906311 -0 + outer loop + vertex 31.99 7.94641 74.72 + vertex 31.9268 7.97588 11.6 + vertex 31.99 7.94641 11.6 + endloop + endfacet + facet normal -0 -0 1 + outer loop + vertex 31.9597 7.96055 74.72 + vertex 31.99 7.94641 74.72 + vertex 31.9526 7.96387 74.72 + endloop + endfacet + facet normal -0.573655 -0.819097 0 + outer loop + vertex 31.99 7.94641 2 + vertex 32.0471 7.90642 10 + vertex 31.99 7.94641 10 + endloop + endfacet + facet normal -0.573655 -0.819097 -0 + outer loop + vertex 32.0471 7.90642 10 + vertex 31.99 7.94641 2 + vertex 32.0471 7.90642 2 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.99 7.94641 74.72 + vertex 32.0471 7.90642 74.72 + vertex 32.0174 7.92721 74.72 + endloop + endfacet + facet normal -0.573655 -0.819097 0 + outer loop + vertex 31.99 7.94641 11.6 + vertex 32.0471 7.90642 74.72 + vertex 31.99 7.94641 74.72 + endloop + endfacet + facet normal -0.573655 -0.819097 -0 + outer loop + vertex 32.0471 7.90642 74.72 + vertex 31.99 7.94641 11.6 + vertex 32.0471 7.90642 11.6 + endloop + endfacet + facet normal -0 -0 1 + outer loop + vertex 32.0174 7.92721 74.72 + vertex 32.0217 7.92421 74.72 + vertex 31.99 7.94641 74.72 + endloop + endfacet + facet normal -0.707178 -0.707035 0 + outer loop + vertex 32.0964 7.85711 2 + vertex 32.0471 7.90642 10 + vertex 32.0471 7.90642 2 + endloop + endfacet + facet normal -0.707178 -0.707035 0 + outer loop + vertex 32.0471 7.90642 10 + vertex 32.0964 7.85711 2 + vertex 32.0964 7.85711 10 + endloop + endfacet + facet normal -0.70628 -0.707933 0 + outer loop + vertex 32.0471 7.90642 11.6 + vertex 32.0728 7.88078 74.72 + vertex 32.0471 7.90642 74.72 + endloop + endfacet + facet normal -0.707178 -0.707035 7.30479e-07 + outer loop + vertex 32.0964 7.85711 11.6 + vertex 32.0728 7.88078 74.72 + vertex 32.0471 7.90642 11.6 + endloop + endfacet + facet normal -0.708153 -0.706059 0 + outer loop + vertex 32.0728 7.88078 74.72 + vertex 32.0964 7.85711 11.6 + vertex 32.0964 7.85711 74.72 + endloop + endfacet + facet normal -0 -0 -1 + outer loop + vertex 32.0728 7.88078 74.72 + vertex 32.0964 7.85711 74.72 + vertex 32.0702 7.88337 74.72 + endloop + endfacet + facet normal -0.996166 -0.0874836 0 + outer loop + vertex 32.19 7.6 11.6 + vertex 32.1839 7.66946 74.72 + vertex 32.1839 7.66946 11.6 + endloop + endfacet + facet normal -0.996166 -0.0874836 0 + outer loop + vertex 32.1839 7.66946 74.72 + vertex 32.19 7.6 11.6 + vertex 32.19 7.6 74.72 + endloop + endfacet + facet normal -0.996166 -0.0874836 0 + outer loop + vertex 32.19 7.6 2 + vertex 32.1839 7.66946 10 + vertex 32.1839 7.66946 2 + endloop + endfacet + facet normal -0.996166 -0.0874836 0 + outer loop + vertex 32.1839 7.66946 10 + vertex 32.19 7.6 2 + vertex 32.19 7.6 10 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.1659 7.73681 11.6 + vertex 32.1659 7.7368 74.72 + vertex 32.1659 7.73681 74.72 + endloop + endfacet + facet normal -0.965794 -0.259311 -4.10822e-08 + outer loop + vertex 32.1659 7.73681 11.6 + vertex 32.1753 7.70179 74.72 + vertex 32.1659 7.7368 74.72 + endloop + endfacet + facet normal -0.966092 -0.258198 6.20407e-07 + outer loop + vertex 32.1839 7.66946 11.6 + vertex 32.1753 7.70179 74.72 + vertex 32.1659 7.73681 11.6 + endloop + endfacet + facet normal -0.966394 -0.257067 0 + outer loop + vertex 32.1753 7.70179 74.72 + vertex 32.1839 7.66946 11.6 + vertex 32.1839 7.66946 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.1839 7.66946 74.72 + vertex 32.175 7.70273 74.72 + vertex 32.1753 7.70179 74.72 + endloop + endfacet + facet normal -0.966092 -0.258198 0 + outer loop + vertex 32.1839 7.66946 2 + vertex 32.1659 7.73681 10 + vertex 32.1659 7.73681 2 + endloop + endfacet + facet normal -0.966092 -0.258198 0 + outer loop + vertex 32.1659 7.73681 10 + vertex 32.1839 7.66946 2 + vertex 32.1839 7.66946 10 + endloop + endfacet + facet normal -0.819077 -0.573684 0 + outer loop + vertex 32.1364 7.8 11.6 + vertex 32.0964 7.85711 74.72 + vertex 32.0964 7.85711 11.6 + endloop + endfacet + facet normal -0.819077 -0.573684 0 + outer loop + vertex 32.0964 7.85711 74.72 + vertex 32.1364 7.8 11.6 + vertex 32.1364 7.8 74.72 + endloop + endfacet + facet normal -0.819077 -0.573684 0 + outer loop + vertex 32.1364 7.8 2 + vertex 32.0964 7.85711 10 + vertex 32.0964 7.85711 2 + endloop + endfacet + facet normal -0.819077 -0.573684 0 + outer loop + vertex 32.0964 7.85711 10 + vertex 32.1364 7.8 2 + vertex 32.1364 7.8 10 + endloop + endfacet + facet normal -0.905656 -0.424013 0 + outer loop + vertex 32.1364 7.8 11.6 + vertex 32.1506 7.76967 74.72 + vertex 32.1364 7.8 74.72 + endloop + endfacet + facet normal -0.906121 -0.423019 5.82263e-07 + outer loop + vertex 32.1659 7.73681 11.6 + vertex 32.1506 7.76967 74.72 + vertex 32.1364 7.8 11.6 + endloop + endfacet + facet normal -0.906549 -0.4221 0 + outer loop + vertex 32.1506 7.76967 74.72 + vertex 32.1659 7.73681 11.6 + vertex 32.1659 7.73681 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.1364 7.8 74.72 + vertex 32.1506 7.76967 74.72 + vertex 32.1513 7.76812 74.72 + endloop + endfacet + facet normal -0.906121 -0.423019 0 + outer loop + vertex 32.1659 7.73681 2 + vertex 32.1364 7.8 10 + vertex 32.1364 7.8 2 + endloop + endfacet + facet normal -0.906121 -0.423019 0 + outer loop + vertex 32.1364 7.8 10 + vertex 32.1659 7.73681 2 + vertex 32.1659 7.73681 10 + endloop + endfacet + facet normal -0.99617 0 -0.0874337 + outer loop + vertex 32.19 -1.99219 78.32 + vertex 32.1839 4 78.3895 + vertex 32.19 4 78.32 + endloop + endfacet + facet normal -0.99617 -0 -0.0874337 + outer loop + vertex 32.1839 4 78.3895 + vertex 32.19 -1.99219 78.32 + vertex 32.1839 -1.99219 78.3895 + endloop + endfacet + facet normal -0.259973 0 -0.965616 + outer loop + vertex 31.9268 -1.99219 78.6959 + vertex 31.8982 4 78.7036 + vertex 31.9268 3.99999 78.6959 + endloop + endfacet + facet normal -0.258377 8.16866e-06 -0.966044 + outer loop + vertex 31.8595 -1.99219 78.7139 + vertex 31.8982 4 78.7036 + vertex 31.9268 -1.99219 78.6959 + endloop + endfacet + facet normal -0.257196 0 -0.966359 + outer loop + vertex 31.8982 4 78.7036 + vertex 31.8595 -1.99219 78.7139 + vertex 31.8595 4 78.7139 + endloop + endfacet + facet normal -0.0871115 0 -0.996199 + outer loop + vertex 31.8595 -1.99219 78.7139 + vertex 31.8149 4 78.7178 + vertex 31.8595 4 78.7139 + endloop + endfacet + facet normal -0.0874337 -2.41593e-06 -0.99617 + outer loop + vertex 31.79 -1.99219 78.72 + vertex 31.8149 4 78.7178 + vertex 31.8595 -1.99219 78.7139 + endloop + endfacet + facet normal -0.0880106 0 -0.99612 + outer loop + vertex 31.8149 4 78.7178 + vertex 31.79 -1.99219 78.72 + vertex 31.79 4 78.72 + endloop + endfacet + facet normal -0.573751 0 -0.81903 + outer loop + vertex 31.99 -1.99219 78.6664 + vertex 32.0471 4 78.6264 + vertex 32.0471 -1.99219 78.6264 + endloop + endfacet + facet normal -0.573751 0 -0.81903 + outer loop + vertex 32.0471 4 78.6264 + vertex 31.99 -1.99219 78.6664 + vertex 31.99 4 78.6664 + endloop + endfacet + facet normal -0.423584 0 -0.905857 + outer loop + vertex 31.99 -1.99219 78.6664 + vertex 31.9545 4 78.683 + vertex 31.99 4 78.6664 + endloop + endfacet + facet normal -0.422964 4.47443e-06 -0.906147 + outer loop + vertex 31.9268 -1.99219 78.6959 + vertex 31.9545 4 78.683 + vertex 31.99 -1.99219 78.6664 + endloop + endfacet + facet normal -0.422169 0 -0.906517 + outer loop + vertex 31.9545 4 78.683 + vertex 31.9268 -1.99219 78.6959 + vertex 31.9268 3.99999 78.6959 + endloop + endfacet + facet normal -0.81903 0 -0.573751 + outer loop + vertex 32.1364 -1.99219 78.52 + vertex 32.0964 4 78.5771 + vertex 32.1364 4 78.52 + endloop + endfacet + facet normal -0.81903 -0 -0.573751 + outer loop + vertex 32.0964 4 78.5771 + vertex 32.1364 -1.99219 78.52 + vertex 32.0964 -1.99219 78.5771 + endloop + endfacet + facet normal -0.270224 0.95446 -0.126435 + outer loop + vertex 32.1659 3.99998 78.4568 + vertex 32.1364 4 78.52 + vertex 32.1517 4 78.4873 + endloop + endfacet + facet normal -0.906147 0 -0.422964 + outer loop + vertex 32.1659 -1.99219 78.4568 + vertex 32.1364 4 78.52 + vertex 32.1659 3.99998 78.4568 + endloop + endfacet + facet normal -0.906147 -0 -0.422964 + outer loop + vertex 32.1364 4 78.52 + vertex 32.1659 -1.99219 78.4568 + vertex 32.1364 -1.99219 78.52 + endloop + endfacet + facet normal -0.966044 0 -0.258377 + outer loop + vertex 32.1659 -1.99219 78.4568 + vertex 32.1659 3.99997 78.4568 + vertex 32.1839 -1.99219 78.3895 + endloop + endfacet + facet normal -0 0 0 + outer loop + vertex 32.1659 3.99997 78.4568 + vertex 32.1659 -1.99219 78.4568 + vertex 32.1659 3.99998 78.4568 + endloop + endfacet + facet normal -0.965999 0 -0.258547 + outer loop + vertex 32.1839 -1.99219 78.3895 + vertex 32.1748 4 78.4235 + vertex 32.1839 4 78.3895 + endloop + endfacet + facet normal -0.96609 -2.08187e-06 -0.258204 + outer loop + vertex 32.1748 4 78.4235 + vertex 32.1839 -1.99219 78.3895 + vertex 32.1659 3.99997 78.4568 + endloop + endfacet + facet normal -0.707107 0 -0.707107 + outer loop + vertex 32.0964 -1.99219 78.5771 + vertex 32.0471 4 78.6264 + vertex 32.0964 4 78.5771 + endloop + endfacet + facet normal -0.707107 -0 -0.707107 + outer loop + vertex 32.0471 4 78.6264 + vertex 32.0964 -1.99219 78.5771 + vertex 32.0471 -1.99219 78.6264 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.99 7.25359 2 + vertex -32.0546 7.24147 2 + vertex -31.964 7.24147 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.0471 7.29358 2 + vertex -32.0546 7.24147 2 + vertex -31.99 7.25359 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.0964 7.34288 2 + vertex -32.0546 7.24147 2 + vertex -32.0471 7.29358 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.1364 7.4 2 + vertex -32.0546 7.24147 2 + vertex -32.0964 7.34288 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.0546 7.24147 2 + vertex -32.1364 7.4 2 + vertex -32.19 0.0078125 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 0.0078125 2 + vertex -32.1364 7.4 2 + vertex -32.1659 7.46319 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 0.0078125 2 + vertex -32.1659 7.46319 2 + vertex -32.1839 7.53054 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 0.0078125 2 + vertex -32.1839 7.53054 2 + vertex -32.19 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.0546 7.24147 2 + vertex -32.19 0.0078125 2 + vertex -32.0546 0.0078125 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.3961 7.66946 2 + vertex -31.39 7.6 2 + vertex 31.39 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.3961 7.66946 2 + vertex 31.3961 7.66946 2 + vertex 31.4141 7.73681 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4141 7.73681 2 + vertex 31.4141 7.73681 2 + vertex 31.4436 7.8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4436 7.8 2 + vertex 31.4436 7.8 2 + vertex 31.4836 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4836 7.85711 2 + vertex 31.4836 7.85711 2 + vertex 31.5329 7.90642 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.5329 7.90642 2 + vertex 31.5329 7.90642 2 + vertex 31.59 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.59 7.94641 2 + vertex 31.59 7.94641 2 + vertex 31.6532 7.97588 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.6532 7.97588 2 + vertex 31.6532 7.97588 2 + vertex 31.7205 7.99392 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.7205 7.99392 2 + vertex 31.7205 7.99392 2 + vertex 31.79 8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.3961 7.66946 2 + vertex -31.3961 7.66946 2 + vertex -31.39 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4141 7.73681 2 + vertex -31.4141 7.73681 2 + vertex -31.3961 7.66946 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4436 7.8 2 + vertex -31.4436 7.8 2 + vertex -31.4141 7.73681 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4836 7.85711 2 + vertex -31.4836 7.85711 2 + vertex -31.4436 7.8 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.5329 7.90642 2 + vertex -31.5329 7.90642 2 + vertex -31.4836 7.85711 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.59 7.94641 2 + vertex -31.59 7.94641 2 + vertex -31.5329 7.90642 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.6532 7.97588 2 + vertex -31.6532 7.97588 2 + vertex -31.59 7.94641 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.7205 7.99392 2 + vertex -31.7205 7.99392 2 + vertex -31.6532 7.97588 2 + endloop + endfacet + facet normal 0 -0 1 + outer loop + vertex -31.7205 7.99392 2 + vertex 31.79 8 2 + vertex -31.79 8 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.39 7.6 2 + vertex 31.3961 7.53054 2 + vertex 31.39 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.3961 7.53054 2 + vertex 31.3961 7.53054 2 + vertex -31.39 7.6 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.3961 7.53054 2 + vertex -31.3961 7.53054 2 + vertex 31.4141 7.46319 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4141 7.46319 2 + vertex 31.4141 7.46319 2 + vertex -31.3961 7.53054 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4141 7.46319 2 + vertex -31.4141 7.46319 2 + vertex 31.4436 7.4 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4436 7.4 2 + vertex 31.4436 7.4 2 + vertex -31.4141 7.46319 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4436 7.4 2 + vertex -31.4436 7.4 2 + vertex 31.4836 7.34288 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.4836 7.34288 2 + vertex 31.4836 7.34288 2 + vertex -31.4436 7.4 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.4836 7.34288 2 + vertex -31.4836 7.34288 2 + vertex 31.5329 7.29358 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.5329 7.29358 2 + vertex 31.5329 7.29358 2 + vertex -31.4836 7.34288 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.5329 7.29358 2 + vertex -31.5329 7.29358 2 + vertex 31.59 7.25359 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -31.59 7.25359 2 + vertex 31.59 7.25359 2 + vertex -31.5329 7.29358 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.59 7.25359 2 + vertex -31.59 7.25359 2 + vertex 31.616 7.24147 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.616 7.24147 2 + vertex -31.59 7.25359 2 + vertex -31.616 7.24147 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 32.0471 7.29358 2 + vertex 32.0546 7.24147 2 + vertex 32.0964 7.34288 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 31.99 7.25359 2 + vertex 32.0546 7.24147 2 + vertex 32.0471 7.29358 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0546 7.24147 2 + vertex 31.99 7.25359 2 + vertex 31.964 7.24147 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 32.1839 7.53054 2 + vertex 32.19 0.0078125 2 + vertex 32.19 7.59999 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 32.1659 7.46319 2 + vertex 32.19 0.0078125 2 + vertex 32.1839 7.53054 2 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex 32.1364 7.4 2 + vertex 32.19 0.0078125 2 + vertex 32.1659 7.46319 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.0546 7.24147 2 + vertex 32.1364 7.4 2 + vertex 32.0964 7.34288 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.1364 7.4 2 + vertex 32.0546 7.24147 2 + vertex 32.19 0.0078125 2 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.19 0.0078125 2 + vertex 32.0546 7.24147 2 + vertex 32.0546 0.0078125 2 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 0.0078125 10 + vertex -32.19 7.6 2 + vertex -32.19 7.6 10 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 7.6 2 + vertex -32.19 0.0078125 10 + vertex -32.19 0.0078125 2 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 71.12 + vertex -32.19 7.6 74.72 + vertex -32.19 4 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 7.6 74.72 + vertex -32.19 4 71.12 + vertex -32.19 7.6 11.6 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 0.0078125 11.6 + vertex -32.19 4 71.12 + vertex -32.19 3.37487 71.1747 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 11.6 + vertex -32.19 3.37487 71.1747 + vertex -32.19 2.76873 71.3371 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 11.6 + vertex -32.19 2.76873 71.3371 + vertex -32.19 2.2 71.6023 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 11.6 + vertex -32.19 2.2 71.6023 + vertex -32.19 1.68597 71.9622 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 11.6 + vertex -32.19 1.68597 71.9622 + vertex -32.19 1.24224 72.406 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 11.6 + vertex -32.19 1.24224 72.406 + vertex -32.19 0.882309 72.92 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.882309 72.92 + vertex -32.19 0.617107 73.4887 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.617107 73.4887 + vertex -32.19 0.454693 74.0949 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.454693 74.0949 + vertex -32.19 0.400001 74.72 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 4 71.12 + vertex -32.19 0.0078125 11.6 + vertex -32.19 7.6 11.6 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 3.37487 78.2653 + vertex -32.19 4 78.32 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 2.76873 78.1029 + vertex -32.19 3.37487 78.2653 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 2.2 77.8377 + vertex -32.19 2.76873 78.1029 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 1.68597 77.4778 + vertex -32.19 2.2 77.8377 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 1.24224 77.034 + vertex -32.19 1.68597 77.4778 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.882309 76.52 + vertex -32.19 1.24224 77.034 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.617107 75.9513 + vertex -32.19 0.882309 76.52 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.454693 75.3451 + vertex -32.19 0.617107 75.9513 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -32.19 0.400001 74.72 + vertex -32.19 0.454693 75.3451 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 0.882309 72.92 + vertex -32.19 -1.99219 78.32 + vertex -32.19 -1.99219 11.6 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -32.19 3.37487 71.1747 + vertex -32.19 -1.99219 11.6 + vertex -32.19 0.0078125 11.6 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -0.454693 74.0949 + vertex 32.19 -0.400001 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -0.617107 73.4887 + vertex 32.19 -0.454693 74.0949 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -0.882309 72.92 + vertex 32.19 -0.617107 73.4887 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -1.24224 72.406 + vertex 32.19 -0.882309 72.92 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -1.68597 71.9622 + vertex 32.19 -1.24224 72.406 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -1.99219 11.6 + vertex 32.19 -1.99219 71.7478 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -1.99219 71.7478 + vertex 32.19 -1.68597 71.9622 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 7.54531 74.0949 + vertex 32.19 7.59999 74.7199 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 7.38289 73.4887 + vertex 32.19 7.54531 74.0949 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 7.11769 72.92 + vertex 32.19 7.38289 73.4887 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 6.75776 72.406 + vertex 32.19 7.11769 72.92 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 6.31403 71.9622 + vertex 32.19 6.75776 72.406 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 5.8 71.6023 + vertex 32.19 6.31403 71.9622 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 5.23127 71.3371 + vertex 32.19 5.8 71.6023 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 4.62513 71.1747 + vertex 32.19 5.23127 71.3371 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 4 71.12 + vertex 32.19 4.62513 71.1747 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 7.59999 11.6 + vertex 32.19 0.0078125 11.6 + vertex 32.19 4 71.12 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.99219 11.6 + vertex 32.19 4 71.12 + vertex 32.19 0.0078125 11.6 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 4 71.12 + vertex 32.19 -0.400001 74.72 + vertex 32.19 4 78.32 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.454693 75.3451 + vertex 32.19 4 78.32 + vertex 32.19 -0.400001 74.72 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.617107 75.9513 + vertex 32.19 4 78.32 + vertex 32.19 -0.454693 75.3451 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -0.882309 76.52 + vertex 32.19 4 78.32 + vertex 32.19 -0.617107 75.9513 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.24224 77.034 + vertex 32.19 4 78.32 + vertex 32.19 -0.882309 76.52 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.99219 78.32 + vertex 32.19 -1.24224 77.034 + vertex 32.19 -1.68597 77.4778 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.99219 78.32 + vertex 32.19 -1.68597 77.4778 + vertex 32.19 -1.99219 77.6922 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 -1.24224 77.034 + vertex 32.19 -1.99219 78.32 + vertex 32.19 4 78.32 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 32.19 0.0078125 2 + vertex 32.19 7.59999 10 + vertex 32.19 7.59999 2 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 32.19 7.59999 10 + vertex 32.19 0.0078125 2 + vertex 32.19 0.0078125 10 + endloop + endfacet + facet normal -0 0 0 + outer loop + vertex -32.19 3.99994 78.32 + vertex -32.19 -1.99219 78.32 + vertex -32.19 4 78.32 + endloop + endfacet + facet normal 0 0 0 + outer loop + vertex -32.19 7.6 11.6 + vertex -32.19 7.6 74.7199 + vertex -32.19 7.6 74.72 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.79 -1.99219 78.72 + vertex 31.79 4 78.72 + vertex 31.79 -1.99219 78.72 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 31.79 4 78.72 + vertex -31.79 -1.99219 78.72 + vertex -31.79 4 78.72 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex -29.06 1.5748 11.6 + vertex 29.06 1.5748 11.6 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -31.79 8 11.6 + vertex -29.06 1.5748 11.6 + vertex 31.79 8 11.6 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -29.06 1.5748 11.6 + vertex -31.79 8 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -31.79 8 11.6 + vertex -31.8595 7.99392 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -31.8595 7.99392 11.6 + vertex -31.9268 7.97588 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -31.9268 7.97588 11.6 + vertex -31.99 7.94641 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -31.99 7.94641 11.6 + vertex -32.0471 7.90642 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -32.0471 7.90642 11.6 + vertex -32.0964 7.85711 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -32.0964 7.85711 11.6 + vertex -32.1364 7.8 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -32.1364 7.8 11.6 + vertex -32.1659 7.73681 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 7.6 11.6 + vertex -32.1659 7.73681 11.6 + vertex -32.1839 7.66946 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -32.19 0.0078125 11.6 + vertex -29.06 1.5748 11.6 + vertex -32.19 7.6 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex -29.06 1.5748 11.6 + vertex -32.19 0.0078125 11.6 + vertex -29.06 0.0078125 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.19 7.59999 11.6 + vertex 32.19 7.6 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.19 7.6 11.6 + vertex 32.1839 7.66946 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.1839 7.66946 11.6 + vertex 32.1659 7.73681 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.1659 7.73681 11.6 + vertex 32.1364 7.8 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.1364 7.8 11.6 + vertex 32.0964 7.85711 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.0964 7.85711 11.6 + vertex 32.0471 7.90642 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 32.0471 7.90642 11.6 + vertex 31.99 7.94641 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 31.99 7.94641 11.6 + vertex 31.9268 7.97588 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 31.79 8 11.6 + vertex 31.9268 7.97588 11.6 + vertex 31.8595 7.99392 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.19 7.59999 11.6 + vertex 31.79 8 11.6 + vertex 29.06 1.5748 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.19 7.59999 11.6 + vertex 29.06 1.5748 11.6 + vertex 32.19 0.0078125 11.6 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 32.19 0.0078125 11.6 + vertex 29.06 1.5748 11.6 + vertex 29.06 0.0078125 11.6 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.19 0.0078125 10 + vertex -29.06 1.5748 10 + vertex -29.06 0.0078125 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.06 1.5748 10 + vertex -32.19 7.6 10 + vertex -31.79 8 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.79 8 10 + vertex -32.19 7.6 10 + vertex -31.8595 7.99392 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.8595 7.99392 10 + vertex -32.19 7.6 10 + vertex -31.9268 7.97588 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.9268 7.97588 10 + vertex -32.19 7.6 10 + vertex -31.99 7.94641 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -31.99 7.94641 10 + vertex -32.19 7.6 10 + vertex -32.0471 7.90642 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.0471 7.90642 10 + vertex -32.19 7.6 10 + vertex -32.0964 7.85711 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.0964 7.85711 10 + vertex -32.19 7.6 10 + vertex -32.1364 7.8 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.1364 7.8 10 + vertex -32.19 7.6 10 + vertex -32.1659 7.73681 10 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -29.06 1.5748 10 + vertex -32.19 0.0078125 10 + vertex -32.19 7.6 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -32.1659 7.73681 10 + vertex -32.19 7.6 10 + vertex -32.1839 7.66946 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.06 1.5748 10 + vertex 32.19 7.59999 10 + vertex 32.19 0.0078125 10 + endloop + endfacet + facet normal 0 -0 -1 + outer loop + vertex 31.79 8 10 + vertex 32.19 7.59999 10 + vertex 29.06 1.5748 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.19 7.59999 10 + vertex 31.79 8 10 + vertex 32.19 7.6 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.19 7.6 10 + vertex 31.79 8 10 + vertex 32.1839 7.66946 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.1839 7.66946 10 + vertex 31.79 8 10 + vertex 32.1659 7.73681 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.1659 7.73681 10 + vertex 31.79 8 10 + vertex 32.1364 7.8 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.1364 7.8 10 + vertex 31.79 8 10 + vertex 32.0964 7.85711 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.0964 7.85711 10 + vertex 31.79 8 10 + vertex 32.0471 7.90642 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.0471 7.90642 10 + vertex 31.79 8 10 + vertex 31.99 7.94641 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.99 7.94641 10 + vertex 31.79 8 10 + vertex 31.9268 7.97588 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 31.9268 7.97588 10 + vertex 31.79 8 10 + vertex 31.8595 7.99392 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 29.06 1.5748 10 + vertex 32.19 0.0078125 10 + vertex 29.06 0.0078125 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -29.06 1.5748 10 + vertex 31.79 8 10 + vertex 29.06 1.5748 10 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 31.79 8 10 + vertex -29.06 1.5748 10 + vertex -31.79 8 10 + endloop + endfacet + facet normal 0.859274 -7.70345e-08 -0.511516 + outer loop + vertex -32.5378 0.0078125 1.1885 + vertex -33.2453 5.70034 6.86645e-07 + vertex -33.2453 8.23333 3.05176e-07 + endloop + endfacet + facet normal 0.859274 0 -0.511516 + outer loop + vertex -33.2453 5.70034 6.86645e-07 + vertex -32.5378 0.0078125 1.1885 + vertex -33.2453 0.0078125 6.86645e-07 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -33.2453 8.23333 3.05176e-07 + vertex -33.2453 5.70034 6.86645e-07 + vertex -33.2453 5.70034 3.05176e-07 + endloop + endfacet + facet normal 0.859251 0 -0.511555 + outer loop + vertex -32.0546 7.24147 2 + vertex -33.2453 8.23333 3.05176e-07 + vertex -33.2453 8.28333 3.05176e-07 + endloop + endfacet + facet normal 0.859216 0 -0.511612 + outer loop + vertex -32.5378 0.0078125 1.1885 + vertex -32.0546 7.24147 2 + vertex -32.0546 0.0078125 2 + endloop + endfacet + facet normal 0.859249 -8.25944e-06 -0.511558 + outer loop + vertex -32.0546 7.24147 2 + vertex -32.5378 0.0078125 1.1885 + vertex -33.2453 8.23333 3.05176e-07 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 33.2453 5.70034 6.86645e-07 + vertex 33.2453 8.23333 3.05176e-07 + vertex 33.2453 5.70034 3.05176e-07 + endloop + endfacet + facet normal -0.859274 -7.70345e-08 -0.511516 + outer loop + vertex 33.2453 5.70034 6.86645e-07 + vertex 32.5378 0.0078125 1.1885 + vertex 33.2453 8.23333 3.05176e-07 + endloop + endfacet + facet normal -0.859274 0 -0.511516 + outer loop + vertex 32.5378 0.0078125 1.1885 + vertex 33.2453 5.70034 6.86645e-07 + vertex 33.2453 0.0078125 6.86645e-07 + endloop + endfacet + facet normal 0 -0.886879 -0.462002 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -31.616 7.24147 2 + vertex -31.964 7.24147 2 + endloop + endfacet + facet normal 0 -0.886879 -0.462002 + outer loop + vertex -31.616 7.24147 2 + vertex -29.4979 8.28333 6.86645e-07 + vertex 31.616 7.24147 2 + endloop + endfacet + facet normal -0 -0.886879 -0.462002 + outer loop + vertex 29.4979 8.28333 6.86645e-07 + vertex 31.616 7.24147 2 + vertex -29.4979 8.28333 6.86645e-07 + endloop + endfacet + facet normal 0 -0.886879 -0.462002 + outer loop + vertex -33.2453 8.28333 3.05176e-07 + vertex -31.964 7.24147 2 + vertex -32.0546 7.24147 2 + endloop + endfacet + facet normal 4.70298e-08 -0.886879 -0.462002 + outer loop + vertex -31.616 7.24147 2 + vertex -33.2453 8.28333 3.05176e-07 + vertex -29.4979 8.28333 6.86645e-07 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -29.4979 8.28333 6.86645e-07 + vertex -33.2453 8.28333 3.05176e-07 + vertex -29.4979 8.28333 3.05176e-07 + endloop + endfacet + facet normal -4.70298e-08 -0.886879 -0.462002 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 31.616 7.24147 2 + vertex 29.4979 8.28333 6.86645e-07 + endloop + endfacet + facet normal 0 -0.886879 -0.462002 + outer loop + vertex 31.616 7.24147 2 + vertex 33.2453 8.28333 3.05176e-07 + vertex 31.964 7.24147 2 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.2453 8.28333 3.05176e-07 + vertex 29.4979 8.28333 6.86645e-07 + vertex 29.4979 8.28333 3.05176e-07 + endloop + endfacet + facet normal 0 -0.886879 -0.462002 + outer loop + vertex 31.964 7.24147 2 + vertex 33.2453 8.28333 3.05176e-07 + vertex 32.0546 7.24147 2 + endloop + endfacet + facet normal -0.859251 0 -0.511555 + outer loop + vertex 33.2453 8.23333 3.05176e-07 + vertex 32.0546 7.24147 2 + vertex 33.2453 8.28333 3.05176e-07 + endloop + endfacet + facet normal -0.859249 -8.25944e-06 -0.511558 + outer loop + vertex 32.5378 0.0078125 1.1885 + vertex 32.0546 7.24147 2 + vertex 33.2453 8.23333 3.05176e-07 + endloop + endfacet + facet normal -0.859216 -0 -0.511612 + outer loop + vertex 32.0546 7.24147 2 + vertex 32.5378 0.0078125 1.1885 + vertex 32.0546 0.0078125 2 + endloop + endfacet + facet normal -0.996193 0 -0.0871715 + outer loop + vertex 3.125 9.5 43.36 + vertex 3.07752 10 43.9026 + vertex 3.125 10 43.36 + endloop + endfacet + facet normal -0.996193 -0 -0.0871715 + outer loop + vertex 3.07752 10 43.9026 + vertex 3.125 9.5 43.36 + vertex 3.07752 9.5 43.9026 + endloop + endfacet + facet normal 0.996193 0 -0.0871715 + outer loop + vertex -3.07752 9.5 43.9026 + vertex -3.125 10 43.36 + vertex -3.07752 10 43.9026 + endloop + endfacet + facet normal 0.996193 0 -0.0871715 + outer loop + vertex -3.125 10 43.36 + vertex -3.07752 9.5 43.9026 + vertex -3.125 9.5 43.36 + endloop + endfacet + facet normal -0.0872 0 -0.996191 + outer loop + vertex 0 9.5 46.485 + vertex 0.54265 10 46.4375 + vertex 0.54265 9.5 46.4375 + endloop + endfacet + facet normal -0.0872 0 -0.996191 + outer loop + vertex 0.54265 10 46.4375 + vertex 0 9.5 46.485 + vertex 0 10 46.485 + endloop + endfacet + facet normal 0.0872 0 0.996191 + outer loop + vertex -0.54265 10 40.2825 + vertex 0 9.5 40.235 + vertex 0 10 40.235 + endloop + endfacet + facet normal 0.0872 0 0.996191 + outer loop + vertex 0 9.5 40.235 + vertex -0.54265 10 40.2825 + vertex -0.54265 9.5 40.2825 + endloop + endfacet + facet normal 0.819146 -0 0.573586 + outer loop + vertex -2.70633 9.5 41.7975 + vertex -2.39389 10 41.3513 + vertex -2.70633 10 41.7975 + endloop + endfacet + facet normal 0.819146 0 0.573586 + outer loop + vertex -2.39389 10 41.3513 + vertex -2.70633 9.5 41.7975 + vertex -2.39389 9.5 41.3513 + endloop + endfacet + facet normal -0.707125 0 -0.707088 + outer loop + vertex 2.39389 9.5 45.3687 + vertex 2.00871 10 45.7539 + vertex 2.39389 10 45.3687 + endloop + endfacet + facet normal -0.707125 -0 -0.707088 + outer loop + vertex 2.00871 10 45.7539 + vertex 2.39389 9.5 45.3687 + vertex 2.00871 9.5 45.7539 + endloop + endfacet + facet normal 0.573528 0 -0.819186 + outer loop + vertex -2.00871 9.5 45.7539 + vertex -1.5625 10 46.0663 + vertex -1.5625 9.5 46.0663 + endloop + endfacet + facet normal 0.573528 0 -0.819186 + outer loop + vertex -1.5625 10 46.0663 + vertex -2.00871 9.5 45.7539 + vertex -2.00871 10 45.7539 + endloop + endfacet + facet normal 0.258846 0 -0.965919 + outer loop + vertex -1.06881 9.5 46.2965 + vertex -0.54265 10 46.4375 + vertex -0.54265 9.5 46.4375 + endloop + endfacet + facet normal 0.258846 0 -0.965919 + outer loop + vertex -0.54265 10 46.4375 + vertex -1.06881 9.5 46.2965 + vertex -1.06881 10 46.2965 + endloop + endfacet + facet normal -0.819146 0 0.573586 + outer loop + vertex 2.39389 9.5 41.3513 + vertex 2.70633 10 41.7975 + vertex 2.39389 10 41.3513 + endloop + endfacet + facet normal -0.819146 0 0.573586 + outer loop + vertex 2.70633 10 41.7975 + vertex 2.39389 9.5 41.3513 + vertex 2.70633 9.5 41.7975 + endloop + endfacet + facet normal -0.906312 0 -0.422609 + outer loop + vertex 2.93654 9.5 44.4288 + vertex 2.70633 10 44.9225 + vertex 2.93654 10 44.4288 + endloop + endfacet + facet normal -0.906312 -0 -0.422609 + outer loop + vertex 2.70633 10 44.9225 + vertex 2.93654 9.5 44.4288 + vertex 2.70633 9.5 44.9225 + endloop + endfacet + facet normal -0.965933 0 -0.258794 + outer loop + vertex 3.07752 9.5 43.9026 + vertex 2.93654 10 44.4288 + vertex 3.07752 10 43.9026 + endloop + endfacet + facet normal -0.965933 -0 -0.258794 + outer loop + vertex 2.93654 10 44.4288 + vertex 3.07752 9.5 43.9026 + vertex 2.93654 9.5 44.4288 + endloop + endfacet + facet normal -0.819146 0 -0.573586 + outer loop + vertex 2.70633 9.5 44.9225 + vertex 2.39389 10 45.3687 + vertex 2.70633 10 44.9225 + endloop + endfacet + facet normal -0.819146 -0 -0.573586 + outer loop + vertex 2.39389 10 45.3687 + vertex 2.70633 9.5 44.9225 + vertex 2.39389 9.5 45.3687 + endloop + endfacet + facet normal -0.422601 0 -0.906316 + outer loop + vertex 1.06881 9.5 46.2965 + vertex 1.5625 10 46.0663 + vertex 1.5625 9.5 46.0663 + endloop + endfacet + facet normal -0.422601 0 -0.906316 + outer loop + vertex 1.5625 10 46.0663 + vertex 1.06881 9.5 46.2965 + vertex 1.06881 10 46.2965 + endloop + endfacet + facet normal -0.258846 0 -0.965919 + outer loop + vertex 0.54265 9.5 46.4375 + vertex 1.06881 10 46.2965 + vertex 1.06881 9.5 46.2965 + endloop + endfacet + facet normal -0.258846 0 -0.965919 + outer loop + vertex 1.06881 10 46.2965 + vertex 0.54265 9.5 46.4375 + vertex 0.54265 10 46.4375 + endloop + endfacet + facet normal -0.573528 0 -0.819186 + outer loop + vertex 1.5625 9.5 46.0663 + vertex 2.00871 10 45.7539 + vertex 2.00871 9.5 45.7539 + endloop + endfacet + facet normal -0.573528 0 -0.819186 + outer loop + vertex 2.00871 10 45.7539 + vertex 1.5625 9.5 46.0663 + vertex 1.5625 10 46.0663 + endloop + endfacet + facet normal 0.819146 0 -0.573586 + outer loop + vertex -2.39389 9.5 45.3687 + vertex -2.70633 10 44.9225 + vertex -2.39389 10 45.3687 + endloop + endfacet + facet normal 0.819146 0 -0.573586 + outer loop + vertex -2.70633 10 44.9225 + vertex -2.39389 9.5 45.3687 + vertex -2.70633 9.5 44.9225 + endloop + endfacet + facet normal 0.707125 0 -0.707088 + outer loop + vertex -2.00871 9.5 45.7539 + vertex -2.39389 10 45.3687 + vertex -2.00871 10 45.7539 + endloop + endfacet + facet normal 0.707125 0 -0.707088 + outer loop + vertex -2.39389 10 45.3687 + vertex -2.00871 9.5 45.7539 + vertex -2.39389 9.5 45.3687 + endloop + endfacet + facet normal 0.422601 0 -0.906316 + outer loop + vertex -1.5625 9.5 46.0663 + vertex -1.06881 10 46.2965 + vertex -1.06881 9.5 46.2965 + endloop + endfacet + facet normal 0.422601 0 -0.906316 + outer loop + vertex -1.06881 10 46.2965 + vertex -1.5625 9.5 46.0663 + vertex -1.5625 10 46.0663 + endloop + endfacet + facet normal 0.0872 0 -0.996191 + outer loop + vertex -0.54265 9.5 46.4375 + vertex 0 10 46.485 + vertex 0 9.5 46.485 + endloop + endfacet + facet normal 0.0872 0 -0.996191 + outer loop + vertex 0 10 46.485 + vertex -0.54265 9.5 46.4375 + vertex -0.54265 10 46.4375 + endloop + endfacet + facet normal -0.0872 0 0.996191 + outer loop + vertex 0 10 40.235 + vertex 0.54265 9.5 40.2825 + vertex 0.54265 10 40.2825 + endloop + endfacet + facet normal -0.0872 0 0.996191 + outer loop + vertex 0.54265 9.5 40.2825 + vertex 0 10 40.235 + vertex 0 9.5 40.235 + endloop + endfacet + facet normal -0.573528 0 0.819186 + outer loop + vertex 1.5625 10 40.6537 + vertex 2.00871 9.5 40.9661 + vertex 2.00871 10 40.9661 + endloop + endfacet + facet normal -0.573528 0 0.819186 + outer loop + vertex 2.00871 9.5 40.9661 + vertex 1.5625 10 40.6537 + vertex 1.5625 9.5 40.6537 + endloop + endfacet + facet normal 0.707125 -0 0.707088 + outer loop + vertex -2.39389 9.5 41.3513 + vertex -2.00871 10 40.9661 + vertex -2.39389 10 41.3513 + endloop + endfacet + facet normal 0.707125 0 0.707088 + outer loop + vertex -2.00871 10 40.9661 + vertex -2.39389 9.5 41.3513 + vertex -2.00871 9.5 40.9661 + endloop + endfacet + facet normal 0.906312 -0 0.422609 + outer loop + vertex -2.93654 9.5 42.2912 + vertex -2.70633 10 41.7975 + vertex -2.93654 10 42.2912 + endloop + endfacet + facet normal 0.906312 0 0.422609 + outer loop + vertex -2.70633 10 41.7975 + vertex -2.93654 9.5 42.2912 + vertex -2.70633 9.5 41.7975 + endloop + endfacet + facet normal 0.996195 -0 0.0871556 + outer loop + vertex -3.125 9.5 43.36 + vertex -3.07752 10 42.8173 + vertex -3.125 10 43.36 + endloop + endfacet + facet normal 0.996195 0 0.0871556 + outer loop + vertex -3.07752 10 42.8173 + vertex -3.125 9.5 43.36 + vertex -3.07752 9.5 42.8173 + endloop + endfacet + facet normal 0.906312 0 -0.422609 + outer loop + vertex -2.70633 9.5 44.9225 + vertex -2.93654 10 44.4288 + vertex -2.70633 10 44.9225 + endloop + endfacet + facet normal 0.906312 0 -0.422609 + outer loop + vertex -2.93654 10 44.4288 + vertex -2.70633 9.5 44.9225 + vertex -2.93654 9.5 44.4288 + endloop + endfacet + facet normal 0.965933 0 -0.258794 + outer loop + vertex -2.93654 9.5 44.4288 + vertex -3.07752 10 43.9026 + vertex -2.93654 10 44.4288 + endloop + endfacet + facet normal 0.965933 0 -0.258794 + outer loop + vertex -3.07752 10 43.9026 + vertex -2.93654 9.5 44.4288 + vertex -3.07752 9.5 43.9026 + endloop + endfacet + facet normal -0.258846 0 0.965919 + outer loop + vertex 0.54265 10 40.2825 + vertex 1.06881 9.5 40.4235 + vertex 1.06881 10 40.4235 + endloop + endfacet + facet normal -0.258846 0 0.965919 + outer loop + vertex 1.06881 9.5 40.4235 + vertex 0.54265 10 40.2825 + vertex 0.54265 9.5 40.2825 + endloop + endfacet + facet normal -0.707125 0 0.707088 + outer loop + vertex 2.00871 9.5 40.9661 + vertex 2.39389 10 41.3513 + vertex 2.00871 10 40.9661 + endloop + endfacet + facet normal -0.707125 0 0.707088 + outer loop + vertex 2.39389 10 41.3513 + vertex 2.00871 9.5 40.9661 + vertex 2.39389 9.5 41.3513 + endloop + endfacet + facet normal -0.96592 0 0.258839 + outer loop + vertex 2.93654 9.5 42.2912 + vertex 3.07752 10 42.8173 + vertex 2.93654 10 42.2912 + endloop + endfacet + facet normal -0.96592 0 0.258839 + outer loop + vertex 3.07752 10 42.8173 + vertex 2.93654 9.5 42.2912 + vertex 3.07752 9.5 42.8173 + endloop + endfacet + facet normal 0.573528 0 0.819186 + outer loop + vertex -2.00871 10 40.9661 + vertex -1.5625 9.5 40.6537 + vertex -1.5625 10 40.6537 + endloop + endfacet + facet normal 0.573528 0 0.819186 + outer loop + vertex -1.5625 9.5 40.6537 + vertex -2.00871 10 40.9661 + vertex -2.00871 9.5 40.9661 + endloop + endfacet + facet normal 0.422601 0 0.906316 + outer loop + vertex -1.5625 10 40.6537 + vertex -1.06881 9.5 40.4235 + vertex -1.06881 10 40.4235 + endloop + endfacet + facet normal 0.422601 0 0.906316 + outer loop + vertex -1.06881 9.5 40.4235 + vertex -1.5625 10 40.6537 + vertex -1.5625 9.5 40.6537 + endloop + endfacet + facet normal 0.96592 -0 0.258839 + outer loop + vertex -3.07752 9.5 42.8173 + vertex -2.93654 10 42.2912 + vertex -3.07752 10 42.8173 + endloop + endfacet + facet normal 0.96592 0 0.258839 + outer loop + vertex -2.93654 10 42.2912 + vertex -3.07752 9.5 42.8173 + vertex -2.93654 9.5 42.2912 + endloop + endfacet + facet normal -0.422601 0 0.906316 + outer loop + vertex 1.06881 10 40.4235 + vertex 1.5625 9.5 40.6537 + vertex 1.5625 10 40.6537 + endloop + endfacet + facet normal -0.422601 0 0.906316 + outer loop + vertex 1.5625 9.5 40.6537 + vertex 1.06881 10 40.4235 + vertex 1.06881 9.5 40.4235 + endloop + endfacet + facet normal -0.906312 0 0.422609 + outer loop + vertex 2.70633 9.5 41.7975 + vertex 2.93654 10 42.2912 + vertex 2.70633 10 41.7975 + endloop + endfacet + facet normal -0.906312 0 0.422609 + outer loop + vertex 2.93654 10 42.2912 + vertex 2.70633 9.5 41.7975 + vertex 2.93654 9.5 42.2912 + endloop + endfacet + facet normal -0.996195 0 0.0871556 + outer loop + vertex 3.07752 9.5 42.8173 + vertex 3.125 10 43.36 + vertex 3.07752 10 42.8173 + endloop + endfacet + facet normal -0.996195 0 0.0871556 + outer loop + vertex 3.125 10 43.36 + vertex 3.07752 9.5 42.8173 + vertex 3.125 9.5 43.36 + endloop + endfacet + facet normal 0.258846 0 0.965919 + outer loop + vertex -1.06881 10 40.4235 + vertex -0.54265 9.5 40.2825 + vertex -0.54265 10 40.2825 + endloop + endfacet + facet normal 0.258846 0 0.965919 + outer loop + vertex -0.54265 9.5 40.2825 + vertex -1.06881 10 40.4235 + vertex -1.06881 9.5 40.4235 + endloop + endfacet + facet normal -0.806615 0.586848 -0.0705825 + outer loop + vertex 3.07752 9.5 43.9026 + vertex 3.125 9.5 43.36 + vertex 1.25 6.92284 43.36 + endloop + endfacet + facet normal -0.782116 0.586844 -0.209545 + outer loop + vertex 2.93654 9.5 44.4288 + vertex 3.07752 9.5 43.9026 + vertex 1.23101 6.92284 43.5771 + endloop + endfacet + facet normal -0.0706056 0.586847 0.806613 + outer loop + vertex 0 6.92284 42.11 + vertex 0.54265 9.5 40.2825 + vertex 0 9.5 40.235 + endloop + endfacet + facet normal 0.0706056 0.586847 -0.806613 + outer loop + vertex 0 9.5 46.485 + vertex 0 6.92284 44.61 + vertex -0.54265 9.5 46.4375 + endloop + endfacet + facet normal -0.0706056 0.586847 0.806613 + outer loop + vertex 0.21706 6.92284 42.129 + vertex 0.54265 9.5 40.2825 + vertex 0 6.92284 42.11 + endloop + endfacet + facet normal 0.57256 0.586843 -0.57253 + outer loop + vertex -0.803484 6.92284 44.3176 + vertex -2.39389 9.5 45.3687 + vertex -2.00871 9.5 45.7539 + endloop + endfacet + facet normal 0.572596 0.586848 -0.572488 + outer loop + vertex -2.39389 9.5 45.3687 + vertex -0.803484 6.92284 44.3176 + vertex -0.957555 6.92284 44.1635 + endloop + endfacet + facet normal 0.34218 0.586845 -0.733843 + outer loop + vertex -1.06881 9.5 46.2965 + vertex -0.427525 6.92284 44.5346 + vertex -1.5625 9.5 46.0663 + endloop + endfacet + facet normal 0.806615 0.586848 -0.0705825 + outer loop + vertex -3.07752 9.5 43.9026 + vertex -1.25 6.92284 43.36 + vertex -3.125 9.5 43.36 + endloop + endfacet + facet normal 0.0706056 0.586847 -0.806613 + outer loop + vertex 0 6.92284 44.61 + vertex -0.21706 6.92284 44.591 + vertex -0.54265 9.5 46.4375 + endloop + endfacet + facet normal -0.34218 0.586845 -0.733843 + outer loop + vertex 1.06881 9.5 46.2965 + vertex 1.5625 9.5 46.0663 + vertex 0.427525 6.92284 44.5346 + endloop + endfacet + facet normal -0.733836 0.586852 -0.342184 + outer loop + vertex 2.70633 9.5 44.9225 + vertex 2.93654 9.5 44.4288 + vertex 1.17461 6.92284 43.7875 + endloop + endfacet + facet normal -0.663284 0.586851 -0.464392 + outer loop + vertex 2.70633 9.5 44.9225 + vertex 1.08253 6.92284 43.985 + vertex 0.957555 6.92284 44.1635 + endloop + endfacet + facet normal -0.572596 0.586848 0.572488 + outer loop + vertex 2.39389 9.5 41.3513 + vertex 0.803484 6.92284 42.4024 + vertex 0.957555 6.92284 42.5565 + endloop + endfacet + facet normal -0.57256 0.586843 0.57253 + outer loop + vertex 0.803484 6.92284 42.4024 + vertex 2.39389 9.5 41.3513 + vertex 2.00871 9.5 40.9661 + endloop + endfacet + facet normal -0.806615 0.586849 0.0705695 + outer loop + vertex 3.125 9.5 43.36 + vertex 3.07752 9.5 42.8173 + vertex 1.25 6.92284 43.36 + endloop + endfacet + facet normal 0.663259 0.586849 -0.46443 + outer loop + vertex -2.39389 9.5 45.3687 + vertex -1.08253 6.92284 43.985 + vertex -2.70633 9.5 44.9225 + endloop + endfacet + facet normal 0.464386 0.586841 -0.663297 + outer loop + vertex -1.5625 9.5 46.0663 + vertex -0.803484 6.92284 44.3176 + vertex -2.00871 9.5 45.7539 + endloop + endfacet + facet normal 0.806619 0.586846 -0.0705559 + outer loop + vertex -1.23101 6.92284 43.5771 + vertex -1.25 6.92284 43.36 + vertex -3.07752 9.5 43.9026 + endloop + endfacet + facet normal 0.572596 0.586848 0.572488 + outer loop + vertex -0.803484 6.92284 42.4024 + vertex -2.39389 9.5 41.3513 + vertex -0.957555 6.92284 42.5565 + endloop + endfacet + facet normal 0.57256 0.586843 0.57253 + outer loop + vertex -2.39389 9.5 41.3513 + vertex -0.803484 6.92284 42.4024 + vertex -2.00871 9.5 40.9661 + endloop + endfacet + facet normal 0.663259 0.586849 0.46443 + outer loop + vertex -1.08253 6.92284 42.735 + vertex -2.39389 9.5 41.3513 + vertex -2.70633 9.5 41.7975 + endloop + endfacet + facet normal -0.209587 0.586845 -0.782104 + outer loop + vertex 1.06881 9.5 46.2965 + vertex 0.427525 6.92284 44.5346 + vertex 0.21706 6.92284 44.591 + endloop + endfacet + facet normal -0.464382 0.586851 -0.663291 + outer loop + vertex 1.5625 9.5 46.0663 + vertex 2.00871 9.5 45.7539 + vertex 0.625 6.92284 44.4425 + endloop + endfacet + facet normal -0.34224 0.586852 -0.73381 + outer loop + vertex 1.5625 9.5 46.0663 + vertex 0.625 6.92284 44.4425 + vertex 0.427525 6.92284 44.5346 + endloop + endfacet + facet normal 0.209588 0.586845 -0.782103 + outer loop + vertex -0.54265 9.5 46.4375 + vertex -0.21706 6.92284 44.591 + vertex -1.06881 9.5 46.2965 + endloop + endfacet + facet normal -0.663261 0.586846 -0.464431 + outer loop + vertex 2.39389 9.5 45.3687 + vertex 2.70633 9.5 44.9225 + vertex 0.957555 6.92284 44.1635 + endloop + endfacet + facet normal -0.782081 0.586854 -0.209645 + outer loop + vertex 2.93654 9.5 44.4288 + vertex 1.23101 6.92284 43.5771 + vertex 1.17461 6.92284 43.7875 + endloop + endfacet + facet normal -0.209588 0.586845 0.782103 + outer loop + vertex 1.06881 9.5 40.4235 + vertex 0.54265 9.5 40.2825 + vertex 0.21706 6.92284 42.129 + endloop + endfacet + facet normal -0.34218 0.586845 0.733843 + outer loop + vertex 1.5625 9.5 40.6537 + vertex 1.06881 9.5 40.4235 + vertex 0.427525 6.92284 42.1854 + endloop + endfacet + facet normal -0.733836 0.586852 0.342184 + outer loop + vertex 2.93654 9.5 42.2912 + vertex 2.70633 9.5 41.7975 + vertex 1.17461 6.92284 42.9325 + endloop + endfacet + facet normal 0.342242 0.586852 -0.733809 + outer loop + vertex -0.427525 6.92284 44.5346 + vertex -0.624999 6.92284 44.4425 + vertex -1.5625 9.5 46.0663 + endloop + endfacet + facet normal 0.464228 0.586859 -0.663392 + outer loop + vertex -0.624999 6.92284 44.4425 + vertex -0.803484 6.92284 44.3176 + vertex -1.5625 9.5 46.0663 + endloop + endfacet + facet normal 0.663288 0.586845 -0.464395 + outer loop + vertex -0.957555 6.92284 44.1635 + vertex -1.08253 6.92284 43.985 + vertex -2.39389 9.5 45.3687 + endloop + endfacet + facet normal 0.782116 0.586844 -0.209545 + outer loop + vertex -2.93654 9.5 44.4288 + vertex -1.23101 6.92284 43.5771 + vertex -3.07752 9.5 43.9026 + endloop + endfacet + facet normal 0.464382 0.586851 0.663291 + outer loop + vertex -0.625 6.92284 42.2775 + vertex -1.5625 9.5 40.6537 + vertex -2.00871 9.5 40.9661 + endloop + endfacet + facet normal 0.0706056 0.586847 0.806613 + outer loop + vertex -0.21706 6.92284 42.129 + vertex 0 6.92284 42.11 + vertex -0.54265 9.5 40.2825 + endloop + endfacet + facet normal 0.34224 0.586852 0.73381 + outer loop + vertex -0.625 6.92284 42.2775 + vertex -0.427525 6.92284 42.1854 + vertex -1.5625 9.5 40.6537 + endloop + endfacet + facet normal 0.663288 0.586845 0.464395 + outer loop + vertex -1.08253 6.92284 42.735 + vertex -0.957555 6.92284 42.5565 + vertex -2.39389 9.5 41.3513 + endloop + endfacet + facet normal 0.806615 0.586849 0.0705695 + outer loop + vertex -1.25 6.92284 43.36 + vertex -3.07752 9.5 42.8173 + vertex -3.125 9.5 43.36 + endloop + endfacet + facet normal 0.733836 0.586852 0.342184 + outer loop + vertex -1.17461 6.92284 42.9325 + vertex -2.70633 9.5 41.7975 + vertex -2.93654 9.5 42.2912 + endloop + endfacet + facet normal 0.806617 0.586848 0.0705558 + outer loop + vertex -1.25 6.92284 43.36 + vertex -1.23101 6.92284 43.1429 + vertex -3.07752 9.5 42.8173 + endloop + endfacet + facet normal -0.0706056 0.586847 -0.806613 + outer loop + vertex 0 9.5 46.485 + vertex 0.54265 9.5 46.4375 + vertex 0 6.92284 44.61 + endloop + endfacet + facet normal -0.806619 0.586846 -0.0705559 + outer loop + vertex 3.07752 9.5 43.9026 + vertex 1.25 6.92284 43.36 + vertex 1.23101 6.92284 43.5771 + endloop + endfacet + facet normal -0.733858 0.586847 -0.342145 + outer loop + vertex 2.70633 9.5 44.9225 + vertex 1.17461 6.92284 43.7875 + vertex 1.08253 6.92284 43.985 + endloop + endfacet + facet normal -0.464386 0.586841 0.663297 + outer loop + vertex 2.00871 9.5 40.9661 + vertex 1.5625 9.5 40.6537 + vertex 0.803484 6.92284 42.4024 + endloop + endfacet + facet normal -0.663261 0.586846 0.464431 + outer loop + vertex 2.70633 9.5 41.7975 + vertex 2.39389 9.5 41.3513 + vertex 0.957555 6.92284 42.5565 + endloop + endfacet + facet normal -0.782081 0.586854 0.209645 + outer loop + vertex 1.23101 6.92284 43.1429 + vertex 2.93654 9.5 42.2912 + vertex 1.17461 6.92284 42.9325 + endloop + endfacet + facet normal -0.782103 0.586848 0.209582 + outer loop + vertex 3.07752 9.5 42.8173 + vertex 2.93654 9.5 42.2912 + vertex 1.23101 6.92284 43.1429 + endloop + endfacet + facet normal 0.209587 0.586845 -0.782104 + outer loop + vertex -0.21706 6.92284 44.591 + vertex -0.427525 6.92284 44.5346 + vertex -1.06881 9.5 46.2965 + endloop + endfacet + facet normal 0.782081 0.586854 -0.209645 + outer loop + vertex -1.17461 6.92284 43.7875 + vertex -1.23101 6.92284 43.5771 + vertex -2.93654 9.5 44.4288 + endloop + endfacet + facet normal 0.209588 0.586845 0.782103 + outer loop + vertex -0.21706 6.92284 42.129 + vertex -0.54265 9.5 40.2825 + vertex -1.06881 9.5 40.4235 + endloop + endfacet + facet normal 0.46424 0.586834 0.663406 + outer loop + vertex -0.803484 6.92284 42.4024 + vertex -0.625 6.92284 42.2775 + vertex -2.00871 9.5 40.9661 + endloop + endfacet + facet normal 0.0706056 0.586847 0.806613 + outer loop + vertex 0 6.92284 42.11 + vertex 0 9.5 40.235 + vertex -0.54265 9.5 40.2825 + endloop + endfacet + facet normal 0.733858 0.586847 0.342145 + outer loop + vertex -1.17461 6.92284 42.9325 + vertex -1.08253 6.92284 42.735 + vertex -2.70633 9.5 41.7975 + endloop + endfacet + facet normal 0.782103 0.586848 0.209582 + outer loop + vertex -1.23101 6.92284 43.1429 + vertex -2.93654 9.5 42.2912 + vertex -3.07752 9.5 42.8173 + endloop + endfacet + facet normal 0.782081 0.586854 0.209645 + outer loop + vertex -1.23101 6.92284 43.1429 + vertex -1.17461 6.92284 42.9325 + vertex -2.93654 9.5 42.2912 + endloop + endfacet + facet normal -0.209588 0.586845 -0.782103 + outer loop + vertex 0.54265 9.5 46.4375 + vertex 1.06881 9.5 46.2965 + vertex 0.21706 6.92284 44.591 + endloop + endfacet + facet normal -0.0706056 0.586847 -0.806613 + outer loop + vertex 0.54265 9.5 46.4375 + vertex 0.21706 6.92284 44.591 + vertex 0 6.92284 44.61 + endloop + endfacet + facet normal -0.57256 0.586843 -0.57253 + outer loop + vertex 2.39389 9.5 45.3687 + vertex 0.803484 6.92284 44.3176 + vertex 2.00871 9.5 45.7539 + endloop + endfacet + facet normal -0.572596 0.586848 -0.572488 + outer loop + vertex 0.803484 6.92284 44.3176 + vertex 2.39389 9.5 45.3687 + vertex 0.957555 6.92284 44.1635 + endloop + endfacet + facet normal -0.46424 0.586834 -0.663406 + outer loop + vertex 2.00871 9.5 45.7539 + vertex 0.803484 6.92284 44.3176 + vertex 0.625 6.92284 44.4425 + endloop + endfacet + facet normal -0.342242 0.586852 0.733809 + outer loop + vertex 0.624999 6.92284 42.2775 + vertex 1.5625 9.5 40.6537 + vertex 0.427525 6.92284 42.1854 + endloop + endfacet + facet normal -0.464228 0.586859 0.663392 + outer loop + vertex 0.803484 6.92284 42.4024 + vertex 1.5625 9.5 40.6537 + vertex 0.624999 6.92284 42.2775 + endloop + endfacet + facet normal -0.663284 0.586851 0.464392 + outer loop + vertex 1.08253 6.92284 42.735 + vertex 2.70633 9.5 41.7975 + vertex 0.957555 6.92284 42.5565 + endloop + endfacet + facet normal -0.733858 0.586847 0.342145 + outer loop + vertex 1.17461 6.92284 42.9325 + vertex 2.70633 9.5 41.7975 + vertex 1.08253 6.92284 42.735 + endloop + endfacet + facet normal -0.806617 0.586848 0.0705558 + outer loop + vertex 1.25 6.92284 43.36 + vertex 3.07752 9.5 42.8173 + vertex 1.23101 6.92284 43.1429 + endloop + endfacet + facet normal 0.733858 0.586847 -0.342145 + outer loop + vertex -1.08253 6.92284 43.985 + vertex -1.17461 6.92284 43.7875 + vertex -2.70633 9.5 44.9225 + endloop + endfacet + facet normal 0.733836 0.586852 -0.342184 + outer loop + vertex -2.70633 9.5 44.9225 + vertex -1.17461 6.92284 43.7875 + vertex -2.93654 9.5 44.4288 + endloop + endfacet + facet normal -0.209587 0.586845 0.782104 + outer loop + vertex 0.427525 6.92284 42.1854 + vertex 1.06881 9.5 40.4235 + vertex 0.21706 6.92284 42.129 + endloop + endfacet + facet normal 0.209587 0.586845 0.782104 + outer loop + vertex -0.427525 6.92284 42.1854 + vertex -0.21706 6.92284 42.129 + vertex -1.06881 9.5 40.4235 + endloop + endfacet + facet normal 0.34218 0.586845 0.733843 + outer loop + vertex -0.427525 6.92284 42.1854 + vertex -1.06881 9.5 40.4235 + vertex -1.5625 9.5 40.6537 + endloop + endfacet + facet normal -0.996196 0 -0.0871385 + outer loop + vertex 1.25 0.0999994 43.36 + vertex 1.23101 6.92284 43.5771 + vertex 1.25 6.92284 43.36 + endloop + endfacet + facet normal -0.996196 -0 -0.0871385 + outer loop + vertex 1.23101 6.92284 43.5771 + vertex 1.25 0.0999994 43.36 + vertex 1.23101 0.0999994 43.5771 + endloop + endfacet + facet normal 0.996196 0 -0.0871385 + outer loop + vertex -1.23101 0.0999994 43.5771 + vertex -1.25 6.92284 43.36 + vertex -1.23101 6.92284 43.5771 + endloop + endfacet + facet normal 0.996196 0 -0.0871385 + outer loop + vertex -1.25 6.92284 43.36 + vertex -1.23101 0.0999994 43.5771 + vertex -1.25 0.0999994 43.36 + endloop + endfacet + facet normal -0.0872 0 -0.996191 + outer loop + vertex 0 0.0999994 44.61 + vertex 0.21706 6.92284 44.591 + vertex 0.21706 0.0999994 44.591 + endloop + endfacet + facet normal -0.0872 0 -0.996191 + outer loop + vertex 0.21706 6.92284 44.591 + vertex 0 0.0999994 44.61 + vertex 0 6.92284 44.61 + endloop + endfacet + facet normal 0.0872 0 0.996191 + outer loop + vertex -0.21706 6.92284 42.129 + vertex 0 0.0999994 42.11 + vertex 0 6.92284 42.11 + endloop + endfacet + facet normal 0.0872 0 0.996191 + outer loop + vertex 0 0.0999994 42.11 + vertex -0.21706 6.92284 42.129 + vertex -0.21706 0.0999994 42.129 + endloop + endfacet + facet normal -0.707173 0 -0.70704 + outer loop + vertex 0.957555 0.0999994 44.1635 + vertex 0.803484 6.92284 44.3176 + vertex 0.957555 6.92284 44.1635 + endloop + endfacet + facet normal -0.707173 -0 -0.70704 + outer loop + vertex 0.803484 6.92284 44.3176 + vertex 0.957555 0.0999994 44.1635 + vertex 0.803484 0.0999994 44.3176 + endloop + endfacet + facet normal 0.573341 0 -0.819317 + outer loop + vertex -0.803484 0.0999994 44.3176 + vertex -0.624999 6.92284 44.4425 + vertex -0.624999 0.0999994 44.4425 + endloop + endfacet + facet normal 0.573341 0 -0.819317 + outer loop + vertex -0.624999 6.92284 44.4425 + vertex -0.803484 0.0999994 44.3176 + vertex -0.803484 6.92284 44.3176 + endloop + endfacet + facet normal 0.258845 0 -0.965919 + outer loop + vertex -0.427525 0.0999994 44.5346 + vertex -0.21706 6.92284 44.591 + vertex -0.21706 0.0999994 44.591 + endloop + endfacet + facet normal 0.258845 0 -0.965919 + outer loop + vertex -0.21706 6.92284 44.591 + vertex -0.427525 0.0999994 44.5346 + vertex -0.427525 6.92284 44.5346 + endloop + endfacet + facet normal -0.707173 0 0.70704 + outer loop + vertex 0.803484 6.92284 42.4024 + vertex 0.957555 0.0999994 42.5565 + vertex 0.957555 6.92284 42.5565 + endloop + endfacet + facet normal -0.707173 0 0.70704 + outer loop + vertex 0.957555 0.0999994 42.5565 + vertex 0.803484 6.92284 42.4024 + vertex 0.803484 0.0999994 42.4024 + endloop + endfacet + facet normal -0.819178 0 0.573539 + outer loop + vertex 0.957555 0.0999994 42.5565 + vertex 1.08253 6.92284 42.735 + vertex 0.957555 6.92284 42.5565 + endloop + endfacet + facet normal -0.819178 0 0.573539 + outer loop + vertex 1.08253 6.92284 42.735 + vertex 0.957555 0.0999994 42.5565 + vertex 1.08253 0.0999994 42.735 + endloop + endfacet + facet normal -0.906335 0 -0.422559 + outer loop + vertex 1.17461 0.0999994 43.7875 + vertex 1.08253 6.92284 43.985 + vertex 1.17461 6.92284 43.7875 + endloop + endfacet + facet normal -0.906335 -0 -0.422559 + outer loop + vertex 1.08253 6.92284 43.985 + vertex 1.17461 0.0999994 43.7875 + vertex 1.08253 0.0999994 43.985 + endloop + endfacet + facet normal -0.965899 0 -0.25892 + outer loop + vertex 1.23101 0.0999994 43.5771 + vertex 1.17461 6.92284 43.7875 + vertex 1.23101 6.92284 43.5771 + endloop + endfacet + facet normal -0.965899 -0 -0.25892 + outer loop + vertex 1.17461 6.92284 43.7875 + vertex 1.23101 0.0999994 43.5771 + vertex 1.17461 0.0999994 43.7875 + endloop + endfacet + facet normal -0.819178 0 -0.573539 + outer loop + vertex 1.08253 0.0999994 43.985 + vertex 0.957555 6.92284 44.1635 + vertex 1.08253 6.92284 43.985 + endloop + endfacet + facet normal -0.819178 -0 -0.573539 + outer loop + vertex 0.957555 6.92284 44.1635 + vertex 1.08253 0.0999994 43.985 + vertex 0.957555 0.0999994 44.1635 + endloop + endfacet + facet normal -0.422678 0 -0.90628 + outer loop + vertex 0.427525 0.0999994 44.5346 + vertex 0.625 6.92284 44.4425 + vertex 0.625 0.0999994 44.4425 + endloop + endfacet + facet normal -0.422678 0 -0.90628 + outer loop + vertex 0.625 6.92284 44.4425 + vertex 0.427525 0.0999994 44.5346 + vertex 0.427525 6.92284 44.5346 + endloop + endfacet + facet normal -0.258845 0 -0.965919 + outer loop + vertex 0.21706 0.0999994 44.591 + vertex 0.427525 6.92284 44.5346 + vertex 0.427525 0.0999994 44.5346 + endloop + endfacet + facet normal -0.258845 0 -0.965919 + outer loop + vertex 0.427525 6.92284 44.5346 + vertex 0.21706 0.0999994 44.591 + vertex 0.21706 6.92284 44.591 + endloop + endfacet + facet normal -0.573343 0 -0.819316 + outer loop + vertex 0.625 0.0999994 44.4425 + vertex 0.803484 6.92284 44.3176 + vertex 0.803484 0.0999994 44.3176 + endloop + endfacet + facet normal -0.573343 0 -0.819316 + outer loop + vertex 0.803484 6.92284 44.3176 + vertex 0.625 0.0999994 44.4425 + vertex 0.625 6.92284 44.4425 + endloop + endfacet + facet normal 0.819178 0 -0.573539 + outer loop + vertex -0.957555 0.0999994 44.1635 + vertex -1.08253 6.92284 43.985 + vertex -0.957555 6.92284 44.1635 + endloop + endfacet + facet normal 0.819178 0 -0.573539 + outer loop + vertex -1.08253 6.92284 43.985 + vertex -0.957555 0.0999994 44.1635 + vertex -1.08253 0.0999994 43.985 + endloop + endfacet + facet normal 0.707173 0 -0.70704 + outer loop + vertex -0.803484 0.0999994 44.3176 + vertex -0.957555 6.92284 44.1635 + vertex -0.803484 6.92284 44.3176 + endloop + endfacet + facet normal 0.707173 0 -0.70704 + outer loop + vertex -0.957555 6.92284 44.1635 + vertex -0.803484 0.0999994 44.3176 + vertex -0.957555 0.0999994 44.1635 + endloop + endfacet + facet normal 0.42268 0 -0.906279 + outer loop + vertex -0.624999 0.0999994 44.4425 + vertex -0.427525 6.92284 44.5346 + vertex -0.427525 0.0999994 44.5346 + endloop + endfacet + facet normal 0.42268 0 -0.906279 + outer loop + vertex -0.427525 6.92284 44.5346 + vertex -0.624999 0.0999994 44.4425 + vertex -0.624999 6.92284 44.4425 + endloop + endfacet + facet normal 0.0872 0 -0.996191 + outer loop + vertex -0.21706 0.0999994 44.591 + vertex 0 6.92284 44.61 + vertex 0 0.0999994 44.61 + endloop + endfacet + facet normal 0.0872 0 -0.996191 + outer loop + vertex 0 6.92284 44.61 + vertex -0.21706 0.0999994 44.591 + vertex -0.21706 6.92284 44.591 + endloop + endfacet + facet normal -0.258845 0 0.965919 + outer loop + vertex 0.21706 6.92284 42.129 + vertex 0.427525 0.0999994 42.1854 + vertex 0.427525 6.92284 42.1854 + endloop + endfacet + facet normal -0.258845 0 0.965919 + outer loop + vertex 0.427525 0.0999994 42.1854 + vertex 0.21706 6.92284 42.129 + vertex 0.21706 0.0999994 42.129 + endloop + endfacet + facet normal -0.0872 0 0.996191 + outer loop + vertex 0 6.92284 42.11 + vertex 0.21706 0.0999994 42.129 + vertex 0.21706 6.92284 42.129 + endloop + endfacet + facet normal -0.0872 0 0.996191 + outer loop + vertex 0.21706 0.0999994 42.129 + vertex 0 6.92284 42.11 + vertex 0 0.0999994 42.11 + endloop + endfacet + facet normal -0.573341 0 0.819317 + outer loop + vertex 0.624999 6.92284 42.2775 + vertex 0.803484 0.0999994 42.4024 + vertex 0.803484 6.92284 42.4024 + endloop + endfacet + facet normal -0.573341 0 0.819317 + outer loop + vertex 0.803484 0.0999994 42.4024 + vertex 0.624999 6.92284 42.2775 + vertex 0.624999 0.0999994 42.2775 + endloop + endfacet + facet normal 0.707173 0 0.70704 + outer loop + vertex -0.957555 6.92284 42.5565 + vertex -0.803484 0.0999994 42.4024 + vertex -0.803484 6.92284 42.4024 + endloop + endfacet + facet normal 0.707173 0 0.70704 + outer loop + vertex -0.803484 0.0999994 42.4024 + vertex -0.957555 6.92284 42.5565 + vertex -0.957555 0.0999994 42.5565 + endloop + endfacet + facet normal 0.422678 0 0.90628 + outer loop + vertex -0.625 6.92284 42.2775 + vertex -0.427525 0.0999994 42.1854 + vertex -0.427525 6.92284 42.1854 + endloop + endfacet + facet normal 0.422678 0 0.90628 + outer loop + vertex -0.427525 0.0999994 42.1854 + vertex -0.625 6.92284 42.2775 + vertex -0.625 0.0999994 42.2775 + endloop + endfacet + facet normal 0.906335 -0 0.422559 + outer loop + vertex -1.17461 0.0999994 42.9325 + vertex -1.08253 6.92284 42.735 + vertex -1.17461 6.92284 42.9325 + endloop + endfacet + facet normal 0.906335 0 0.422559 + outer loop + vertex -1.08253 6.92284 42.735 + vertex -1.17461 0.0999994 42.9325 + vertex -1.08253 0.0999994 42.735 + endloop + endfacet + facet normal 0.819178 -0 0.573539 + outer loop + vertex -1.08253 0.0999994 42.735 + vertex -0.957555 6.92284 42.5565 + vertex -1.08253 6.92284 42.735 + endloop + endfacet + facet normal 0.819178 0 0.573539 + outer loop + vertex -0.957555 6.92284 42.5565 + vertex -1.08253 0.0999994 42.735 + vertex -0.957555 0.0999994 42.5565 + endloop + endfacet + facet normal 0.996196 -0 0.0871385 + outer loop + vertex -1.25 0.0999994 43.36 + vertex -1.23101 6.92284 43.1429 + vertex -1.25 6.92284 43.36 + endloop + endfacet + facet normal 0.996196 0 0.0871385 + outer loop + vertex -1.23101 6.92284 43.1429 + vertex -1.25 0.0999994 43.36 + vertex -1.23101 0.0999994 43.1429 + endloop + endfacet + facet normal 0.965899 0 -0.25892 + outer loop + vertex -1.17461 0.0999994 43.7875 + vertex -1.23101 6.92284 43.5771 + vertex -1.17461 6.92284 43.7875 + endloop + endfacet + facet normal 0.965899 0 -0.25892 + outer loop + vertex -1.23101 6.92284 43.5771 + vertex -1.17461 0.0999994 43.7875 + vertex -1.23101 0.0999994 43.5771 + endloop + endfacet + facet normal -0.965899 0 0.25892 + outer loop + vertex 1.17461 0.0999994 42.9325 + vertex 1.23101 6.92284 43.1429 + vertex 1.17461 6.92284 42.9325 + endloop + endfacet + facet normal -0.965899 0 0.25892 + outer loop + vertex 1.23101 6.92284 43.1429 + vertex 1.17461 0.0999994 42.9325 + vertex 1.23101 0.0999994 43.1429 + endloop + endfacet + facet normal -0.996196 0 0.0871385 + outer loop + vertex 1.23101 0.0999994 43.1429 + vertex 1.25 6.92284 43.36 + vertex 1.23101 6.92284 43.1429 + endloop + endfacet + facet normal -0.996196 0 0.0871385 + outer loop + vertex 1.25 6.92284 43.36 + vertex 1.23101 0.0999994 43.1429 + vertex 1.25 0.0999994 43.36 + endloop + endfacet + facet normal 0.573343 0 0.819316 + outer loop + vertex -0.803484 6.92284 42.4024 + vertex -0.625 0.0999994 42.2775 + vertex -0.625 6.92284 42.2775 + endloop + endfacet + facet normal 0.573343 0 0.819316 + outer loop + vertex -0.625 0.0999994 42.2775 + vertex -0.803484 6.92284 42.4024 + vertex -0.803484 0.0999994 42.4024 + endloop + endfacet + facet normal 0.965899 -0 0.25892 + outer loop + vertex -1.23101 0.0999994 43.1429 + vertex -1.17461 6.92284 42.9325 + vertex -1.23101 6.92284 43.1429 + endloop + endfacet + facet normal 0.965899 0 0.25892 + outer loop + vertex -1.17461 6.92284 42.9325 + vertex -1.23101 0.0999994 43.1429 + vertex -1.17461 0.0999994 42.9325 + endloop + endfacet + facet normal 0.906335 0 -0.422559 + outer loop + vertex -1.08253 0.0999994 43.985 + vertex -1.17461 6.92284 43.7875 + vertex -1.08253 6.92284 43.985 + endloop + endfacet + facet normal 0.906335 0 -0.422559 + outer loop + vertex -1.17461 6.92284 43.7875 + vertex -1.08253 0.0999994 43.985 + vertex -1.17461 0.0999994 43.7875 + endloop + endfacet + facet normal -0.42268 0 0.906279 + outer loop + vertex 0.427525 6.92284 42.1854 + vertex 0.624999 0.0999994 42.2775 + vertex 0.624999 6.92284 42.2775 + endloop + endfacet + facet normal -0.42268 0 0.906279 + outer loop + vertex 0.624999 0.0999994 42.2775 + vertex 0.427525 6.92284 42.1854 + vertex 0.427525 0.0999994 42.1854 + endloop + endfacet + facet normal -0.906335 0 0.422559 + outer loop + vertex 1.08253 0.0999994 42.735 + vertex 1.17461 6.92284 42.9325 + vertex 1.08253 6.92284 42.735 + endloop + endfacet + facet normal -0.906335 0 0.422559 + outer loop + vertex 1.17461 6.92284 42.9325 + vertex 1.08253 0.0999994 42.735 + vertex 1.17461 0.0999994 42.9325 + endloop + endfacet + facet normal 0.258845 0 0.965919 + outer loop + vertex -0.427525 6.92284 42.1854 + vertex -0.21706 0.0999994 42.129 + vertex -0.21706 6.92284 42.129 + endloop + endfacet + facet normal 0.258845 0 0.965919 + outer loop + vertex -0.21706 0.0999994 42.129 + vertex -0.427525 6.92284 42.1854 + vertex -0.427525 0.0999994 42.1854 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex -29.06 0.0078125 11.6 + vertex -29.06 1.5748 10 + vertex -29.06 1.5748 11.6 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex -29.06 1.5748 10 + vertex -29.06 0.0078125 11.6 + vertex -29.06 0.0078125 10 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex 29.06 0.0078125 10 + vertex 29.06 1.5748 11.6 + vertex 29.06 1.5748 10 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex 29.06 1.5748 11.6 + vertex 29.06 0.0078125 10 + vertex 29.06 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 1.5748 45.485 + vertex 0 1.5748 46.235 + vertex -0.499238 1.5748 46.1913 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 1.5748 45.485 + vertex -0.499238 1.5748 46.1913 + vertex -0.983308 1.5748 46.0616 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 1.5748 46.235 + vertex 0 1.5748 45.485 + vertex 0.499238 1.5748 46.1913 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 1.5748 45.2003 + vertex -0.983308 1.5748 46.0616 + vertex -1.4375 1.5748 45.8498 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 0.499238 1.5748 46.1913 + vertex 0 1.5748 45.485 + vertex 0.983308 1.5748 46.0616 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 1.5748 45.2003 + vertex -1.4375 1.5748 45.8498 + vertex -1.84801 1.5748 45.5624 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.0625 1.5748 45.2003 + vertex 0.983308 1.5748 46.0616 + vertex 0 1.5748 45.485 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.983308 1.5748 46.0616 + vertex 1.0625 1.5748 45.2003 + vertex 1.4375 1.5748 45.8498 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 1.5748 45.2003 + vertex -1.84801 1.5748 45.5624 + vertex -2.20238 1.5748 45.208 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -0.983308 1.5748 46.0616 + vertex -1.0625 1.5748 45.2003 + vertex 0 1.5748 45.485 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.8403 1.5748 44.4225 + vertex -2.20238 1.5748 45.208 + vertex -2.48982 1.5748 44.7975 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.20238 1.5748 45.208 + vertex -1.8403 1.5748 44.4225 + vertex -1.0625 1.5748 45.2003 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.70162 1.5748 44.3433 + vertex -1.8403 1.5748 44.4225 + vertex -2.48982 1.5748 44.7975 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.83132 1.5748 43.8592 + vertex -1.8403 1.5748 44.4225 + vertex -2.70162 1.5748 44.3433 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.125 1.5748 43.36 + vertex -2.83132 1.5748 43.8592 + vertex -2.875 1.5748 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.83132 1.5748 43.8592 + vertex -2.125 1.5748 43.36 + vertex -1.8403 1.5748 44.4225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.83132 1.5748 42.8608 + vertex -2.125 1.5748 43.36 + vertex -2.875 1.5748 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.70162 1.5748 42.3767 + vertex -2.125 1.5748 43.36 + vertex -2.83132 1.5748 42.8608 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.70162 1.5748 42.3767 + vertex -1.8403 1.5748 42.2975 + vertex -2.125 1.5748 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.48982 1.5748 41.9225 + vertex -1.8403 1.5748 42.2975 + vertex -2.70162 1.5748 42.3767 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -2.20238 1.5748 41.512 + vertex -1.8403 1.5748 42.2975 + vertex -2.48982 1.5748 41.9225 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -1.8403 1.5748 42.2975 + vertex -2.20238 1.5748 41.512 + vertex -1.0625 1.5748 41.5197 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.84801 1.5748 41.1576 + vertex -1.0625 1.5748 41.5197 + vertex -2.20238 1.5748 41.512 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.4375 1.5748 40.8702 + vertex -1.0625 1.5748 41.5197 + vertex -1.84801 1.5748 41.1576 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 1.4375 1.5748 45.8498 + vertex 1.0625 1.5748 45.2003 + vertex 1.84801 1.5748 45.5624 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 1.84801 1.5748 45.5624 + vertex 1.0625 1.5748 45.2003 + vertex 2.20238 1.5748 45.208 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 1.5748 44.4225 + vertex 2.20238 1.5748 45.208 + vertex 1.0625 1.5748 45.2003 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 2.20238 1.5748 45.208 + vertex 1.8403 1.5748 44.4225 + vertex 2.48982 1.5748 44.7975 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 1.5748 44.4225 + vertex 2.70162 1.5748 44.3433 + vertex 2.48982 1.5748 44.7975 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 1.5748 44.4225 + vertex 2.83132 1.5748 43.8592 + vertex 2.70162 1.5748 44.3433 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.125 1.5748 43.36 + vertex 2.83132 1.5748 43.8592 + vertex 1.8403 1.5748 44.4225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.125 1.5748 43.36 + vertex 2.875 1.5748 43.36 + vertex 2.83132 1.5748 43.8592 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.125 1.5748 43.36 + vertex 2.83132 1.5748 42.8608 + vertex 2.875 1.5748 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.125 1.5748 43.36 + vertex 2.70162 1.5748 42.3767 + vertex 2.83132 1.5748 42.8608 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 1.5748 42.2975 + vertex 2.70162 1.5748 42.3767 + vertex 2.125 1.5748 43.36 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 1.5748 42.2975 + vertex 2.48982 1.5748 41.9225 + vertex 2.70162 1.5748 42.3767 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 2.20238 1.5748 41.512 + vertex 1.8403 1.5748 42.2975 + vertex 1.0625 1.5748 41.5197 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.8403 1.5748 42.2975 + vertex 2.20238 1.5748 41.512 + vertex 2.48982 1.5748 41.9225 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0.983308 1.5748 40.6584 + vertex 1.0625 1.5748 41.5197 + vertex 0 1.5748 41.235 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.0625 1.5748 41.5197 + vertex 1.84801 1.5748 41.1576 + vertex 2.20238 1.5748 41.512 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -0.983308 1.5748 40.6584 + vertex -1.0625 1.5748 41.5197 + vertex -1.4375 1.5748 40.8702 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -1.0625 1.5748 41.5197 + vertex -0.983308 1.5748 40.6584 + vertex 0 1.5748 41.235 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 1.0625 1.5748 41.5197 + vertex 1.4375 1.5748 40.8702 + vertex 1.84801 1.5748 41.1576 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -0.499238 1.5748 40.5287 + vertex 0 1.5748 41.235 + vertex -0.983308 1.5748 40.6584 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 1.0625 1.5748 41.5197 + vertex 0.983308 1.5748 40.6584 + vertex 1.4375 1.5748 40.8702 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 1.5748 40.485 + vertex 0 1.5748 41.235 + vertex -0.499238 1.5748 40.5287 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 1.5748 41.235 + vertex 0.499238 1.5748 40.5287 + vertex 0.983308 1.5748 40.6584 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 0 1.5748 41.235 + vertex 0 1.5748 40.485 + vertex 0.499238 1.5748 40.5287 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -29.06 1.5748 10 + vertex 29.06 1.5748 11.6 + vertex -29.06 1.5748 11.6 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 29.06 1.5748 11.6 + vertex -29.06 1.5748 10 + vertex 29.06 1.5748 10 + endloop + endfacet + facet normal 0.965925 -0 0.258822 + outer loop + vertex 1.8403 0.0999994 44.4225 + vertex 2.125 1.5748 43.36 + vertex 1.8403 1.5748 44.4225 + endloop + endfacet + facet normal 0.965925 0 0.258822 + outer loop + vertex 2.125 1.5748 43.36 + vertex 1.8403 0.0999994 44.4225 + vertex 2.125 0.0999994 43.36 + endloop + endfacet + facet normal -0.965925 0 0.258822 + outer loop + vertex -2.125 0.0999994 43.36 + vertex -1.8403 1.5748 44.4225 + vertex -2.125 1.5748 43.36 + endloop + endfacet + facet normal -0.965925 0 0.258822 + outer loop + vertex -1.8403 1.5748 44.4225 + vertex -2.125 0.0999994 43.36 + vertex -1.8403 0.0999994 44.4225 + endloop + endfacet + facet normal 0.258822 0 0.965925 + outer loop + vertex 0 1.5748 45.485 + vertex 1.0625 0.0999994 45.2003 + vertex 1.0625 1.5748 45.2003 + endloop + endfacet + facet normal 0.258822 0 0.965925 + outer loop + vertex 1.0625 0.0999994 45.2003 + vertex 0 1.5748 45.485 + vertex 0 0.0999994 45.485 + endloop + endfacet + facet normal 0.707107 0 0.707107 + outer loop + vertex 1.0625 1.5748 45.2003 + vertex 1.8403 0.0999994 44.4225 + vertex 1.8403 1.5748 44.4225 + endloop + endfacet + facet normal 0.707107 0 0.707107 + outer loop + vertex 1.8403 0.0999994 44.4225 + vertex 1.0625 1.5748 45.2003 + vertex 1.0625 0.0999994 45.2003 + endloop + endfacet + facet normal 0.258822 0 -0.965925 + outer loop + vertex 0 0.0999994 41.235 + vertex 1.0625 1.5748 41.5197 + vertex 1.0625 0.0999994 41.5197 + endloop + endfacet + facet normal 0.258822 0 -0.965925 + outer loop + vertex 1.0625 1.5748 41.5197 + vertex 0 0.0999994 41.235 + vertex 0 1.5748 41.235 + endloop + endfacet + facet normal -0.707107 0 0.707107 + outer loop + vertex -1.8403 1.5748 44.4225 + vertex -1.0625 0.0999994 45.2003 + vertex -1.0625 1.5748 45.2003 + endloop + endfacet + facet normal -0.707107 0 0.707107 + outer loop + vertex -1.0625 0.0999994 45.2003 + vertex -1.8403 1.5748 44.4225 + vertex -1.8403 0.0999994 44.4225 + endloop + endfacet + facet normal -0.258822 0 0.965925 + outer loop + vertex -1.0625 1.5748 45.2003 + vertex 0 0.0999994 45.485 + vertex 0 1.5748 45.485 + endloop + endfacet + facet normal -0.258822 0 0.965925 + outer loop + vertex 0 0.0999994 45.485 + vertex -1.0625 1.5748 45.2003 + vertex -1.0625 0.0999994 45.2003 + endloop + endfacet + facet normal 0.965925 0 -0.258822 + outer loop + vertex 2.125 0.0999994 43.36 + vertex 1.8403 1.5748 42.2975 + vertex 2.125 1.5748 43.36 + endloop + endfacet + facet normal 0.965925 0 -0.258822 + outer loop + vertex 1.8403 1.5748 42.2975 + vertex 2.125 0.0999994 43.36 + vertex 1.8403 0.0999994 42.2975 + endloop + endfacet + facet normal 0.707107 0 -0.707107 + outer loop + vertex 1.8403 0.0999994 42.2975 + vertex 1.0625 1.5748 41.5197 + vertex 1.8403 1.5748 42.2975 + endloop + endfacet + facet normal 0.707107 0 -0.707107 + outer loop + vertex 1.0625 1.5748 41.5197 + vertex 1.8403 0.0999994 42.2975 + vertex 1.0625 0.0999994 41.5197 + endloop + endfacet + facet normal -0.258822 0 -0.965925 + outer loop + vertex -1.0625 0.0999994 41.5197 + vertex 0 1.5748 41.235 + vertex 0 0.0999994 41.235 + endloop + endfacet + facet normal -0.258822 0 -0.965925 + outer loop + vertex 0 1.5748 41.235 + vertex -1.0625 0.0999994 41.5197 + vertex -1.0625 1.5748 41.5197 + endloop + endfacet + facet normal -0.707107 0 -0.707107 + outer loop + vertex -1.0625 0.0999994 41.5197 + vertex -1.8403 1.5748 42.2975 + vertex -1.0625 1.5748 41.5197 + endloop + endfacet + facet normal -0.707107 -0 -0.707107 + outer loop + vertex -1.8403 1.5748 42.2975 + vertex -1.0625 0.0999994 41.5197 + vertex -1.8403 0.0999994 42.2975 + endloop + endfacet + facet normal -0.965925 0 -0.258822 + outer loop + vertex -1.8403 0.0999994 42.2975 + vertex -2.125 1.5748 43.36 + vertex -1.8403 1.5748 42.2975 + endloop + endfacet + facet normal -0.965925 -0 -0.258822 + outer loop + vertex -2.125 1.5748 43.36 + vertex -1.8403 0.0999994 42.2975 + vertex -2.125 0.0999994 43.36 + endloop + endfacet + facet normal -1 0 0 + outer loop + vertex -33.1978 -1.99219 11.6 + vertex -33.1978 0.0078125 79.7122 + vertex -33.1978 0.0078125 11.6 + endloop + endfacet + facet normal -1 -0 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex -33.1978 -1.99219 11.6 + vertex -33.1978 -1.99219 79.7122 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -31.79 -1.99219 78.72 + vertex 33.1822 -1.99219 79.7122 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -31.8595 -1.99219 78.7139 + vertex -31.79 -1.99219 78.72 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -31.9268 -1.99219 78.6959 + vertex -31.8595 -1.99219 78.7139 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -31.99 -1.99219 78.6664 + vertex -31.9268 -1.99219 78.6959 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -32.0471 -1.99219 78.6264 + vertex -31.99 -1.99219 78.6664 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -32.0964 -1.99219 78.5771 + vertex -32.0471 -1.99219 78.6264 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -32.1364 -1.99219 78.52 + vertex -32.0964 -1.99219 78.5771 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -32.1659 -1.99219 78.4568 + vertex -32.1364 -1.99219 78.52 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -32.1839 -1.99219 78.3895 + vertex -32.1659 -1.99219 78.4568 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 79.7122 + vertex -32.19 -1.99219 78.32 + vertex -32.1839 -1.99219 78.3895 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 -1.99219 11.6 + vertex -32.19 -1.99219 78.32 + vertex -33.1978 -1.99219 79.7122 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -32.19 -1.99219 78.32 + vertex -33.1978 -1.99219 11.6 + vertex -32.19 -1.99219 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 -1.99219 78.72 + vertex 33.1822 -1.99219 79.7122 + vertex -31.79 -1.99219 78.72 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.8595 -1.99219 78.7139 + vertex 33.1822 -1.99219 79.7122 + vertex 31.79 -1.99219 78.72 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.9268 -1.99219 78.6959 + vertex 33.1822 -1.99219 79.7122 + vertex 31.8595 -1.99219 78.7139 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.99 -1.99219 78.6664 + vertex 33.1822 -1.99219 79.7122 + vertex 31.9268 -1.99219 78.6959 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.0471 -1.99219 78.6264 + vertex 33.1822 -1.99219 79.7122 + vertex 31.99 -1.99219 78.6664 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.0964 -1.99219 78.5771 + vertex 33.1822 -1.99219 79.7122 + vertex 32.0471 -1.99219 78.6264 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.1364 -1.99219 78.52 + vertex 33.1822 -1.99219 79.7122 + vertex 32.0964 -1.99219 78.5771 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.1659 -1.99219 78.4568 + vertex 33.1822 -1.99219 79.7122 + vertex 32.1364 -1.99219 78.52 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.1839 -1.99219 78.3895 + vertex 33.1822 -1.99219 79.7122 + vertex 32.1659 -1.99219 78.4568 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 -1.99219 78.32 + vertex 33.1822 -1.99219 79.7122 + vertex 32.1839 -1.99219 78.3895 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 -1.99219 77.6922 + vertex 33.1822 -1.99219 79.7122 + vertex 32.19 -1.99219 78.32 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 -1.99219 71.7478 + vertex 33.1822 -1.99219 79.7122 + vertex 32.19 -1.99219 77.6922 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 -1.99219 11.6 + vertex 32.19 -1.99219 71.7478 + vertex 32.19 -1.99219 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 -1.99219 71.7478 + vertex 33.1822 -1.99219 11.6 + vertex 33.1822 -1.99219 79.7122 + endloop + endfacet + facet normal 1 -0 0 + outer loop + vertex 33.1822 -1.99219 79.7122 + vertex 33.1822 0.0078125 11.6 + vertex 33.1822 0.0078125 79.7122 + endloop + endfacet + facet normal 1 0 0 + outer loop + vertex 33.1822 0.0078125 11.6 + vertex 33.1822 -1.99219 79.7122 + vertex 33.1822 -1.99219 11.6 + endloop + endfacet + facet normal -0 0 1 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex 33.1822 -1.99219 79.7122 + vertex 33.1822 0.0078125 79.7122 + endloop + endfacet + facet normal 0 0 1 + outer loop + vertex 33.1822 -1.99219 79.7122 + vertex -33.1978 0.0078125 79.7122 + vertex -33.1978 -1.99219 79.7122 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.0546 0.0078125 2 + vertex 32.5378 0.0078125 1.1885 + vertex 32.19 0.0078125 2 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 29.06 0.0078125 11.6 + vertex 29.06 0.0078125 10 + vertex 32.19 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex 31.79 0.0078125 80.72 + vertex -31.79 0.0078125 80.72 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex -31.79 0.0078125 80.72 + vertex -32.2068 0.0078125 80.6835 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 79.7122 + vertex 31.79 0.0078125 80.72 + vertex -33.1978 0.0078125 79.7122 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex -32.2068 0.0078125 80.6835 + vertex -32.6108 0.0078125 80.5753 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex -32.6108 0.0078125 80.5753 + vertex -32.99 0.0078125 80.3985 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 31.79 0.0078125 80.72 + vertex 33.1822 0.0078125 79.7122 + vertex 32.2068 0.0078125 80.6835 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex -32.99 0.0078125 80.3985 + vertex -33.3327 0.0078125 80.1585 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.2068 0.0078125 80.6835 + vertex 33.1822 0.0078125 79.7122 + vertex 32.6108 0.0078125 80.5753 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 79.7122 + vertex -33.3327 0.0078125 80.1585 + vertex -33.6285 0.0078125 79.8627 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.6108 0.0078125 80.5753 + vertex 33.1822 0.0078125 79.7122 + vertex 32.99 0.0078125 80.3985 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.99 0.0078125 80.3985 + vertex 33.1822 0.0078125 79.7122 + vertex 33.3327 0.0078125 80.1585 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.8685 0.0078125 79.52 + vertex -33.1978 0.0078125 79.7122 + vertex -33.6285 0.0078125 79.8627 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -34.0453 0.0078125 79.1408 + vertex -33.1978 0.0078125 79.7122 + vertex -33.8685 0.0078125 79.52 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -34.1535 0.0078125 78.7368 + vertex -33.1978 0.0078125 79.7122 + vertex -34.0453 0.0078125 79.1408 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -34.19 0.0078125 78.32 + vertex -33.1978 0.0078125 79.7122 + vertex -34.1535 0.0078125 78.7368 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -34.19 0.0078125 78.32 + vertex -33.1978 0.0078125 11.6 + vertex -33.1978 0.0078125 79.7122 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -32.19 0.0078125 11.6 + vertex -32.19 0.0078125 10 + vertex -29.06 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -33.1978 0.0078125 11.6 + vertex -32.19 0.0078125 10 + vertex -32.19 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -34.19 0.0078125 6.86645e-07 + vertex -32.19 0.0078125 10 + vertex -33.1978 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -32.19 0.0078125 2 + vertex -32.5378 0.0078125 1.1885 + vertex -32.0546 0.0078125 2 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -32.19 0.0078125 10 + vertex -34.19 0.0078125 6.86645e-07 + vertex -32.19 0.0078125 2 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -34.19 0.0078125 6.86645e-07 + vertex -33.1978 0.0078125 11.6 + vertex -34.19 0.0078125 78.32 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -32.19 0.0078125 2 + vertex -34.19 0.0078125 6.86645e-07 + vertex -32.5378 0.0078125 1.1885 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex -32.5378 0.0078125 1.1885 + vertex -34.19 0.0078125 6.86645e-07 + vertex -33.2453 0.0078125 6.86645e-07 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex 33.3327 0.0078125 80.1585 + vertex 33.1822 0.0078125 79.7122 + vertex 33.6285 0.0078125 79.8627 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 79.7122 + vertex 33.8685 0.0078125 79.52 + vertex 33.6285 0.0078125 79.8627 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 79.7122 + vertex 34.0453 0.0078125 79.1408 + vertex 33.8685 0.0078125 79.52 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 79.7122 + vertex 34.1535 0.0078125 78.7368 + vertex 34.0453 0.0078125 79.1408 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 79.7122 + vertex 34.19 0.0078125 78.32 + vertex 34.1535 0.0078125 78.7368 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 11.6 + vertex 34.19 0.0078125 78.32 + vertex 33.1822 0.0078125 79.7122 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 0.0078125 10 + vertex 32.19 0.0078125 11.6 + vertex 29.06 0.0078125 10 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 0.0078125 11.6 + vertex 32.19 0.0078125 10 + vertex 33.1822 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 32.19 0.0078125 10 + vertex 32.19 0.0078125 2 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 32.19 0.0078125 2 + vertex 32.5378 0.0078125 1.1885 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 34.19 0.0078125 6.86645e-07 + vertex 32.5378 0.0078125 1.1885 + vertex 33.2453 0.0078125 6.86645e-07 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 32.19 0.0078125 10 + vertex 34.19 0.0078125 6.86645e-07 + vertex 33.1822 0.0078125 11.6 + endloop + endfacet + facet normal 0 -1 0 + outer loop + vertex 33.1822 0.0078125 11.6 + vertex 34.19 0.0078125 6.86645e-07 + vertex 34.19 0.0078125 78.32 + endloop + endfacet + facet normal 0 -1 -0 + outer loop + vertex -29.06 0.0078125 11.6 + vertex -32.19 0.0078125 10 + vertex -29.06 0.0078125 10 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex 32.19 -1.99219 11.6 + vertex 33.1822 0.0078125 11.6 + vertex 33.1822 -1.99219 11.6 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex 33.1822 0.0078125 11.6 + vertex 32.19 -1.99219 11.6 + vertex 32.19 0.0078125 11.6 + endloop + endfacet + facet normal 0 0 -1 + outer loop + vertex -33.1978 -1.99219 11.6 + vertex -32.19 0.0078125 11.6 + vertex -32.19 -1.99219 11.6 + endloop + endfacet + facet normal -0 0 -1 + outer loop + vertex -32.19 0.0078125 11.6 + vertex -33.1978 -1.99219 11.6 + vertex -33.1978 0.0078125 11.6 + endloop + endfacet +endsolid OpenSCAD_Model diff --git a/systems/files/zif-diagnostic-cartridge-top.stl b/systems/files/zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..05eb6f4 Binary files /dev/null and b/systems/files/zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/card_preview_Dead_Test_-_Side.jpeg b/systems/images/card_preview_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..d7c8d1e Binary files /dev/null and b/systems/images/card_preview_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/card_preview_Dead_Test_-_Top.jpeg b/systems/images/card_preview_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..ac03eb4 Binary files /dev/null and b/systems/images/card_preview_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/card_preview_zif-diagnostic-cartridge-bottom.stl b/systems/images/card_preview_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..f5a6af4 Binary files /dev/null and b/systems/images/card_preview_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/card_preview_zif-diagnostic-cartridge-top.stl b/systems/images/card_preview_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..7ac7ef1 Binary files /dev/null and b/systems/images/card_preview_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/large_display_IMG_20200104_174055.jpg b/systems/images/large_display_IMG_20200104_174055.jpg new file mode 100644 index 0000000..bfc8947 Binary files /dev/null and b/systems/images/large_display_IMG_20200104_174055.jpg differ diff --git a/systems/images/large_display_keychain_mm_by_ks_20200104-62-7jppmq.stl b/systems/images/large_display_keychain_mm_by_ks_20200104-62-7jppmq.stl new file mode 100644 index 0000000..9bc49bf Binary files /dev/null and b/systems/images/large_display_keychain_mm_by_ks_20200104-62-7jppmq.stl differ diff --git a/systems/images/large_display_nut_job_20200104-57-84dp28.stl b/systems/images/large_display_nut_job_20200104-57-84dp28.stl new file mode 100644 index 0000000..bdb07da Binary files /dev/null and b/systems/images/large_display_nut_job_20200104-57-84dp28.stl differ diff --git a/systems/images/large_display_uchtyt_na_stojaki_trytek.stl b/systems/images/large_display_uchtyt_na_stojaki_trytek.stl new file mode 100644 index 0000000..f73f0d7 Binary files /dev/null and b/systems/images/large_display_uchtyt_na_stojaki_trytek.stl differ diff --git a/systems/images/large_preview_Dead_Test_-_Side.jpeg b/systems/images/large_preview_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..fe44e6e Binary files /dev/null and b/systems/images/large_preview_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/large_preview_Dead_Test_-_Top.jpeg b/systems/images/large_preview_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..743d01f Binary files /dev/null and b/systems/images/large_preview_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/large_preview_zif-diagnostic-cartridge-bottom.stl b/systems/images/large_preview_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..bcbd41f Binary files /dev/null and b/systems/images/large_preview_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/large_preview_zif-diagnostic-cartridge-top.stl b/systems/images/large_preview_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..d46b979 Binary files /dev/null and b/systems/images/large_preview_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/large_thumb_Dead_Test_-_Side.jpeg b/systems/images/large_thumb_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..ab75174 Binary files /dev/null and b/systems/images/large_thumb_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/large_thumb_Dead_Test_-_Top.jpeg b/systems/images/large_thumb_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..581ba1c Binary files /dev/null and b/systems/images/large_thumb_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/large_thumb_zif-diagnostic-cartridge-bottom.stl b/systems/images/large_thumb_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..2f43531 Binary files /dev/null and b/systems/images/large_thumb_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/large_thumb_zif-diagnostic-cartridge-top.stl b/systems/images/large_thumb_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..34f0997 Binary files /dev/null and b/systems/images/large_thumb_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/medium_preview_Dead_Test_-_Side.jpeg b/systems/images/medium_preview_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..554af23 Binary files /dev/null and b/systems/images/medium_preview_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/medium_preview_Dead_Test_-_Top.jpeg b/systems/images/medium_preview_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..1b8a52e Binary files /dev/null and b/systems/images/medium_preview_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/medium_preview_zif-diagnostic-cartridge-bottom.stl b/systems/images/medium_preview_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..faca9fd Binary files /dev/null and b/systems/images/medium_preview_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/medium_preview_zif-diagnostic-cartridge-top.stl b/systems/images/medium_preview_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..56cfd78 Binary files /dev/null and b/systems/images/medium_preview_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/medium_thumb_Dead_Test_-_Side.jpeg b/systems/images/medium_thumb_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..cd27883 Binary files /dev/null and b/systems/images/medium_thumb_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/medium_thumb_Dead_Test_-_Top.jpeg b/systems/images/medium_thumb_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..427f718 Binary files /dev/null and b/systems/images/medium_thumb_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/medium_thumb_zif-diagnostic-cartridge-bottom.stl b/systems/images/medium_thumb_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..07abf76 Binary files /dev/null and b/systems/images/medium_thumb_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/medium_thumb_zif-diagnostic-cartridge-top.stl b/systems/images/medium_thumb_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..1b35b3e Binary files /dev/null and b/systems/images/medium_thumb_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/small_preview_Dead_Test_-_Side.jpeg b/systems/images/small_preview_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..f497ab2 Binary files /dev/null and b/systems/images/small_preview_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/small_preview_Dead_Test_-_Top.jpeg b/systems/images/small_preview_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..a0248fe Binary files /dev/null and b/systems/images/small_preview_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/small_preview_zif-diagnostic-cartridge-bottom.stl b/systems/images/small_preview_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..02940fd Binary files /dev/null and b/systems/images/small_preview_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/small_preview_zif-diagnostic-cartridge-top.stl b/systems/images/small_preview_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..a590690 Binary files /dev/null and b/systems/images/small_preview_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/small_thumb_Dead_Test_-_Side.jpeg b/systems/images/small_thumb_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..a02f428 Binary files /dev/null and b/systems/images/small_thumb_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/small_thumb_Dead_Test_-_Top.jpeg b/systems/images/small_thumb_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..12913ef Binary files /dev/null and b/systems/images/small_thumb_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/small_thumb_zif-diagnostic-cartridge-bottom.stl b/systems/images/small_thumb_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..6839394 Binary files /dev/null and b/systems/images/small_thumb_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/small_thumb_zif-diagnostic-cartridge-top.stl b/systems/images/small_thumb_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..44c78a3 Binary files /dev/null and b/systems/images/small_thumb_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/tiny_preview_Dead_Test_-_Side.jpeg b/systems/images/tiny_preview_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..a153d21 Binary files /dev/null and b/systems/images/tiny_preview_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/tiny_preview_Dead_Test_-_Top.jpeg b/systems/images/tiny_preview_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..d8d2344 Binary files /dev/null and b/systems/images/tiny_preview_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/tiny_preview_zif-diagnostic-cartridge-bottom.stl b/systems/images/tiny_preview_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..3dabef3 Binary files /dev/null and b/systems/images/tiny_preview_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/tiny_preview_zif-diagnostic-cartridge-top.stl b/systems/images/tiny_preview_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..8b238a5 Binary files /dev/null and b/systems/images/tiny_preview_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/tiny_thumb_Dead_Test_-_Side.jpeg b/systems/images/tiny_thumb_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..b224805 Binary files /dev/null and b/systems/images/tiny_thumb_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/tiny_thumb_Dead_Test_-_Top.jpeg b/systems/images/tiny_thumb_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..feffee1 Binary files /dev/null and b/systems/images/tiny_thumb_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/tiny_thumb_zif-diagnostic-cartridge-bottom.stl b/systems/images/tiny_thumb_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..68cd73d Binary files /dev/null and b/systems/images/tiny_thumb_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/tiny_thumb_zif-diagnostic-cartridge-top.stl b/systems/images/tiny_thumb_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..a609852 Binary files /dev/null and b/systems/images/tiny_thumb_zif-diagnostic-cartridge-top.stl differ diff --git a/systems/images/tinycard_preview_Dead_Test_-_Side.jpeg b/systems/images/tinycard_preview_Dead_Test_-_Side.jpeg new file mode 100644 index 0000000..420db92 Binary files /dev/null and b/systems/images/tinycard_preview_Dead_Test_-_Side.jpeg differ diff --git a/systems/images/tinycard_preview_Dead_Test_-_Top.jpeg b/systems/images/tinycard_preview_Dead_Test_-_Top.jpeg new file mode 100644 index 0000000..b91387a Binary files /dev/null and b/systems/images/tinycard_preview_Dead_Test_-_Top.jpeg differ diff --git a/systems/images/tinycard_preview_zif-diagnostic-cartridge-bottom.stl b/systems/images/tinycard_preview_zif-diagnostic-cartridge-bottom.stl new file mode 100644 index 0000000..5f5c431 Binary files /dev/null and b/systems/images/tinycard_preview_zif-diagnostic-cartridge-bottom.stl differ diff --git a/systems/images/tinycard_preview_zif-diagnostic-cartridge-top.stl b/systems/images/tinycard_preview_zif-diagnostic-cartridge-top.stl new file mode 100644 index 0000000..e9cf7bf Binary files /dev/null and b/systems/images/tinycard_preview_zif-diagnostic-cartridge-top.stl differ