NNory Blog
HomeReviewsLife GuidesJournal
NORY
Grish's Personal Space

A personal space where I share my thoughts on music, movies, and life through journaling. Join me on this journey of discovery and reflection.

Explore

  • Home
  • Reviews
  • Life Guides
  • Journal

Resources

  • Bookmarks
  • Tags
  • RSS Feed

Stay Updated

Get the latest articles and resources sent to your inbox.

© 2026 Grish. All rights reserved.

Privacy PolicyTerms of Service

    How I Wasted 15 Hours Debugging a Bug I Created a Month Ago (And Completely Forgot About)

    A brutal debugging story from Brave: a self-inflicted location bug, Unicode mismatches, and the pragmatic server-side fix that shipped.

    NorySight
    NorySight
    February 12, 2026
    31 minutes
    Debugging Game Development Rust Java Backend Lessons Learned
    Status: Ready to listen

    Your browser does not support speech synthesis.

    There's a special kind of pain reserved for developers who spend an entire day hunting down a bug, only to discover they created it themselves. And then there's an even deeper circle of hell for those who realize they made that change a month ago and completely forgot about it.

    Welcome to my 15-hour descent into madness.

    The Setup: Building Brave

    I'm working on a game called Brave. It's not your typical Unity or Godot project, the client is pure, brute-force Rust. No game engine, no frameworks. Just raw Rust handling everything from rendering to physics to networking. It's been challenging, occasionally maddening, but deeply satisfying.

    The backend? That's Java.

    Before you judge that architectural choice, hear me out: Rust is fantastic for the performance-critical game client where you need every last drop of speed and control. But Java makes backend development so much more productive. Better string handling (especially with Unicode), mature server libraries, faster iteration cycles, easier debugging. It's pragmatic, not pure.

    I recently pushed out a limited early access version, about 60-70 downloads, roughly 10 active players, and a two-week testing window that's now closed. With the early access feedback trickling in, I've been adding features and polishing rough edges.

    That's when I encountered the bug.

    The Bug Appears: Welcome to Nowhere

    I was working on improvements to a special mob in Château Noir, one of the endgame locations. Better stats, special effects on kill, the usual refinements to make combat more interesting.

    I loaded into the location and immediately noticed something wrong: no notification. Usually when you enter a new area, you get a "You entered [Location Name]" message. Nothing appeared.

    Weird, but not alarming. Probably just forgot to add the log message somewhere.

    Then I tried to kill the mob.

    I couldn't even hit it.

    My attacks just... didn't register. No damage numbers, no hit confirmation, nothing. It was like I was swinging at air, except the mob was clearly right there in front of me.

    The Investigation Begins: This Should Be Simple

    My first thought was obvious: I must have broken something with the mob improvements I just coded. I started checking the combat code, the stat calculations, the hit detection logic. Everything looked fine.

    I added more logging to the mob combat system. Nothing appeared in the logs.

    I added even more logging. Still nothing.

    That's when I started to get suspicious. Why weren't any logs appearing? Not even the basic ones that should fire whenever combat happens?

    Then I realized: I wasn't even getting the location entry logs.

    Something was fundamentally wrong with the location system itself.

    Down the Rabbit Hole: 15 Hours of Pain

    I shifted focus to the location code. This is where things get tricky, location-related code touches every part of the game. Movement, combat, quests, spawning, everything depends on knowing where the player is. I'm honestly scared of touching location code because one small change that doesn't return what it's supposed to can break the entire game.

    But I had no choice. The bug was clearly location-related.

    I started comparing the client and server location mappings. They matched. "Château Noir" on the client, "Château Noir" on the server. Identical.

    So why was the server rejecting my location?

    I added more logs. I traced the network requests. I checked the database queries. Hours passed. I was getting genuinely angry now, the kind of frustrated where you start questioning whether you should abandon programming and become a farmer instead.

    I kept looking at the location names. Client: "Château Noir". Server: "Château Noir". They were the same. What was I missing?

    Then, buried in the client code, I saw it.

    A tiny utility function. So small I'd scrolled past it a dozen times.

    let cleaned_location = strip_special_chars(&location_name);
    network::send_to_server(&cleaned_location);

    And just like that, everything clicked.

    The Realization: I Am the World's Biggest Idiot

    One month ago, I had added that stripping function.

    The context came flooding back: I was working on a new action system that needed to check player locations. Dealing with special characters, accented letters, Unicode symbols, all that, was annoying. I kept having to copy-paste "Château Noir" with the special â character. It was tedious.

    So I added a simple function to strip the special characters for internal checks. Made the client-side validation logic much cleaner. Seemed like a reasonable solution at the time.

    And then I completely forgot that this stripped version was also being sent to the server.

    Here's what was happening:

    1. Game location: "Château Noir" (with special characters)
    2. Client strips it: "Chteau Noir" (no special characters)
    3. Client sends to server: "Chteau Noir"
    4. Server tries to look up: "Chteau Noir"
    5. Server database has: "Château Noir" (original with special characters)
    6. Server finds: NOTHING
    7. Server response: "You are NOWHERE" (limbo state)
    8. Player can't perform any actions

    The client-side validator was checking against the original "Château Noir" and passing. The server was receiving "Chteau Noir" and failing to find it in the database. The two systems were living in different realities.

    The player, me, was stuck in a limbo state. Technically in the location according to the client, but nowhere according to the server. And since the server has final authority on what you can and can't do, I was effectively frozen.

    15 hours. I had spent 15 hours debugging a problem I created myself a month ago and completely forgot about.

    My exact internal monologue: "What the fuck. I am an idiot. I am the world's biggest idiot."

    It was painful and hilarious in equal measure.

    But Why Didn't This Break Earlier?

    You might be wondering: if I added this bug a month ago, why am I only discovering it now?

    Two reasons:

    First, out of all the locations in the game, only three have special characters in their names:

    • Château Noir
    • Déjà Vu Valley
    • [One location with a name so long and full of special characters that I don't even want to type it out]

    The rest of the locations use plain ASCII characters, so the stripping function had no effect on them.

    Second, those three locations are all endgame content. They're not areas you casually stumble into in your first few hours of play.

    For the past month, I'd been working on other parts of the game, areas without special characters. I never visited those three problematic locations during development. And the early access players? They only had two weeks, and none of them reached endgame content in that time.

    The bug was sitting there, lurking, waiting for someone to finally visit Château Noir.

    That someone was me, trying to improve a mob, a month after I'd planted the landmine.

    The Solution: Should I Just Revert It?

    Once I understood the problem, the obvious solution seemed simple: just remove the stripping function and send the original location names with special characters intact.

    I tried that.

    It broke everything.

    Turns out, over the past month, I'd built other systems on top of that stripped-name assumption. Sub-location areas, action validators, various client-side checks, they all expected the cleaned strings. Reverting the stripping would require refactoring a significant chunk of the client code.

    My options:

    1. Revert the stripping and spend days refactoring client code, then push an update to all players
    2. Keep the stripping and make the server smart enough to handle both versions

    I went with option 2.

    The Fix: Location Aliases

    The solution was elegant and took about 4-5 lines of code in the Java backend.

    I added an alias system to the location mapping:

    class Location {
        String displayName;           // "Château Noir"
        List<String> aliases;         // ["Chteau Noir", "chateau noir"]
        // ... coordinates, spawn points, available actions, etc.
    }

    Now when the server receives a location lookup, it checks both the main display name and any aliases. If the client sends "Chteau Noir", the server finds it via the alias and correctly maps it to "Château Noir".

    Since only three locations have special characters, I manually added the stripped versions as aliases. No need for auto-generation or complex logic.

    The benefits:

    • No client update required: existing players don't need to download anything
    • Minimal code changes: just the backend, which I can hotfix immediately
    • Solves the immediate problem: players can actually play in those locations now
    • Future-proof: the alias system can be extended for other use cases (alternate names, localization, abbreviations)

    It was the pragmatic choice. Not the "pure" solution, but the one that actually shipped.

    The Aftermath: Too Exhausted to Care

    After implementing the alias system, I tested it.

    I loaded into Château Noir. The location notification appeared. Good sign.

    I attacked the mob. I could hit it. The mob took damage. It died.

    Victory! Except...

    The mob improvements I'd spent all that time coding before discovering the location bug? They didn't work quite right. The stats were off, the special effects weren't triggering properly.

    You know what I did?

    Nothing.

    I was so exhausted from 15 hours of debugging that I just... couldn't be bothered. I'd fix it tomorrow. I could kill the mob now, even if the improvements were a bit janky. That was good enough for today.

    I closed my laptop and walked away.

    The Lessons: What I Learned (The Hard Way)

    1. You Are Your Own Worst Enemy

    When debugging, don't assume it's the library, the framework, the compiler, or cosmic rays. It was probably you. You probably changed something weeks ago and forgot about it.

    Check your own recent commits. Check what seemed like a "quick fix" or a "simple improvement" that you made when you were tired or rushing.

    The bug is coming from inside the house.

    2. Document Everything (Yes, Even the "Obvious" Stuff)

    A month in software development is an eternity. You will forget why you made certain decisions. You will forget what seemed obvious at the time.

    Leave comments. Write commit messages that actually explain the reasoning. Update documentation.

    Your future self is a different person with no memory of your current context. Help them out.

    3. Consider the Full Pipeline

    When I added that stripping function, I only thought about the immediate client-side benefits. I didn't think about how it would affect the data being sent to the server.

    I literally forgot the backend existed.

    Always trace your data through the entire system: where does it come from, how is it transformed, where does it go, and what format does each component expect?

    4. Endgame Content Needs Testing Too

    I was so focused on early-game polish and new player experience that I wasn't regularly testing the endgame locations. That's how this bug sat undetected for a month.

    Just because content is "later in the game" doesn't mean it can wait for testing. Bugs don't care about your progression curve.

    5. Pragmatic Solutions Beat Perfect Ones

    The "correct" architectural solution would have been to remove the stripping, refactor all the dependent client code, and ensure both client and server were working with identical data formats.

    But that would have taken days and required pushing an update to all players.

    The alias system took 10 minutes to implement and solved the problem immediately. Sometimes good enough is better than perfect, especially when you're trying to ship.

    6. It's Okay to Walk Away

    After 15 hours of debugging, when I finally got the core issue fixed but the mob improvements still didn't work right, I could have pushed through. I could have spent another hour or two getting everything perfect.

    Instead, I walked away.

    That was the right choice. I was exhausted, frustrated, and not thinking clearly anymore. More work would have just created more bugs.

    Sometimes the most productive thing you can do is close your laptop and come back tomorrow with fresh eyes.

    Wrapping Up

    So there you have it: the story of how I spent 15 hours debugging a problem I created, forgot about, and then had to engineer around instead of simply reverting.

    The bug was me. I was the bug all along.

    If you're reading this and thinking "I would never do something that stupid", you're wrong. You will. Maybe not today, maybe not this month, but eventually you'll add some "harmless" change, forget about it, and then waste an entire day rediscovering your own mistake.

    The code gods are patient, but they always collect their due.

    And if you're currently stuck on a bug that makes no sense, that seems to violate all logic, that can't possibly be your fault? Check your git history from a few weeks ago. Check that thing you changed that seemed so simple and obvious you didn't even think twice about it.

    It's probably sitting there, mocking you, waiting to waste 15 hours of your life.

    Now if you'll excuse me, I need to go add detailed comments to every single function I've written in the past month, just in case future-me comes back and forgets why any of this exists.

    And also fix those mob improvements. Tomorrow. Definitely tomorrow.


    Have you ever wasted embarrassing amounts of time on self-inflicted bugs? What's your personal record for debugging something you broke yourself? Share your stories in the comments, misery loves company, and I desperately need to know I'm not alone in this.

    P.S. (February 2-6, 2026)

    I wish I could say this was the end of my self-inflicted debugging saga, but no. From February 2 to February 6, I hit another desync bug that took me four straight days to fully root-cause.

    This one was even meaner because visually everything looked correct on the client. The UI disappeared, controls came back, and it felt like the container was closed. But on the server, the container session was still considered active.

    What Actually Went Wrong

    In my utility layer I used the wrong close function:

    • clientCloseContainer: closes GUI on the client only
    • closeContainer: closes GUI and sends the close packet to the server

    I was calling the first one in code paths where I needed the second.

    So the real flow looked like this:

    1. Player opens container/UI
    2. Client and server are initially in sync (container_open = true)
    3. My code calls clientCloseContainer
    4. Client UI closes locally (container_open = false on client view)
    5. No close packet is sent
    6. Server state remains container_open = true
    7. Server rejects or blocks actions that require "no container open"
    8. Player experiences random "why can't I do anything?" behavior

    That mismatch is classic desync: both sides are acting logically based on different truths.

    Why It Was So Hard to Debug

    The worst part: there was no obvious crash, no clear stack trace, and no single "boom" point. It showed up as weird downstream behavior:

    • movement still worked, so it looked like the game was "mostly fine"
    • interactions silently failing
    • actions ignored by authoritative server checks
    • state transitions that worked only after reconnecting/reloading

    From my perspective, I kept debugging the systems that were failing (actions, interaction handlers, validators), not the system that caused the failure (container close semantics).

    I also had old utility code (2-3 months old) and I trusted it too much. I assumed "close container" meant the same thing everywhere. It didn't.

    That was the trap: because I could still move around, I blamed everything except container state. I thought the issue came from whatever gameplay changes I was making that day, not from one utility call quietly skipping a server packet.

    The Core Technical Lesson

    Client-side UI state is not authoritative state.

    If gameplay permissions depend on server-side session flags, then "closing the screen" and "closing the container protocol state" are two different operations. They must stay coupled.

    A quick mental model that would've saved me days:

    Visual close != Protocol close
    Protocol close requires packet
    No packet => server never transitions state

    What I Changed

    I replaced the wrong utility calls with closeContainer in the relevant paths so close events always notify the server. After that, the desync disappeared because both sides transitioned state together again.

    I also added a rule for myself when touching UI/container code:

    • If a flow affects gameplay permissions, confirm both client state and server state transitions
    • Log packet send + server ack at least once when adding/changing the flow
    • Never assume utility methods with similar names are behaviorally equivalent

    Four days for one wrong method call. Brutal, but honestly a perfect reminder that in multiplayer systems, tiny client/server contract mistakes can masquerade as "random bugs" for days.

    February 10, 2026: The Whiplash Moment

    A few days later, around February 10, I opened my older pathfinding code and had the opposite reaction: "Wait... this is actually clean."

    It had clear structure, useful logging, and even funny debug prints that made tracing behavior easier. Reading it felt like finding a note from a past version of me that actually knew what he was doing.

    The contrast was wild:

    • when I looked at messy parts, I thought my younger self was an idiot
    • when I looked at pathfinding, I thought my younger self was a genius

    Same developer. Same project. Totally different code quality depending on context, fatigue, and how much I respected observability while writing it.

    The Four-Day Debugging Loop

    Those four days were basically the same cycle:

    1. wake up
    2. debug for hours
    3. fail to find root cause
    4. go to sleep frustrated
    5. repeat

    At my worst point, I was close to deleting the whole project. Not because the bug was "hard" in a deep algorithmic sense, but because it was hard in the most demoralizing way: everything looked almost correct while still being fundamentally out of sync.

    You Might Also Like

    Related Posts

    December 22, 2025
    Arch LinuxDebuggingLinux
    34 minutes

    Two Days of Debugging: A One-Character Story

    How I spent two days debugging a race condition in my Arch Linux customization script simply because I didn't read the manual.

    Read Article
    December 19, 2025
    bojack-horsemanmental-healthpersonal-growth
    14 minutes

    Two and a Half Years Later: BoJack Horseman Still Haunts Me

    A personal reflection on how BoJack Horseman changed my perspective on growth, mental health, and the uncomfortable truth that nobody is looking out for you.

    Read Article
    April 24, 2025
    Game DevelopmentPersonalDevlog
    12 minutes

    Devlog 1: The Dream Begins

    The start of my journey to create a game from an old childhood dream.

    Read Article