Junethack 2023 – the 13th NetHack Cross-Variant Summer Tournament

•1. June 2023 • Leave a Comment

The thirteenth installment of the annual NetHack Cross-Variant Summer Tournament called Junethack will started June 1 at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks. In other words, if you suck at playing NetHack, this is the tournament for you!

This year, we have (again) the most variants ever available for you to play (I swear, NetHack forks don’t grow on trees even if it feels like it). 20 different variants of NetHack make the full set of cross-variant achievements harder to get than in any years before. We really don’t expect anybody to catch get them all. Because of this the cross-variant achievements are fine-grained and you earn one for every new milestone in a new variant.

This year there’s a variant returning to the tournament that for the last time was playable in the 2014 tournament. AceHack got resurrected and is playable on the Hardfought public servers.

The newest entry in the family of NetHack variants is HackEM, another variant following in Slash’EM’s footsteps and trying to reconcile crazy game design with the desire of players to die in games. It is also playable on the Hardfought public servers.

Also consider joining a clan. Playing with others is great fun and the clan trophies are designed such that a clan member cannot have a negative impact on a clan’s standing. Many clans are communicating on IRC or Discord if you are looking for an existing clan.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Libera.Chat IRC network. There’s also a dedicated discord channel #nethack-junethack on the Roguelikes discord channel which is synced with the IRC channel.

There’s a FAQ on the NetHack wiki.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

Junethack 2022 – the 12th NetHack Cross-Variant Summer Tournament

•1. June 2022 • Leave a Comment

The twelfth installment of the annual NetHack Cross-Variant Summer Tournament called Junethack has started today at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks. In other words, if you suck at playing NetHack, this is the tournament for you!

This year there’s a variant returning to the tournament that for the last (and only) time was playable in the 2015 tournament. Since then SlashTHEM received a lots of updates, making it worthwhile to try out again. In total, there are 19 different variants of NetHack to play and get achievements and trophies in.

Also consider joining a clan. Playing with others is great fun and the clan trophies are designed such that a clan member cannot have a negative impact on a clan’s standing. Many clans are communicating on IRC or Discord if you are looking for an existing clan.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Libera.Chat IRC network. There’s also a dedicated discord channel #nethack-junethack on the Roguelikes discord channel which is synced with the IRC channel.

There’s a FAQ on the NetHack wiki.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

Junethack 2021 – the 11th NetHack Cross-Variant Summer Tournament

•31. May 2021 • Leave a Comment

The elventh installment of the annual NetHack Cross-Variant Summer Tournament called Junethack will start at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks. In other words, if you suck at playing NetHack, this is the tournament for you!

For once, there is no new NetHack fork in the tournament (you weren’t ascending most of them anyway). Nevertheless, there are 19 different variants of NetHack to play and get achievements and trophies in.

Also consider joining a clan. Playing with others is great fun and the clan trophies are designed such that a clan member cannot have a negative impact on a clan’s standing.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Libera.Chat IRC network. There’s also a dedicated discord channel #nethack-junethack on the Roguelikes discord channel which is synced with the IRC channel.

There’s a FAQ on the NetHack wiki.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

Junethack 2020 – the 10th NetHack Cross-Variant Summer Tournament

•31. May 2020 • Leave a Comment

The tenth installment of the annual NetHack Cross-Variant Summer Tournament called Junethack will start at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks.

This year, we have (again) the most variants ever available for you to play. 19 different variants of NetHack make the full set of cross-variant achievements harder to get than in any years before. We really don’t expect anybody to catch get them all. Because of this you earn a new cross-variant achievement for every new milestone in a new variant (this even includes milestones for your first variant, blame the lazy programmers).

The newly supported variants this year is the bleeding edge development version of NetHack 3.7.0 playable on the hardfought public servers. The next variant is one of the oldest NetHack forks, Slash’EM. Last but not least, GnollHack is playable on the gnollhack.com public servers.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Freenode IRC network. There’s also a dedicated discord channel #nethack-junethack on the Roguelikes discord channel which is synced with the IRC channel.

There’s a FAQ on the NetHack wiki.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

 

Level distribution of NetHack 3.4.3 and UnNetHack

•29. January 2020 • Leave a Comment

In 2014 I ran some tests on the level compiler in UnNetHack. The level compiler is different from the one in NetHack 3.4.3. It’s an extended version of this patch by Pasi Kallinen. Pasi added this to SporkHack and after I took it from there and added it to UnNetHack, Pasi himself updated and extended it for the UnNetHack code. Since 3.6 it has also been incorporated into NetHack itself.

I wanted to stress-test this level generation code to find bugs and as a by-product of this process, generate some statistics on how dungeons in UnNetHack look like. I added some hacks so that starting a game would run automatically through the whole dungeon and record the dungeon layout on file. Any error or warning from the code would also generate a core dump and as the random seed got logged, I could easily debug any issue that cropped up.

The code was simple enough to apply to Vanilla as well, so I ran some tests on Vanilla as well. At the time of testing, this was still 3.4.3. I will have to rerun the stress test with the current version of NetHack, especially now that we have changed the level files format to Lua.

In UnNetHack, several issues were identified and fixed. For example an endless loop in place_random_engraving() or the level compiler crashing in the ice room vault while placing a trap.

The only bug I found with NetHack 3.4.3 was that if you get the Frontier Town version of mine town, on the right side there might be not enough space for downstairs placement. To my knowledge, this hasn’t happened ever to a player but it also is not a game breaker. You can always dig down and use the upstairs to come back up. The game will place you at a random position if the target level has no downstairs.

For both Vanilla and UnNetHack, I produced each 1 million dungeons. The big room came up 40.04% in Vanilla and 40.07% in UnNetHack which is close enough to the theoretical 40% that one would expect. Fort Ludios was 74.80% in Vanilla which is slightly lower than the 74.9% that have been calculated more than 15 years ago by Robert R. Schneck.
https://groups.google.com/forum/#!msg/rec.games.roguelike.nethack/tzlaN5uzKw8/fHPrsgfRQ8gJ

We don’t have his code, so I can’t reproduce his findings and I don’t know if the difference was caused by him not using 3.4.3 (although unlikely, I think nothing changed in the level generation code between previous 3.4 versions and 3.4.3), some error in his or my code changes, or maybe even just by using different random number generators. But the difference does not seem to be significant enough to worry about.

In UnNetHack, Fort Ludios came up 93.51%, this is due to a change in the portal placement code so that it would appear more often. The nymph level was in 45% of all generated dungeons.

I made two charts that visualize the data for NetHack 3.4.3 and UnNetHack.

I left out some levels because they are dependant on each other and only clutter up the chart. For example, Sokoban is always directly below Oracle, fakewiz1 and fakewiz2 have the same probabilities, or the wizard’s tower levels.

https://bhaak.net/nethack/levels/vanilla.html
https://bhaak.net/nethack/levels/unnethack.html

I was surprised to see that the new UnNetHack levels don’t influence the placement of the Vanilla levels. Fort Ludios is the only exception but we deliberately tweaked its placement so this was to be expected.

The code for both Vanilla and UnNetHack are in branches in the normal UnNetHack git repository:
https://github.com/unnethack/unnethack/tree/level_statistics
https://github.com/unnethack/unnethack/tree/nethack_level_statistics

The raw data, including the levels I removed for the charts:
https://bhaak.net/nethack/levels/nethack_levels.txt
https://bhaak.net/nethack/levels/unnethack_levels.txt

Junethack 2019 – the 9th NetHack Cross-Variant Summer Tournament

•1. June 2019 • Leave a Comment

The ninth installment of the annual NetHack Cross-Variant Summer Tournament called Junethack will start at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks.

This year, we have (again) the most variants ever available for you to play. 16 different variants of NetHack make the cross-variant achievements harder to get than in any years before. We don’t expect anybody to get them all (did I hear a “challenge accepted” there?). Because of this we have changed them and now you earn a new cross-variant achievement for every new milestone in a new variant.

dNetHack SLEX, notdNetHack, and EvilHack are new variants that are featured for the first time in the tournament, available for play on the hardfought.org and em.slashem.me public servers.

The clan trophy for fastest intended deaths by certain monsters has been suspended for this year, after various exploits have been discovered and used last year.

For the first time ever, a specific role/race combination has been excluded from tournament. The elven Anachrononaut has turned into a grindfest in its relentless fight against gorgoniankind and needs to take a break and let other roles and races kill Medusa.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Freenode IRC network.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

UnNetHack 5.2.0 – “Happy New Year” released!

•1. January 2019 • 5 Comments

Announcing the release of UnNetHack 5.2.0.

Source code and Windows TTY and GUI executables can be downloaded here:
http://sourceforge.net/projects/unnethack/files/unnethack/5.2.0/. Homebrew for OSX has also already been updated.

This is a release that brings the released version up to speed with the version that is already deployed at public servers. It is not bones and saves compatible with the previously released version 5.1.0.

The (mostly) complete ChangeLog is in the download packages or can be read online at https://github.com/UnNetHack/UnNetHack/blob/5.2.x/ChangeLog

Besides lots of bugfixes, some of the more notable changes include:

Reproducible dungeon layout. Set OPTIONS=seed:number to get the same dungeon layout for new games. The seed number is noted in the dumplog.

item changes:

  • Decreased weight of all mithril armor, especially ones of elven make
  • Wand wresting happens always when an empty wand is zapped with a chance depending on BUC state if an effect is produced
  • Putting blessed and uncursed scrolls of charging into a bag of tricks charges it
  • Picking up scrolls of scare monsters change BUC from blessed to uncursed to cursed
  • Picked up scrolls of scare monsters only turn to dust if they are cursed

user interface improvements:

  • Automatically identify scrolls of scare monster during pickup when turning to dust or changing known BUC state
  • Don’t autopickup unpaid items
  • Change default of shopkeeper sell prompt to ‘n’
  • Added naming last broken object to #name menu
  • Added stone, strangled, levitation, flying, and riding indicators to the status line
  • Status indicators on third status line if the terminal is high enough (example with every indicator turned on)
  • Pressing > or < autotravels to the stairs if not already on the right kind of stairs
  • customizable colors:

 

Happy Hacking!

Junethack 2018 – the 8th NetHack Cross-Variant Summer Tournament

•31. May 2018 • 1 Comment

The eigth installment of the annual NetHack Cross-Variant Summer Tournament called Junethack will start on Friday at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks.

This year, we have the most variants to play ever. 13 different variants of NetHack make the cross-variant achievements harder to get than in any years before. So we have decided to adjust the cross-variant achievements but with keeping the tradition of making changes at the very last minute, the changes to the cross-variant achievements haven’t been done yet and will be finalized after the start of the tournament.

NetHack 1.3d, the first ever version of NetHack is again part of the tournament thanks to the variant happy people of hardfought.org which are also to thank for again having a public server in Australia.

xNetHack and SpliceHack are new variants that are featured for the first time in the tournament, both available for play on the hardfought.org public servers.

UnNetHack has a lot of new achievements. Essentially killing any unique monster will earn an achievement and killing every unique of certain classes (e.g. riders or quest leaders) will earn you an additional achievement.

The clan trophy for log(points) has been dropped, as it was too easily abused. A replacement is planned but not yet finished.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Freenode IRC network.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

Junethack 2017 – the 7th NetHack Cross-Variant Summer Tournament

•2. June 2017 • Leave a Comment

The seventh installment of the annual NetHack Cross-Variant Summer Tournament called Junethack has started on Thursday at midnight UTC and will be running until June 30 midnight UTC.

This tournament is trying to appeal not only to the hardcore serial ascenders but also to players that get constantly mangled and beaten to death in unrealistic brutal situations by this sadistic game (that means probably you) by offering various non-winning achievements and encouraging people to try and play NetHack forks.

This year, we have the most variants to play ever. 11 different variants of NetHack make the cross-variant trophies harder to get than in any years before.

You participate in the tournament by playing NetHack 3.6, NetHack 3.4.3, SporkHack, UnNethack, GruntHack, dNetHack, NetHack4, NetHack Fourk, DynaHack, FIQhack, or Slash’EM Extended on the supported public servers and linking those accounts on your Junethack user page.

You can get in contact with the organizers of Junethack here, on reddit, on rec.games.roguelike.nethack, and in the #junethack IRC channel on the Freenode IRC network.

If you want to help out in the development of the tournament, you can check out our code from GitHub.

Register at the tournament homepage and start venturing into the Dungeons of Doom!

Saving save games

•15. September 2014 • Leave a Comment

This text has been written by the admin of un.nethack.nu, regarding the recent move of the public servers onto new machines. You can also read it on his blog.

Long post so TL;DR for lazy people:

Patched 47 binaries to achieve backwards compatibility with old saves

I decided to redo the setup for un.nethack.nu mostly from scratch, to make it easier to support shared state between multiple servers. For historical reasons unnethack ran as uid 5. This uid belongs to the “games” user in debian, which the original server ran. Nowadays I prefer CentOS where uid 5 by default belongs to the username “sync”.

So I took the opportunity to use a dedicated user with a non-system uid. This became a pretty big issue since nethack also writes the uid to the save file and if it doesn’t match what it’s running as when you restore it’ll helpfully tell you the save file is not yours and delete it.

Brainstorming in the unnethack IRC channel a few options came up:

  1. Move back to uid 5
  2. Recompile all the binaries without the check
  3. Remove the uid check in the binaries
  4. Try to replace the uid inside the save files

The first is the ugly solution, which I see as a last resort. Investigation of the code writing the save files found that the uid was being written to a non-fixed position. This makes it pretty risky to mess with it. Removing the uid check and recompiling was done for the latest binary. Unfortunately there are currently 48 different versions of unnethack with version dependent saves. Even if I had some kind of tag or reference to the point in history where each binary was built I wouldn’t rely on being able to get the exact same binary. So investigation began on the fourth option.

Finding the code that does the uid check was easy, just need to look for the string being printed when it happens, “Saved game was not yours.”. The uid check and printing seems to be done in “restgamestate” in restore.c:

  STATIC_OVL
  boolean
  restgamestate(fd, stuckid, steedid)
  register int fd;
  unsigned int *stuckid, *steedid;  /* STEED */
  {
      /* discover is actually flags.explore */
      boolean remember_discover = discover;
      struct obj *otmp;
      int uid;
  
      mread(fd, (genericptr_t) &uid, sizeof uid);
      if (uid != getuid()) {    /* strange ... */
        /* for wizard mode, issue a reminder; for others, treat it
           as an attempt to cheat and refuse to restore this file */
        pline("Saved game was not yours.");
  #ifdef WIZARD
        if (!wizard)
  #endif
          return FALSE;
      }
  // More code...

And restgamestate is being called by dorecover (strange name for a function that restores save files…), also in restore.c:

  int
  dorecover(fd)
  register int fd;
  {
      unsigned int stuckid = 0, steedid = 0;  /* not a register */
      xchar ltmp;
      int rtmp;
      struct obj *otmp;
  
  #ifdef STORE_PLNAME_IN_FILE
      mread(fd, (genericptr_t) plname, PL_NSIZ);
  #endif
  
      restoring = TRUE;
      getlev(fd, 0, (xchar)0, FALSE);
      if (!restgamestate(fd, &stuckid, &steedid)) {
          display_nhwindow(WIN_MESSAGE, TRUE);
          savelev(-1, 0, FREE_SAVE);    /* discard current level */
          (void) close(fd);
          (void) delete_savefile();
          restoring = FALSE;
          return(0);
      }
      
  // More code...

So if the uid check in “restgamestate” fails it will return FALSE and cause “dorecover” to delete the save file. It seems the easiest solution would be to remove the “return FALSE” in the uid check. Now we need to find this part in the binary. The best and easiest way I found to disassemble in linux is to use objdump:

    $ objdump -D -S -g -t -T unnethack.45 | less -i

I started by searching for calls to getuid and looking at what function they were in. After a number of hits I find a call to it in “dorecover”:

000000000050cf10 :
  .....
  50cf4f:  e8 fc ef ff ff   callq  50bf50 
  50cf54:  44 8b 64 24 14   mov    0x14(%rsp),%r12d
  50cf59:  e8 82 14 0f 00   callq  5fe3e0 
  50cf5e:  41 39 c4         cmp    %eax,%r12d
  50cf61:  74 19            je     50cf7c 
  50cf63:  31 c0            xor    %eax,%eax
  50cf65:  bf 06 48 6a 00   mov    $0x6a4806,%edi
  50cf6a:  e8 21 f3 fd ff   callq  4ec290 
  .....

But getuid isn’t called in dorecover. The nearby calls (mread, pline) actually looks like the code from restgamestate. Lets see what’s being put in the edi register by the mov instruction before the call to pline by looking at the string data in the binary:

    $ objdump -s -j .rodata unnethack.45 | less -i

Searching for ” 6a48″ finds the place (6a4800 and then 6 characters in):

  6a4800 74202564 21005361 76656420 67616d65  t %d!.Saved game
  6a4810 20776173 206e6f74 20796f75 72732e00   was not yours..

So it is the code we’re looking for, it must have been inlined. Let’s analyze what that part actually does:

000000000050cf10 :
  .....
  50cf4f:  e8 fc ef ff ff       callq  50bf50            # read piece of the file
  50cf54:  44 8b 64 24 14       mov    0x14(%rsp),%r12d         # move into %r12d (i think)
  50cf59:  e8 82 14 0f 00       callq  5fe3e0         # call getuid
  50cf5e:  41 39 c4             cmp    %eax,%r12d               # compare result to %r12d
  50cf61:  74 19                je     50cf7c   # if the same, jump to 50cf7c -|
  50cf63:  31 c0                xor    %eax,%eax                # set %eax to 0                |
  50cf65:  bf 06 48 6a 00       mov    $0x6a4806,%edi           # 6a4806: "Saved game was not yours."
  50cf6a:  e8 21 f3 fd ff       callq  4ec290            # print it                     |
  50cf6f:  80 3d 94 4c 42 00 00 cmpb   $0x0,0x424c94(%rip)      # 931c0a  check if field at flags + 0xA is equal to 0 (most likely WIZARD flag)
  50cf76:  0f 84 66 03 00 00    je     50d2e2  # if so, jump to 50d2e2 -|     |
  50cf7c:  ba d0 00 00 00       mov    $0xd0,%edx               #                        |   <-| Here!
  50cf81:  be 00 1c 93 00       mov    $0x931c00,%esi           #                        |
  50cf86:  89 df                mov    %ebx,%edi                #                        |
  50cf88:  e8 c3 ef ff ff       callq  50bf50            #                        |
                                                                #                        |
  .....                                                         #                        |
                                                                #                        |  
  50d2e2:  be 01 00 00 00       mov    $0x1,%esi                #                      <-| Here! 
  50d2e7:  8b 3d 47 dd 41 00    mov    0x41dd47(%rip),%edi      # 92b034 
  50d2ed:  ff 15 ed 0e 45 00    callq  *0x450eed(%rip)          # 95e1e0 
  50d2f3:  ba 04 00 00 00       mov    $0x4,%edx
  50d2f8:  31 f6                xor    %esi,%esi
  50d2fa:  bf ff ff ff ff       mov    $0xffffffff,%edi
  50d2ff:  e8 3c 40 00 00       callq  511340 
  50d304:  89 df                mov    %ebx,%edi
  50d306:  e8 35 3b 10 00       callq  610e40 
  50d30b:  e8 e0 cd f5 ff       callq  46a0f0 
  50d310:  c6 05 69 01 43 00 00 movb   $0x0,0x430169(%rip)      # 93d480 
  50d317:  48 83 c4 20          add    $0x20,%rsp
  50d31b:  31 c0                xor    %eax,%eax
  50d31d:  5b                   pop    %rbx
  50d31e:  5d                   pop    %rbp
  50d31f:  41 5c                pop    %r12
  50d321:  c3                   retq

The patch needed turned out to be very simple, change the jump at 50cf61 to an unconditional one.

Looking at an x86 opcode and instructions reference we can find that the JMP (unconditional jump) opcode of the same type as the JE (opcode 0x74, rel8, jump relative to position) is 0xEB. Looking through a few unnethack binaries the disassembled output is very similar, following a pattern for 32-bit and another for 64-bit. So I wrote a fuzzy patcher:

    #!/usr/bin/env python
    import sys
    import subprocess
    
    patch_64 = {"needle": [0x44, '*', '*', '*', '*',
                           0xe8, '*', '*', '*', '*',
                           0x41, '*', '*',
                           0x74, 0x19,
                           0x31, 0xc0,
                           0xbf],
                "pos": 13,
                "new": [0xEB]}
    patch_32 = {"needle": [0x8b, '*', '*',
                           0xe8, '*', '*', '*', '*',
                           0x39, 0xc3,
                           0x74, 0x19,
                           0xc7],
                "pos": 10,
                "new": [0xEB]}
    
    def run(cmd):
        return subprocess.Popen(cmd,
                                stdout=subprocess.PIPE).communicate()[0]
        
    def main():
        if len(sys.argv) == 1:
            print("usage: patcher.py ")
            return
    
        fn = sys.argv[1]
        print("opening %s" % fn)
    
        output = run(['file', fn])
        if output.find('64-bit') > -1:
            print("64-bit binary")
            patch = patch_64
        elif output.find('32-bit') > -1:
            print("32-bit binary")
            patch = patch_32
        else:
            raise Exception("Unknown binary: %s" % output)
        
            
        f = open(fn, 'r+b')
        b = f.read(1)
        fpos = 1
        npos = 0
        needle = patch["needle"]
        spos = -1
        while not b == "":
            b = ord(b)
            if npos == len(needle):
                print "found it"
                if not spos == -1:
                    raise Exception("Expected to find pattern only once")
                spos = fpos - 1
                npos = 0
            if needle[npos] == '*' or needle[npos] == b:
                npos = npos + 1
            else:
                npos = 0
            b = f.read(1)
            fpos = fpos + 1
    
        if spos == -1:
            raise Exception("Could not find pattern")
    
        f.seek(spos - len(needle) + patch["pos"])
        b = ord(f.read(1))
        assert b == needle[patch["pos"]]
    
        print("writing patch")
        f.seek(spos - len(needle) + patch["pos"])
        for b in patch["new"]:
            f.write(chr(b))
    
        print("done!")
        
    if __name__ == "__main__":
        main()

This worked for all but one binary, where the compiler had not inlined the restgamestate call. It was easily patched manually, with the help of emacs wonderful hexl-mode to hexedit the binary.