modpatch.el is an Emacs minor mode for producing and maintaining patch files instead of editing the original sources in place.
It solves the following workflow:
- You want to edit
Assets/Lua/foo.lua, but you are not allowed to (or don't want to) commit direct changes there. - Instead, you want to maintain one or more
.patchfiles somewhere under your mods directory (for exampleMods/MyBalanceMod/Lua/foo.lua.patch) that describe howfoo.luashould be changed. - You want Emacs to show you the final, patched version while you're working, and treat that as the "real" buffer text.
- When you save, you do not want to overwrite
Assets/Lua/foo.lua. You want to regenerate the unified diff and write that to your patch file(s). - Sometimes you really do want to edit the real upstream file, and when you do, you want your patch file(s) automatically rewritten so they still produce the desired result against the new upstream version.
modpatch.el makes that workflow first-class.
It provides:
- A "patch-authoring" mode where saving writes
.patchfiles instead of writing to the original file. - A "rebase" mode where you temporarily edit the original file on disk and patches get regenerated on save.
- Support for multiple patch variants per base file, and an interactive command to switch between them.
- Automatic activation: reopening a file you've patched automatically shows the patched view and puts you back in patch-authoring mode.
- Persistence of associations (which base file maps to which patch files, and what the intended modded result is).
The original file in the game/assets tree. Example:
/game/Assets/Lua/testmodule.lua
Unified diff files that describe modifications to the base file. Example:
/mods/balance/Lua/testmodule.lua.patch
/mods/cheats/Lua/testmodule.lua.patch
Each patch file corresponds to a variant. You may choose which one you're authoring at any given time.
The full text of "what you want the game to see" for that file after all edits are applied. In normal patch-authoring mode, your buffer shows the desired text, not the original base file.
modpatch.el keeps track of the most recent desired text for each base file so it can:
- regenerate patches,
- restore buffers after reload,
- rebase cleanly when the base file changes.
Place modpatch.el in some directory, for example ~/.emacs.d/modpatch/.
Then in your init:
(add-to-list 'load-path (expand-file-name "modpatch" "~/.emacs.d"))
(load-file (expand-file-name "modpatch/modpatch.el" "~/.emacs.d"))This loads the package, defines the minor modes, and also loads any saved associations from the default modpatch-associations-file path (see below).
modpatch.el keeps a global hash table mapping each base file to:
- its patch files,
- its last known desired text.
This table is persisted to disk so Emacs can restore state between sessions.
You can choose where that state is stored by setting modpatch-associations-file before calling modpatch-load-associations, like this:
(setq modpatch-associations-file "/path/to/modpatch-assoc.el")
(modpatch-load-associations)
(add-to-list 'load-path (expand-file-name "modpatch" "~/.emacs.d"))
(load-file (expand-file-name "modpatch/modpatch.el" "~/.emacs.d"))This does two things:
- It sets where associations will be read from and written to.
- It then loads the table from that file before you start working.
You can also change the associations file later while Emacs is already running. For example:
(setq modpatch-associations-file "/some/other/location/alt-assoc.el")
(modpatch-load-associations)After that call, modpatch.el will start using the new file for persistence going forward. This lets you maintain different sets of mod associations in different projects.
-
You open a base file.
-
modpatch.elsees that the file has patches associated with it, applies the patch (or selected patch) to generate the desired text, replaces the buffer contents with that desired text, and enablesmodpatch-mode.At this point:
- What you see in the buffer is your modded version, not the raw file on disk.
- Saving does not touch the base file. It regenerates patch files.
-
You edit and save:
- The buffer diff is computed against the current on-disk base file.
- The unified diff is written to the active patch file (or all associated patch files if no active one is selected).
- The buffer is marked clean (Emacs believes it was saved).
- The base file on disk is left untouched.
-
You need to update upstream (for example because the game updated its Lua scripts). You switch to rebase mode:
- The buffer is replaced with the true on-disk base file.
- Saves now write to disk normally.
- After each save, modpatch automatically regenerates the patch file(s) so they still apply cleanly.
- When done, you exit rebase mode; modpatch reconstructs the desired text from the latest patch file(s) on disk and puts you back in patch-authoring mode.
-
You can maintain multiple patch variants for the same base file and interactively switch which variant you're authoring.
Internally, modpatch.el keeps a hash table modpatch--table:
-
Key: absolute path of a base file.
-
Value: plist with
:patches→ list of absolute patch file paths for that base file.:desired→ last known desired text for that base file.
Each buffer visiting a base file also has buffer-local variables:
modpatch--base-file: the absolute path to the base file on disk.modpatch--active-patch-file: the currently selected patch variant for this buffer (or nil if not narrowed to a single variant).modpatch--rebase-mode-p: non-nil if we are currently editing the real base file on disk (rebase mode).
modpatch.el installs a find-file-hook (modpatch-maybe-activate).
When you open a file X:
-
If
Xdoes not have an entry inmodpatch--table, nothing special happens; you just visit the file normally. -
If
Xdoes have an entry:- The base file on disk is read.
- The associated patch file(s) are applied to produce the desired text. If we are tracking a currently active patch variant, that variant is used. Otherwise all known patches are applied in order.
- The buffer contents are replaced with the desired text.
- The buffer is marked unmodified.
modpatch-modeis turned on in that buffer.
From that point on, you are in patch-authoring mode for that file.
This is the default mode when modpatch-mode is on and we are not in rebase mode.
In this mode:
- The buffer contents are the desired, modded version.
- The real file on disk is not shown.
- Saving does not write the buffer to disk. Instead,
modpatch.elintercepts the save viawrite-contents-functions.
When you hit C-x C-s:
-
It reads the current on-disk content of the base file.
-
It diffs that against your buffer content, generating a unified diff.
-
It writes that diff to one or more patch files:
- If
modpatch--active-patch-fileis non-nil, only that patch file is updated. - Otherwise, all patch files in
:patchesare updated.
- If
-
It updates the
:desiredentry for this base file inmodpatch--tableto match the buffer content. -
It marks the buffer clean (not modified).
The base file on disk is left unchanged.
Key bindings in this mode:
-
C-c m amodpatch-add-patch-targetAdd a new patch file path for this base file. After calling this, saving will start updating that patch file. -
C-c m pmodpatch-select-active-patchChoose which patch file you are currently authoring. This does three things:- Prompts you with completion over all known patch files for this base.
- Applies only that selected patch file to the base file to compute what the buffer should look like.
- Replaces the current buffer with that result and records that selected patch file in
modpatch--active-patch-file.
-
C-c m rmodpatch-enter-rebase-modeSwitch into rebase mode (described below). -
C-c m Rmodpatch-exit-rebase-modeSwitch back to patch-authoring mode (when already in rebase mode). -
C-c m smodpatch-save-associationsPersist the current association table (modpatch--table) to disk immediately.
A single base file can have multiple .patch files associated with it. Example:
/mods/Balance/Lua/testmodule.lua.patch/mods/Cheats/Lua/testmodule.lua.patch
These represent different variants.
When in patch-authoring mode:
- Run
C-c m p(modpatch-select-active-patch). - You will be prompted to pick one of the known patch files.
- The buffer will be rebuilt to show what the base file would look like after applying only that chosen patch file.
- From now on, saving updates only that chosen patch file.
- The choice is remembered in
modpatch--active-patch-filefor this buffer.
Sometimes upstream changes and you need to reconcile your mod with a new version of the base file. This is what rebase mode is for.
Enter rebase mode with C-c m r (modpatch-enter-rebase-mode) while in patch-authoring mode.
When you enter rebase mode:
-
Your current buffer (which is showing the desired text) is committed to disk as the authoritative desired state:
- The unified diff between the on-disk base file and your buffer is generated.
- The associated patch file(s) are updated.
- The association entry’s
:desiredis updated to match your buffer contents.
-
Then the buffer is replaced with the real on-disk content of the base file. You now see the true upstream file.
-
modpatch--rebase-mode-pis set to non-nil. -
An
after-save-hookis installed so that every time you save in this mode,modpatch.elregenerates the patch files against the new upstream content.
Effectively, you are now editing the base file directly. Saving (C-x C-s) writes to disk like normal.
After each save in rebase mode:
modpatch.elcomputes the diff between the new base file text on disk and the last known desired text (the modded result you are targeting).- That diff is written to the relevant patch file(s) so the patch stays valid against the updated base.
This is what keeps your mod patches from going stale when the upstream file changes.
When you are done rebasing, run C-c m R (modpatch-exit-rebase-mode).
When you exit rebase mode:
-
modpatch.elrecomputes the modded view to show in the buffer. It does this by reading the latest patch file(s) off disk, applying them to the now-current base file, and rebuilding the desired text fresh. This ensures you see the newest patch output, not a cached copy.- If you have an active patch file (
modpatch--active-patch-file), it prefers that one so you remain focused on the same variant you were editing.
- If you have an active patch file (
-
The buffer is replaced with that recomputed modded view.
-
The
after-save-hookis removed. -
modpatch--rebase-mode-pis set back to nil. -
You are back in patch-authoring mode, where saving again updates patch files instead of touching the base file on disk.
You can re-enter rebase mode at any time.
All commands below assume modpatch-mode is active in the buffer.
-
modpatch-add-patch-targetKey:C-c m aAdd a patch file to the association for this base file. You typically point it somewhere in your mod directory, e.g.Mods/MyMod/Lua/testmodule.lua.patch. -
modpatch-select-active-patchKey:C-c m pChoose which patch file is currently active in this buffer. The buffer is rebuilt from disk using only that patch, and further saves update only that patch. -
modpatch-enter-rebase-modeKey:C-c m rSwitch to rebase mode:- Save your current desired modded result into patch form.
- Show you the real upstream file.
- From now on, saving writes upstream and auto-regenerates the patch file(s) to stay aligned.
-
modpatch-exit-rebase-modeKey:C-c m RLeave rebase mode:- Reconstruct the modded view from the (now updated) base file plus the latest patch file(s).
- Return to patch-authoring behavior.
-
modpatch-save-associationsKey:C-c m sWrite the current association table (modpatch--table) tomodpatch-associations-file. The file is Lisp-readable. You can version this file in git alongside your mods. -
modpatch-load-associationsManually reload the association table frommodpatch-associations-file. You can call this after changingmodpatch-associations-fileto point Emacs at a different set of mod metadata.
-
Open
Assets/Lua/testmodule.lua. If this file has never been patched before, Emacs opens it normally. -
Run
M-x modpatch-mode. The mode records the file path and sets up buffer-local state. Then runC-c m a(modpatch-add-patch-target) and give it a patch file path, for example:Mods/MyBalanceMod/Lua/testmodule.lua.patch -
Edit the file in-place (you're seeing your desired version of the file).
-
C-x C-s. Instead of saving toAssets/Lua/testmodule.lua, modpatch:- Differs the current on-disk original vs your buffer.
- Writes that diff as a unified patch to
Mods/MyBalanceMod/Lua/testmodule.lua.patch. - Marks the buffer unmodified.
-
Later, upstream updates
Assets/Lua/testmodule.lua. RunC-c m rto enter rebase mode.- Your current mod view is captured and patches updated.
- Buffer now shows the new upstream version directly. Edit and save; each save rewrites your patch file so it still represents your desired changes relative to the new upstream file.
-
Run
C-c m Rto exit rebase mode.- Buffer is reconstructed by applying your latest patch file(s) to the updated upstream.
- You are back in patch-authoring mode.
-
If you maintain multiple patch variants (for example a “balance” version and a “cheats” version), you can switch which variant you're editing via
C-c m p(modpatch-select-active-patch). The buffer will be rebuilt using only that selected patch file, and future saves will update that specific patch file.
-
diffandpatchmust be available in your environment.modpatch.elshells out to:diff -uto generate unified diffspatchto apply them If you are on Windows, you can install these via MSYS2 or Git for Windows. Alternatively, you can reworkmodpatch--generate-diffandmodpatch--apply-patchto use pure Emacs primitives (diff-no-select,epatch-buffer).
-
Paths in the generated patch header are normalized so that patch applies cleanly regardless of the actual directory layout. Internally we rewrite the header lines (
---/+++) to a neutral filename before runningpatch. -
modpatch.elnever silently writes to the base file in patch-authoring mode. The only time it will write to the real base file is in rebase mode, which you enter explicitly withC-c m r. -
You can keep the association file (
modpatch-associations-file) under version control in your mod project so that your patch relationships, desired texts, and active variants survive across sessions and machines.
This completes the feature set:
- Patch-authoring mode with redirected save.
- Rebase mode for upstream changes.
- Multiple patch variants per base file and interactive switching.
- Automatic reopening into the patched view.
- Persistence across sessions, with configurable storage and reload.