Website powered by

Building a KotOR Modding Suite from Scratch Retrospective: 5 Weeks of Python Development

A Full Project Retrospective: · PyQt5 · ModernGL · Flask · Pillow · NumPy Assisted by GenSpark AI Developer

Over the past 5 weeks, the Ghostworks pipeline has been one of the most demanding and rewarding experiences I've had as a programmer. One question "why does it take a KotOR modder 4–6 hours and 6 tools to place one scripted NPC?" grew into three Python desktop applications: GModular (level assembly and 3D viewport), GhostScripter (NWScript IDE and dialogue editor), and GhostRigger (model viewer and asset toolkit). A unified suite replacing an ecosystem of aging, disconnected community tools: and a project that forced me to start building real, shippable software.

The pipeline that emerged: Binary I/O → Domain Modelling → Architecture → Qt UI → Test Suite → Debugging → Profiling → Shipping

Early Stages: Architecture and the Blockout The first lesson from environment art.... don't rush detail before layout, hit immediately here. Before any UI could be drawn, the apps needed to speak the game's language: GFF V3.2, a binary format behind every meaningful KotOR data file. The initial reader worked. The writer did not. GFFWriter.to_bytes() called a stub shim instead of the actual encoder: silently producing invalid binary. Nothing crashed. It just wrote wrong data. This is the blockout lesson, but for data: establish what is true at the foundation before building on top of it. The fix: a breadth-first two-phase algorithm, assign stable struct indices in Phase 1, encode all field data in Phase 2.

Asset Creation: Three Tools, Three Turning Points GModular's 3D viewport: ModernGL in PyQt5, GLSL shaders, orbit camera, ray-cast picking from 2D click into 3D space. The first render: a flat grid. By the end: color-coded object placeholders, orbit/pan/zoom, gimbal-safe gizmos, walkmesh collision, NPC patrol preview. Each feature exposed a gap: quaternion vs. Euler rotation, NDC-to-world unprojection, object vs. world-space transforms.

GhostScripter's dialogue editor went through full rewrites. The first version used basic QGraphicsScene rectangles. Eventually it gained Blueprint-style Bézier wires, Unreal-accurate spline rendering, grid snapping, and a Legacy tree-list mode. The lesson: direct widget connections cascade bugs at scale. A signal/slot model with a debounce timer changed everything.

GhostRigger's software rasterizer: 6,000+ lines... was its own discipline: per-frame vertex projection, painter-sorted draw loop, UV-textured per-triangle rendering, entirely in PIL.

The Freeze Bug: When a Clean Algorithm Isn't Optional Opening a real KotOR dialogue file: 193 nodes, cyclic back-edges, caused GhostScripter to hang. No crash, no error. The layout algorithm used list.pop(0) as a BFS queue with no visited set. On a cyclic graph: Bellman-Ford, O(V²·E) on 193 nodes. Not crashing... running forever. Replacing it with collections.deque and a visited set brought layout from never-finishing to under 1ms. A clean algorithm isn't aesthetic. It's the difference between software that works and software that silently fails.

The Hardest Lessons: Testing and Provable Correctness Writing binary files KotOR loads requires precise byte layout and correct offset tables. Wrong output means files the game silently ignores... no error, it either works or it doesn't. Test suites became non-negotiable: GhostRigger, 1,596 passing tests including a crash audit across 2,527 real models, zero crashes; GhostScripter, 403; GModular, 44. The crash audit caught categories of bugs no targeted test would have found. Writing tests before touching working code is something I now understand in my hands, not just in theory.

Shipping and Reflection The IPC layer: localhost HTTP JSON across fixed ports, every bridge fail-safe, turned three apps into a suite. A missing companion tool produces a status bar message, not a crash. Then the final lesson: the gap between "runs on my machine" and "ships to a user." A PyInstaller bug invisible during development, only surfacing in the frozen executable, was the last thing between a project and a product.

Looking back over 5 weeks and 23 iterations, the biggest change isn't the codebase.... it's how I think. Each layer has a clear contract. Mutations are reversible. Dependencies fail gracefully. The format you write is bit-for-bit identical to what the engine expects. The pipeline isn't a checklist anymore. It's a way of thinking.

Work on this project is still ongoing, but this is the result of my experience working on it from the past 5 weeks.

Credit to ndixUR (MDLOps), the PyKotor contributors, and the researchers at Xoreos, whose twenty years of reverse-engineering work on BioWare's proprietary formats made binary parsing possible at all. Credit to the Pillow and NumPy maintainers, whose libraries underpin GhostRigger's entire rendering pipeline. And to whoever left the comment in the PyKotor source noting that bytes 15–100 of a real TPC file are always zero, you saved me a day.