New blog post about modmapper in hidden mode
@ -0,0 +1,692 @@
|
|||||||
|
---
|
||||||
|
title: "Modmapper: Putting every Skyrim mod on a map with Rust"
|
||||||
|
layout: post
|
||||||
|
image: /img/blog/modmapper.jpg
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
|
|
||||||
|
[Modmapper](https://modmapper.com) is a website that I made that puts every mod
|
||||||
|
for the game [Elder Scrolls V:
|
||||||
|
Skyrim](https://en.wikipedia.org/wiki/The_Elder_Scrolls_V:_Skyrim) uploaded to
|
||||||
|
[Nexus Mods](https://www.nexusmods.com/) on an interactive map.
|
||||||
|
|
||||||
|
<a href="https://modmapper.com" target="_blank">
|
||||||
|
![Screenshot of modmapper.com](/img/blog/modmapper.jpg)
|
||||||
|
</a>
|
||||||
|
|
||||||
|
You can view the map at [https://modmapper.com](https://modmapper.com).
|
||||||
|
|
||||||
|
Released in 2011, Skyrim is over a decade old now. But, its vast modding
|
||||||
|
community has kept it alive and relevant to this day. [Skyrim is still in the
|
||||||
|
top 50 games being played on Steam in 2022](https://steamcharts.com/top/p.2) and
|
||||||
|
I think it's no coincidence that [it's also one of the most modded games
|
||||||
|
ever](https://www.nexusmods.com/games?).
|
||||||
|
|
||||||
|
<!--excerpt-->
|
||||||
|
|
||||||
|
The enormous and enduring modding community around the Elder Scrolls games is
|
||||||
|
why I have a special fondness for the series. I was 13 when I first got
|
||||||
|
interested in programming through [making mods for Elder Scrolls IV:
|
||||||
|
Oblivion](https://www.nexusmods.com/users/512579?tab=user+files&BH=2). I quickly
|
||||||
|
realized I got way more satisfaction out of modding the game than actually
|
||||||
|
playing it. I was addicted to being able to create whatever my mind imagined in
|
||||||
|
my favorite game.
|
||||||
|
|
||||||
|
I was working on mod for Skyrim earlier in the year[^bazaarrealm] and was
|
||||||
|
looking for the best places to put new buildings in the game world. I really
|
||||||
|
wanted areas of the game world off the beaten (heavily-modded) path. After over
|
||||||
|
a decade of modifications, there could be conflicts with hundreds of mods in any
|
||||||
|
area I chose which could cause issues like multiple buildings overlapping or
|
||||||
|
terrain changes causing floating rocks and trees.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div class="row">
|
||||||
|
<figure>
|
||||||
|
<img alt="Example of a conflict between two mods that both chose the
|
||||||
|
same spot to put a lamp post and sign post so they are clipping"
|
||||||
|
src="/img/blog/modmapper-clipping-example2.jpg" />
|
||||||
|
<figurecaption>
|
||||||
|
<em>
|
||||||
|
Example of a conflict between two mods that both chose the same
|
||||||
|
spot to put a lamp post and sign post so they are clipping.
|
||||||
|
Screenshot by <a
|
||||||
|
href="https://www.nexusmods.com/users/63732336">
|
||||||
|
AndreySG</a>
|
||||||
|
</em>
|
||||||
|
</figurecaption>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<img alt="Example of a conflict between two mods that both chose the
|
||||||
|
same spot to put a building and rock so they are clipping"
|
||||||
|
src="/img/blog/modmapper-clipping-example1.jpg" />
|
||||||
|
<figurecaption>
|
||||||
|
<em>
|
||||||
|
Example of a conflict between two mods that both chose the same
|
||||||
|
spot to put a building and rock so they are clipping. Screenshot
|
||||||
|
by <a href="https://www.reddit.com/user/LewdManoSaurus">
|
||||||
|
LewdManoSaurus</a>
|
||||||
|
</em>
|
||||||
|
</figurecaption>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<img alt="Example of a conflict between two mods that both chose the
|
||||||
|
same spot to put a building and tree so they are clipping"
|
||||||
|
src="/img/blog/modmapper-clipping-example3.jpg" />
|
||||||
|
<figurecaption>
|
||||||
|
<em>
|
||||||
|
Example of a conflict between two mods that both chose the same
|
||||||
|
spot to put a building and tree so they are clipping. Screenshot
|
||||||
|
by <a
|
||||||
|
href="https://www.nexusmods.com/skyrimspecialedition/users/51448566">
|
||||||
|
Janquel</a>
|
||||||
|
</em>
|
||||||
|
</figurecaption>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<img alt="Example of a conflict between two mods that both chose the
|
||||||
|
same spot to put a woodcutting mill"
|
||||||
|
src="/img/blog/modmapper-clipping-example4.jpg" />
|
||||||
|
<figurecaption>
|
||||||
|
<em>
|
||||||
|
Example of a conflict between two mods that both chose the
|
||||||
|
same spot to put a woodcutting mill so they are clipping.
|
||||||
|
Screenshot by <a
|
||||||
|
href="https://www.nexusmods.com/skyrimspecialedition/users/51448566">
|
||||||
|
Janquel</a>
|
||||||
|
</em>
|
||||||
|
</figurecaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Mod authors usually use a tool like
|
||||||
|
[TES5Edit](https://www.nexusmods.com/skyrim/mods/25859) to analyze a group of
|
||||||
|
mod plugins to find conflicts and create patches to resolve them on a
|
||||||
|
case-by-case basis. But, I was unsatisfied with that. I wanted to be assured
|
||||||
|
that there would be no conflicts, or at least know the set of all possible mods
|
||||||
|
out there that could conflict so I could manually patch those few mods. There
|
||||||
|
was no good solution for finding conflicts across all mods though. Mod authors
|
||||||
|
would need to download every Skyrim mod ever and no one has time to download all
|
||||||
|
85,000+ Skyrim mods, and no one has the computer memory to load all of those in
|
||||||
|
TES5Edit at the same time.
|
||||||
|
|
||||||
|
Through that frustration, Modmapper was born with the mission to create a
|
||||||
|
database of all Skyrim mod exterior cell edits. With that database I can power
|
||||||
|
the website which visualizes how popular cells are in aggregate as well as allow
|
||||||
|
the user to drill down to individual cells, mods, or plugins to find potential
|
||||||
|
conflicts without ever having to download files themselves.
|
||||||
|
|
||||||
|
When I [released the website about 7 months
|
||||||
|
ago](https://www.reddit.com/r/skyrimmods/comments/sr8k4d/modmapper_over_14_million_cell_edits_from_every/)
|
||||||
|
it made a big splash in the Skyrim modding community. No one had ever visualized
|
||||||
|
mods on a map like this before, and it gave everyone a new perspective on the
|
||||||
|
vast library of Skyrim mods. It was even [featured on the front page of PC
|
||||||
|
Gamer's
|
||||||
|
website](https://www.pcgamer.com/skyrim-modmapper-is-a-weirdly-beautiful-way-to-manage-your-mods/).
|
||||||
|
Thirteen-year-old me, who regularly read the monthly PC Gamer magazine, would
|
||||||
|
have been astounded.
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://www.pcgamer.com/skyrim-modmapper-is-a-weirdly-beautiful-way-to-manage-your-mods/"
|
||||||
|
target="_blank">
|
||||||
|
![Screenshot of PC Gamer article titled "Skyrim Modmapper is a weirdly
|
||||||
|
beautiful way to manage your mods" by Robert Zak published April 20,
|
||||||
|
2022](/img/blog/modmapper-pcgamer.jpg)
|
||||||
|
</a>
|
||||||
|
|
||||||
|
The comments posted to the initial mod I posted on Nexus Mods[^takedown] for the
|
||||||
|
project were very amusing. It seemed to be blowing their minds:
|
||||||
|
|
||||||
|
> "Quite possibly this could be the best mod for
|
||||||
|
Skyrim. This hands-down makes everyone's life easier to be able to see which of
|
||||||
|
their mods might be conflicting." -- [Nexus Mods comment by
|
||||||
|
lorddonk](/img/blog/modmapper-comment15.png)
|
||||||
|
|
||||||
|
> "The 8th wonder of Skyrim. That's a Titan's work requiring a monk's
|
||||||
|
> perserverance. Finally, a place to go check (in)compatibilities !!! Voted.
|
||||||
|
> Endorsed." -- [Nexus Mods comment by
|
||||||
|
> jfjb2005](/img/blog/modmapper-comment3.png)
|
||||||
|
|
||||||
|
> "They shall sing songs of your greatness! Wow, just wow." -- [Nexus Mods
|
||||||
|
> comment by
|
||||||
|
LumenMystic](/img/blog/modmapper-comment7.png)
|
||||||
|
|
||||||
|
> "Holy Batman Tits! Be honest..... You're a Govt Agent and made this mod during
|
||||||
|
> your "Terrorist Watch Shift" using a CIA super computer.." -- [Nexus Mods
|
||||||
|
comment by toddrizzle](/img/blog/modmapper-comment1.png)
|
||||||
|
|
||||||
|
> "What drugs are you on and can I have some?" -- [Nexus Mods comment by
|
||||||
|
> thappysnek](/img/blog/modmapper-comment11.png)
|
||||||
|
|
||||||
|
> "This is madness! Author are some kind of overhuman?! GREAT work!"-- [Nexus
|
||||||
|
> Mods comment by TeodorWild](/img/blog/modmapper-comment10.png)
|
||||||
|
|
||||||
|
> "You are an absolute legend. Bards will sing tales of your exploits" -- [Nexus
|
||||||
|
> Mods comment by burntwater](/img/blog/modmapper-comment2.png)
|
||||||
|
|
||||||
|
> "I wanted to say something, but I'll just kneel before thee and worship. This
|
||||||
|
> would have taken me a lifetime. Amazing." -- [Nexus Mods comment by
|
||||||
|
> BlueGunk](/img/blog/modmapper-comment8.png)
|
||||||
|
|
||||||
|
> "Finally found the real dragonborn" -- [Nexus Mods comment by
|
||||||
|
> yag1z](/img/blog/modmapper-comment6.png)
|
||||||
|
|
||||||
|
> "he is the messiah!" -- [Nexus Mods comment by
|
||||||
|
> Cursedobjects](/img/blog/modmapper-comment12.png)
|
||||||
|
|
||||||
|
> "A god amongst men." -- [Nexus Mods comment by
|
||||||
|
> TheMotherRobbit](/img/blog/modmapper-comment13.png)
|
||||||
|
|
||||||
|
Apparently knowing how to program is now a god-like ability! This is the type of
|
||||||
|
feedback most programmers aspire to get from their users. I knew the tool was
|
||||||
|
neat and fun to build, but I didn't realize it was *that* sorely needed by the
|
||||||
|
community.
|
||||||
|
|
||||||
|
Today, Modmapper has a sustained user-base of around 7.5k unique visitors a
|
||||||
|
month[^analytics] and I still see it mentioned in reddit threads or discord
|
||||||
|
servers whenever someone is asking about the places a mod edits or what mods
|
||||||
|
might be conflicting in a particular cell.
|
||||||
|
|
||||||
|
The rest of this blog post will delve into how I built the website and how I
|
||||||
|
gathered all of the data necessary to display the visualization.
|
||||||
|
|
||||||
|
### Downloading ALL THE MODS!
|
||||||
|
|
||||||
|
![Meme with the title "DOWNLOAD ALL THE MODS!"](/img/blog/allthemods.jpg)
|
||||||
|
|
||||||
|
In order for the project to work I needed to collect all the Skyrim mod plugin
|
||||||
|
files.
|
||||||
|
|
||||||
|
While there are a number of places people upload Skyrim mods, [Nexus
|
||||||
|
Mods](https://nexusmods.com) is conveniently the most popular and has the vast
|
||||||
|
majority of mods. So, I would only need to deal with this one source. Luckily,
|
||||||
|
[they have a nice API
|
||||||
|
handy](https://app.swaggerhub.com/apis-docs/NexusMods/nexus-mods_public_api_params_in_form_data/1.0).
|
||||||
|
|
||||||
|
[modmapper](https://github.com/thallada/modmapper) is the project I created to
|
||||||
|
do this. It is a Rust binary that:
|
||||||
|
|
||||||
|
* Uses [reqwest](https://crates.io/crates/reqwest) to make requests to [Nexus
|
||||||
|
Mods](https://nexusmods.com) for pages of last updated mods.
|
||||||
|
* Uses [scraper](https://crates.io/crates/scraper) to scrape the HTML for
|
||||||
|
individual mod metadata (since the Nexus API doesn't provide an endpoint to
|
||||||
|
list mods).
|
||||||
|
* Makes requests to the Nexus Mods API to get file and download information for
|
||||||
|
each mod, using [serde](https://serde.rs/) to parse the
|
||||||
|
[JSON](https://en.wikipedia.org/wiki/JSON) responses.
|
||||||
|
* Requests the content preview data for each file and walks through the list of
|
||||||
|
files in the archive looking for a Skyrim plugin file (`.esp`, `.esm`, or
|
||||||
|
`.esl`).
|
||||||
|
* If it finds a plugin, it decides to download the mod. It hits the download API
|
||||||
|
to get a download link and downloads the mod file archive.
|
||||||
|
* Then it extracts the archive using one of:
|
||||||
|
[compress_tools](https://crates.io/crates/compress-tools),
|
||||||
|
[unrar](https://crates.io/crates/unrar), or [7zip](https://www.7-zip.org/) via
|
||||||
|
[`std::process::Commmand`](https://doc.rust-lang.org/std/process/struct.Command.html)
|
||||||
|
(depending on what type of archive it is).
|
||||||
|
* With the ESP files (Elder Scrolls Plugin files) extracted, I then use my
|
||||||
|
[skyrim-cell-dump](https://github.com/thallada/skyrim-cell-dump) library (more
|
||||||
|
on that later!) to extract all of the cell edits into structured data.
|
||||||
|
* Uses [seahash](https://crates.io/crates/seahash) to create a fast unique hash
|
||||||
|
for plugin files.
|
||||||
|
* It then saves all of this data to a [postgres](https://www.postgresql.org/)
|
||||||
|
database using the [sqlx crate](https://crates.io/crates/sqlx).
|
||||||
|
* Uses extensive logging with the [tracing
|
||||||
|
crate](https://crates.io/crates/tracing) so I can monitor the output and have
|
||||||
|
a history of a run to debug later if I discover an issue.
|
||||||
|
|
||||||
|
It is designed to be run as a nightly [cron](https://en.wikipedia.org/wiki/Cron)
|
||||||
|
job which downloads mods that have updated on Nexus Mods since the last run.
|
||||||
|
|
||||||
|
To keep costs for this project low, I decided to make the website entirely
|
||||||
|
static. So, instead of creating an API server that would have to be constantly
|
||||||
|
running to serve requests from the website by making queries directly to the
|
||||||
|
database, I would dump all of the data that the website needed from the database
|
||||||
|
to JSON files, then upload those files to [Amazon
|
||||||
|
S3](https://aws.amazon.com/s3/) and serve them through the [Cloudflare
|
||||||
|
CDN](https://www.cloudflare.com/cdn/) which has servers all over the world.
|
||||||
|
|
||||||
|
So, for example, every mod in the database has a JSON file uploaded to
|
||||||
|
`https://mods.modmapper.com/skyrimspecialedition/<nexus_mod_id>.json` and the
|
||||||
|
website frontend will fetch that file when a user clicks a link to that mod in
|
||||||
|
the UI.
|
||||||
|
|
||||||
|
The cost for S3 is pretty reasonable to me ($~3.5/month), and Cloudflare has a
|
||||||
|
[generous free tier](https://www.cloudflare.com/plans/#price-matrix) that allows
|
||||||
|
me to host everything through it for free.
|
||||||
|
|
||||||
|
The server that I actually run `modmapper` on to download all of the mods is a
|
||||||
|
server I already have at home that I also use for other purposes. The output of
|
||||||
|
each run is uploaded to S3, and I also make a full backup of the database and
|
||||||
|
plugin files to [Dropbox](https://www.dropbox.com).
|
||||||
|
|
||||||
|
A lot of people thought it was insane that I downloaded every mod[^adult-mods],
|
||||||
|
but in reality it wasn't too bad once I got all the issues resolved in
|
||||||
|
`modmapper`. I just let it run in the background all day and it would chug
|
||||||
|
through the list of mods one-by-one. Most of the time ended up being spent
|
||||||
|
waiting while the Nexus Mod's API hourly rate limit was reached on my
|
||||||
|
account.[^rate-limit]
|
||||||
|
|
||||||
|
As a result of this project I believe I now have the most complete set of all
|
||||||
|
Skyrim plugins to date (extracted plugins only without other textures, models,
|
||||||
|
etc.)[^plugin-collection]. Compressed, it totals around 99 GB, uncompressed: 191
|
||||||
|
GB.
|
||||||
|
|
||||||
|
[After I downloaded Skyrim Classic mods in addition to Skyrim Special
|
||||||
|
Edition](#finishing-the-collection-by-adding-all-skyrim-classic-mods), here are
|
||||||
|
some counts from the database:
|
||||||
|
|
||||||
|
|:---|:---|
|
||||||
|
| **Mods** | 113,028 |
|
||||||
|
| **Files** | 330,487 |
|
||||||
|
| **Plugins** | 534,831 |
|
||||||
|
| **Plugin Cell Edits** | 33,464,556 |
|
||||||
|
|
||||||
|
### Parsing Skyrim plugin files
|
||||||
|
|
||||||
|
The Skyrim game engine has a concept of
|
||||||
|
[worldspaces](https://en.uesp.net/wiki/Skyrim:Worldspaces) which are exterior
|
||||||
|
areas where the player can travel to. The biggest of these being, of course,
|
||||||
|
Skyrim itself (which, in the lore, is a province of the continent of
|
||||||
|
[Tamriel](https://en.uesp.net/wiki/Lore:Tamriel) on the planet
|
||||||
|
[Nirn](https://en.uesp.net/wiki/Lore:Nirn)). Worldspaces are recorded in a
|
||||||
|
plugin file as [WRLD
|
||||||
|
records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/WRLD).
|
||||||
|
|
||||||
|
Worldspaces are then chunked up into a square grid of cells. The Skyrim
|
||||||
|
worldspace consists of a little over 11,000 square cells. Mods that make a
|
||||||
|
changes to the game world have a record in the plugin (a [CELL
|
||||||
|
record](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/CELL)) with the
|
||||||
|
cell's X and Y coordinates and a list changes in that cell.
|
||||||
|
|
||||||
|
There is some prior art ([esplugin](https://github.com/Ortham/esplugin),
|
||||||
|
[TES5Edit](https://github.com/TES5Edit/TES5Edit),
|
||||||
|
[zedit](https://github.com/z-edit/zedit)) of open-source programs that could
|
||||||
|
parse Skyrim plugins and extract this data. However, all of these were too broad
|
||||||
|
for my purpose or relied on the assumption of being run in the context of a load
|
||||||
|
order where the master files of a plugin would also be available. I wanted a
|
||||||
|
program that could take a single plugin in isolation and skip through all of the
|
||||||
|
non-relevant parts of it and dump just the CELL and WRLD record data plus some
|
||||||
|
metadata about the plugin from the header as fast as possible.
|
||||||
|
|
||||||
|
After discovering [the wonderful documentation on the UESP wiki about the Skyrim
|
||||||
|
mod file format](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format), I
|
||||||
|
realized this would be something that would be possible to make myself.
|
||||||
|
[skyrim-cell-dump](https://github.com/thallada/skyrim-cell-dump) is a Rust
|
||||||
|
library/CLI program that accepts a Skyrim mod file and spits out the header
|
||||||
|
metadata of the plugin, the worlds edited/created, and all of the cells it
|
||||||
|
edits/creates.
|
||||||
|
|
||||||
|
Under the hood, it uses the [nom crate](https://crates.io/crates/nom) to read
|
||||||
|
through the plugin until it finds the relevant records, then uses
|
||||||
|
[flate2](https://crates.io/crates/flate2) to decompress any compressed record
|
||||||
|
data, and finally outputs the extracted data formatted to JSON with
|
||||||
|
[serde](https://crates.io/crates/serde).
|
||||||
|
|
||||||
|
Overall, I was pretty happy with this toolkit of tools and was able to quickly
|
||||||
|
get the data I needed from plugins. My only gripe was that I never quite figured
|
||||||
|
out how to properly do error handling with nom. If there was ever an error, I
|
||||||
|
didn't get much data in the error about what failed besides what function it
|
||||||
|
failed in. I often had to resort to peppering in a dozen `dbg!()` statements to
|
||||||
|
figure out what went wrong.
|
||||||
|
|
||||||
|
I built it as both a library and binary crate so that I could import it in other
|
||||||
|
libraries and get the extracted data directly as Rust structs without needing to
|
||||||
|
go through JSON. I'll go more into why this was useful later.
|
||||||
|
|
||||||
|
### Building the website
|
||||||
|
|
||||||
|
Since I wanted to keep server costs low and wanted the site to be as fast as
|
||||||
|
possible for users, I decided pretty early on that the site would be purely
|
||||||
|
static HTML and JavaScript with no backend server. I decided to use the [Next.js
|
||||||
|
web framework](https://nextjs.org/) with
|
||||||
|
[TypeScript](https://www.typescriptlang.org/) since it was what I was familiar
|
||||||
|
with using in my day job. While it does have [server-side rendering
|
||||||
|
support](https://nextjs.org/docs/basic-features/pages#server-side-rendering)
|
||||||
|
which would require running a backend [Node.js](https://nodejs.org/en/) server,
|
||||||
|
it also supports a limited feature-set that can be [exported as static
|
||||||
|
HTML](https://nextjs.org/docs/advanced-features/static-html-export).
|
||||||
|
|
||||||
|
I host the site on [Cloudflare pages](https://pages.cloudflare.com/) which is
|
||||||
|
available on their free tier and made deploying from Github commits a
|
||||||
|
breeze[^cloudflare]. The web code is in my [modmapper-web
|
||||||
|
repo](https://github.com/thallada/modmapper-web).
|
||||||
|
|
||||||
|
The most prominent feature of the website is the interactive satellite map of
|
||||||
|
Skyrim. Two essential resources made this map possible: [the map tile images
|
||||||
|
from the UESP skyrim map](https://srmap.uesp.net/) and
|
||||||
|
[Mapbox](https://www.mapbox.com/).
|
||||||
|
|
||||||
|
[Mapbox provides a JS library for its WebGL
|
||||||
|
map](https://docs.mapbox.com/mapbox-gl-js/api/) which allows specifying a
|
||||||
|
[raster tile
|
||||||
|
source](https://docs.mapbox.com/mapbox-gl-js/example/map-tiles/)[^3d-terrain].
|
||||||
|
|
||||||
|
The [UESP team painstakingly loaded every cell in the Skyrim worldspace in the
|
||||||
|
Creation Kit and took a
|
||||||
|
screenshot](https://en.uesp.net/wiki/UESPWiki:Skyrim_Map_Design). Once I figured
|
||||||
|
out which image tiles mapped to which in-game cell it was relatively easy to put
|
||||||
|
a map together by plugging them into the Mapbox map as a raster tile source.
|
||||||
|
|
||||||
|
The heatmap overlaid on the map is created using a [Mapbox
|
||||||
|
layer](https://docs.mapbox.com/help/glossary/layer/) that fills a cell with a
|
||||||
|
color on a gradient from green to red depending on how many edits that cell has
|
||||||
|
across the whole database of mods.
|
||||||
|
|
||||||
|
![Screenshot closeup of modmapper.com displaying a grid of colored cells from
|
||||||
|
green to red overlaid atop a satellite map of
|
||||||
|
Skyrim](/img/blog/modmapper-heatmap-closeup.jpg)
|
||||||
|
|
||||||
|
The sidebar on the site is created using [React](https://reactjs.org/) and
|
||||||
|
[Redux](https://redux.js.org/) and uses the
|
||||||
|
[next/router](https://nextjs.org/docs/api-reference/next/router) to keep track
|
||||||
|
of which page the user is on with URL parameters.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div class="row">
|
||||||
|
<img alt="Screenshot of modmapper.com sidebar with a cell selected"
|
||||||
|
src="/img/blog/modmapper-cell-sidebar.jpg" class="half-left" />
|
||||||
|
<img alt="Screenshot of modmapper.com sidebar with a mod selected"
|
||||||
|
src="/img/blog/modmapper-mod-sidebar.jpg" class="half-right" />
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The mod search is implemented using
|
||||||
|
[MiniSearch](https://lucaong.github.io/minisearch/) that asynchronously loads
|
||||||
|
the giant search indices for each game containing every mod name and id.
|
||||||
|
|
||||||
|
![Screenshot of modmapper.com with "trees" entered into the search bar with a
|
||||||
|
number of LE and SE mod results listed underneath in a
|
||||||
|
dropdown](/img/blog/modmapper-search.jpg)
|
||||||
|
|
||||||
|
One of the newest features of the site allows users to drill down to a
|
||||||
|
particular plugin within a file of a mod and "Add" it to their list. All of the
|
||||||
|
added plugins will be listed in the sidebar and the cells they edit displayed in
|
||||||
|
purple outlines and conflicts between them displayed in red outlines.
|
||||||
|
|
||||||
|
![Screenshot of modmapper.com with 4 Added Plugins and the map covered in purple
|
||||||
|
and red boxes](/img/blog/modmapper-added-plugins.jpg)
|
||||||
|
|
||||||
|
### Loading plugins client-side with WebAssembly
|
||||||
|
|
||||||
|
A feature that many users requested after the initial release was being able to
|
||||||
|
load a list of the mods currently installed on their game and see which ones of
|
||||||
|
that set conflict with each other[^second-announcement]. Implementing this
|
||||||
|
feature was one of the most interesting parts of the project. Choosing to use
|
||||||
|
Rust made made it possible, since everything I was running server-side to
|
||||||
|
extract the plugin data could also be done client-side in the browser with the
|
||||||
|
same Rust code compiled to [WebAssembly](https://webassembly.org/).
|
||||||
|
|
||||||
|
I used [wasm-pack](https://github.com/rustwasm/wasm-pack) to create
|
||||||
|
[skyrim-cell-dump-wasm](https://github.com/thallada/skyrim-cell-dump-wasm/)
|
||||||
|
which exported the `parse_plugin` function from my
|
||||||
|
[skyrim-cell-dump](https://github.com/thallada/skyrim-cell-dump) Rust library
|
||||||
|
compiled to WebAssembly. It also exports a `hash_plugin` function that creates a
|
||||||
|
unique hash for a plugin file's slice of bytes using
|
||||||
|
[seahash](https://crates.io/crates/seahash) so the site can link plugins a user
|
||||||
|
has downloaded on their hard-drive to plugins that have been downloaded by
|
||||||
|
modmapper and saved in the database.
|
||||||
|
|
||||||
|
Dragging-and-dropping the Skyrim Data folder on to the webpage or selecting the
|
||||||
|
folder in the "Open Skyrim Data directory" dialog kicks off a process that
|
||||||
|
starts parsing all of the plugin files in that directory in parallel using [Web
|
||||||
|
Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
|
||||||
|
|
||||||
|
I developed my own
|
||||||
|
[`WorkerPool`](https://github.com/thallada/modmapper-web/blob/4af628559030c3f24618b29b46d4a40af2f200a6/lib/WorkerPool.ts)
|
||||||
|
that manages creating a pool of available workers and assigns them to plugins to
|
||||||
|
process. The pool size is the number of cores on the user's device so that the
|
||||||
|
site can process as many plugins in parallel as possible. After a plugin
|
||||||
|
finishes processing a plugin and sends the output to the redux store, it gets
|
||||||
|
added back to the pool and is then assigned a new plugin to process if there are
|
||||||
|
any[^wasm-troubles].
|
||||||
|
|
||||||
|
Once all plugins have been loaded, the map updates by displaying all of the
|
||||||
|
cells edited in a purple box and any cells that are edited by more than one
|
||||||
|
plugin in a red box.
|
||||||
|
|
||||||
|
![Screenshot of modmapper.com with 74 Loaded Plugins and the map filled with
|
||||||
|
purple and red boxes](/img/blog/modmapper-loaded-plugins.jpg)
|
||||||
|
|
||||||
|
Users can also drag-and-drop or paste their `plugins.txt` file, which is the
|
||||||
|
file that the game uses to define the load order of plugins and which plugins
|
||||||
|
are enabled or disabled. Adding the `plugins.txt` sorts the list of loaded
|
||||||
|
plugins in the sidebar in load order and enables or disables plugins as defined
|
||||||
|
in the `plugins.txt`.
|
||||||
|
|
||||||
|
![Screenshot of modmapper.com with the Paste plugins.txt dialog
|
||||||
|
open](/img/blog/modmapper-pluginstxt-dialog.jpg)
|
||||||
|
|
||||||
|
Selecting a cell in the map will display all of the loaded cells that edit that
|
||||||
|
cell in the sidebar.
|
||||||
|
|
||||||
|
![Screenshot of modmapper.com with a conflicted cell selected on the map and 4
|
||||||
|
Loaded Plugins displayed](/img/blog/modmapper-conflicted-cell.jpg)
|
||||||
|
|
||||||
|
The ability to load plugins straight from a user's hard-drive allows users to
|
||||||
|
map mods that haven't even been uploaded to Nexus Mods.
|
||||||
|
|
||||||
|
### Vortex integration
|
||||||
|
|
||||||
|
The initial mod I released on the Skyrim Special Edition page of Nexus Mods was
|
||||||
|
[taken
|
||||||
|
down](https://www.reddit.com/r/skyrimmods/comments/svnz4a/modmapper_got_removed/)
|
||||||
|
by the site admins since it didn't contain an actual mod and they didn't agree
|
||||||
|
that it qualified as a "Utility".
|
||||||
|
|
||||||
|
Determined to have an actual mod page for Modmapper on Nexus Mods, I decided to
|
||||||
|
make a [Vortex](https://www.nexusmods.com/about/vortex/) integration for
|
||||||
|
modmapper. Vortex is a mod manager made by the developers of Nexus Mods and they
|
||||||
|
allow creating extensions to the tool and have their own [mod section for Vortex
|
||||||
|
extensions](https://www.nexusmods.com/site).
|
||||||
|
|
||||||
|
With the help of [Pickysaurus](https://www.nexusmods.com/skyrim/users/31179975),
|
||||||
|
one of the community managers for Nexus Mods, I created a [Vortex integration
|
||||||
|
for Modmapper](https://www.nexusmods.com/site/mods/371). It adds a context menu
|
||||||
|
option on mods to view the mod in Modmapper with all of the cells it edits
|
||||||
|
selected in purple. It also adds a button next to every plugin file to view just
|
||||||
|
that plugin in Modmapper (assuming it has been processed by Modmapper).
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div class="row">
|
||||||
|
<img alt="Screenshot of Vortex mod list with a mod context menu open which
|
||||||
|
shows a 'See on Modmapper' option"
|
||||||
|
src="/img/blog/modmapper-vortex-mod-menu.jpg" class="half-left" />
|
||||||
|
<img alt="Screenshot of Vortex plugin list with 'See on Modmapper' buttons
|
||||||
|
on the right of each plugin row"
|
||||||
|
src="/img/blog/modmapper-vortex-plugin-button.jpg" class="half-right" />
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
To enable the latter part, I had to include `skyrim-cell-dump-wasm` in the
|
||||||
|
extension so that I could hash the plugin contents with `seahash` to get the
|
||||||
|
same hash that Modmapper would have generated. It only does this hashing when
|
||||||
|
you click the "See on Modmapper" button to save from excessive CPU usage when
|
||||||
|
viewing the plugin list.
|
||||||
|
|
||||||
|
After releasing the Vortex plugin, Pickysaurus [published a news article about
|
||||||
|
modmapper to the Skyrim Special Edition
|
||||||
|
site](https://www.nexusmods.com/skyrimspecialedition/news/14678) which also got
|
||||||
|
a lot of nice comments ❤️.
|
||||||
|
|
||||||
|
### Finishing the collection by adding all Skyrim Classic mods
|
||||||
|
|
||||||
|
Skyrim is very silly in that it has [many
|
||||||
|
editions](https://ag.hyperxgaming.com/article/12043/every-skyrim-edition-released-over-the-last-decade).
|
||||||
|
But there was only one that split the modding universe into two: [Skyrim Special
|
||||||
|
Edition (SE)](https://en.uesp.net/wiki/Skyrim:Special_Edition).
|
||||||
|
It was released in October 2016 with a revamped game engine that brought some
|
||||||
|
sorely needed graphical upgrades. However, it also contained changes to how mods
|
||||||
|
worked, requiring all mod authors to convert their mods to SE. This created big
|
||||||
|
chasm in the library of mods, and Nexus Mods had to make a separate section for
|
||||||
|
SE-only mods.
|
||||||
|
|
||||||
|
When I started downloading mods in 2021, I started only with Skyrim SE mods,
|
||||||
|
which, at the time of writing, totals at over [55,000 mods on Nexus
|
||||||
|
Mods](https://www.nexusmods.com/skyrimspecialedition/mods/).
|
||||||
|
|
||||||
|
After releasing with just SE mods, many users requested that all of the classic
|
||||||
|
pre-SE Skyrim mods be added as well. This month, I finally finished downloading
|
||||||
|
all Skyrim Classic mods, which, at the time of writing, totals at over [68,000
|
||||||
|
mods on Nexus Mods](https://www.nexusmods.com/skyrim/mods/). That brings the
|
||||||
|
total downloaded and processed mods for Modmapper at over 113,000
|
||||||
|
mods[^adult-mods]!
|
||||||
|
|
||||||
|
### The future
|
||||||
|
|
||||||
|
A lot of users had great feedback and suggestions on what to add to the site. I
|
||||||
|
could only implement so many of them, though. The rest I've been keeping track
|
||||||
|
of on [this Trello board](https://trello.com/b/VdpTQ7ar/modmapper).
|
||||||
|
|
||||||
|
Some of the headline items on it are:
|
||||||
|
|
||||||
|
* Add [Solstheim map](https://dbmap.uesp.net/)
|
||||||
|
|
||||||
|
Since map tiles images are available for that worldspace and because I have
|
||||||
|
already recorded edits to the worldspace in my database, it shouldn't be too
|
||||||
|
terribly difficult.
|
||||||
|
* Add [Mod Organizer 2](https://www.modorganizer.org/) plugin
|
||||||
|
|
||||||
|
Lots of people requested this since it's a very popular mod manager compared
|
||||||
|
to Vortex. MO2 supports python extensions so I created
|
||||||
|
[skyrim-cell-dump-py](https://github.com/thallada/skyrim-cell-dump-py) to
|
||||||
|
export the Rust plugin processing code to a Python library. I got a bit stuck
|
||||||
|
on actually creating the plugin though, so it might be a while until I get to
|
||||||
|
that.
|
||||||
|
* Find a way to display interior cell edits on the map
|
||||||
|
|
||||||
|
The map is currently missing edits to interior cells. Since almost all
|
||||||
|
interior cells in Skyrim have a link to the exterior world through a door
|
||||||
|
teleporter, it should be possible to map an interior cell edit to an exterior
|
||||||
|
cell on the map based on which cell the door leads out to.
|
||||||
|
|
||||||
|
That will require digging much more into the plugin files for more data, and
|
||||||
|
city worldspaces will complicate things further. Then there's the question of
|
||||||
|
interiors with multiple doors to different exterior cells, or interior cells
|
||||||
|
nested recursively deep within many other interior cells.
|
||||||
|
* Create a standalone [Electron](https://www.electronjs.org) app that can run
|
||||||
|
outside the browser
|
||||||
|
|
||||||
|
I think this would solve a lot of the issues I ran into while developing the
|
||||||
|
website. Since Electron has a Node.js process running on the user's computer
|
||||||
|
outside the sandboxed browser process, it gives me much more flexibility. It
|
||||||
|
could do things like automatically load a user's plugin files. Or just load
|
||||||
|
plugins at all wihtout having to deal with the annoying dialog that lies to
|
||||||
|
the user saying they're about to upload their entire Data folder hundreds of
|
||||||
|
gigabytes full of files to a server (I really wish the
|
||||||
|
[HTMLInputElement.webkitdirectory](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory)
|
||||||
|
API would use the same underlying code as the [HTML Drag and Drop
|
||||||
|
API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)
|
||||||
|
which is a lot better).
|
||||||
|
* Improving the search
|
||||||
|
|
||||||
|
The mod search feature struggles the most with the static generated nature of
|
||||||
|
the site. I found it very hard to pack all of the necessary info for the
|
||||||
|
search index for all 100k+ mods (index for both SE and LE is around 6 MB).
|
||||||
|
Asynchronously loading the indices with MiniSearch keeps it from freezing up
|
||||||
|
the browser, but it does take a very long time to fully load. I can't help
|
||||||
|
think that there's a better way to shard the indices somehow and only fetch
|
||||||
|
what I need based on what the user is typing into the search.
|
||||||
|
|
||||||
|
To be clear, a lot of the Todos on the board are pipe-dreams. I may never get to
|
||||||
|
them. This project is sustained purely by my motivation and self-interests and
|
||||||
|
if something is too much of a pain to get working I'll just drop it.
|
||||||
|
|
||||||
|
There will also be future Elder Scrolls games, and [future Bethesda games based
|
||||||
|
on roughly the same game engine](https://bethesda.net/en/game/starfield). It
|
||||||
|
would be neat to create similar database for those games as the modding
|
||||||
|
community develops in realtime.
|
||||||
|
|
||||||
|
Overall, I'm glad I made something of use to the modding community. I hope to
|
||||||
|
keep the site running for as long as people are modding Skyrim (until the
|
||||||
|
heat-death of the universe, probably).
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Footnotes
|
||||||
|
|
||||||
|
[^bazaarrealm]:
|
||||||
|
Unfortunately, I basically lost interest on the mod after working on
|
||||||
|
Modmapper. I might still write a blog post about it eventually since I did a
|
||||||
|
lot of interesting hacking on the Skyrim game engine to try to add some
|
||||||
|
asynchronous multiplayer aspects. [Project is here if anyone is curious in
|
||||||
|
the meantime](https://github.com/thallada/BazaarRealmPlugin).
|
||||||
|
|
||||||
|
[^takedown]:
|
||||||
|
I sadly only have screenshots for some of the comments on that mod since it
|
||||||
|
was eventually taken down by the Nexus Mod admins. See explanation about
|
||||||
|
that in the [Vortex integration section](#vortex-integration).
|
||||||
|
|
||||||
|
[^analytics]:
|
||||||
|
As recorded by Cloudflare's server side analytics, which may record a fair
|
||||||
|
amount of bot traffic. I suspect this is the most accurate number I can get
|
||||||
|
since most of my users probably use an ad blocker that blocks client-side
|
||||||
|
analytics.
|
||||||
|
|
||||||
|
[^adult-mods]:
|
||||||
|
Every mod on Nexus Mods except for adult mods since the site restricts
|
||||||
|
viewing adult mods to only logged-in users and I wasn't able to get my
|
||||||
|
scraping bot to log in as a user.
|
||||||
|
|
||||||
|
[^rate-limit]:
|
||||||
|
Apparently my mass-downloading did not go unnoticed by the Nexus Mod admins.
|
||||||
|
I think it's technically against their terms of service to automatically
|
||||||
|
download mods, but I somehow got on their good side and was spared the
|
||||||
|
ban-hammer. I don't recommend anyone else run modmapper themselves on the
|
||||||
|
entire site unless you talk to the admins beforehand and get the okay from
|
||||||
|
them.
|
||||||
|
|
||||||
|
[^plugin-collection]:
|
||||||
|
If you would like access to this dataset of plugins to do some data-mining
|
||||||
|
please reach out to me at [tyler@hallada.net](mailto:tyler@hallada.net)
|
||||||
|
(Note: only contains plugins files, no models, textures, audio, etc.). I
|
||||||
|
don't plan on releasing it publicly since that would surely go against many
|
||||||
|
mod authors' wishes/licenses.
|
||||||
|
|
||||||
|
[^cloudflare]:
|
||||||
|
I'm not sure I want to recommend anyone else use Cloudflare after [the whole
|
||||||
|
Kiwi Farms
|
||||||
|
debacle](https://www.theverge.com/2022/9/6/23339889/cloudflare-kiwi-farms-content-moderation-ddos).
|
||||||
|
I now regret having invested so much of the infrastructure in them. However,
|
||||||
|
I'm only using their free-tier, so at least I am a net-negative for their
|
||||||
|
business? I would recommend others look into
|
||||||
|
[Netlify](https://www.netlify.com/) or [fastly](https://www.fastly.com/) for
|
||||||
|
similar offerings to Cloudflare pages/CDN.
|
||||||
|
|
||||||
|
[^3d-terrain]:
|
||||||
|
I also tried to add a [raster Terrain-DEM
|
||||||
|
source](https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/)
|
||||||
|
for rendering the terrain in 3D. I got fairly far [generating my own DEM RGB
|
||||||
|
tiles](https://github.com/syncpoint/terrain-rgb) from an upscaled [greyscale
|
||||||
|
heightmap](https://i.imgur.com/9RErBDo.png) [constructed from the LAND
|
||||||
|
records in Skyrim.esm](https://www.nexusmods.com/skyrim/mods/80692) (view it
|
||||||
|
[here](https://www.dropbox.com/s/56lffk021riil6h/heightmap-4x_foolhardy_Remacri_rgb.tif?dl=0)).
|
||||||
|
But, it came out all wrong: [giant cliffs in the middle of the
|
||||||
|
map](/img/blog/modmapper-terrain-cliff.jpg) and [tiny spiky lumps with big
|
||||||
|
jumps in elevation at cell boundaries](/img/blog/modmapper-bad-terrain.jpg).
|
||||||
|
Seemed like too much work to get right than it was worth it.
|
||||||
|
|
||||||
|
[^second-announcement]:
|
||||||
|
[This was the announcement I posted to /r/skyrimmods for this feature](
|
||||||
|
https://www.reddit.com/r/skyrimmods/comments/ti3gjh/modmapper_update_load_plugins_in_your_load_order/)
|
||||||
|
|
||||||
|
[^wasm-troubles]:
|
||||||
|
At first, I noticed a strange issue with re-using the same worker on
|
||||||
|
different plugins multiple times. After a while (~30 reuses per worker), the
|
||||||
|
processing would slow to a crawl and eventually strange things started
|
||||||
|
happening (I was listening to music in my browser and it started to pop and
|
||||||
|
crack). It seemed like the speed of processing increased exponentially to
|
||||||
|
the number of times the worker was reused. So, to avoid this, I had to make
|
||||||
|
the worker pool terminate and recreate workers after every plugin processed.
|
||||||
|
This ended up not being as slow as it sounds and worked fine. However, I
|
||||||
|
recently discovered that [wee_alloc, the most suggested allocator to use
|
||||||
|
with rust in wasm, has a memory leak and is mostly unmaintained
|
||||||
|
now](https://www.reddit.com/r/rust/comments/x1cle0/dont_use_wee_alloc_in_production_code_targeting/).
|
||||||
|
I switched to the default allocator and I didn't run into the exponentially
|
||||||
|
slow re-use problem. For some reason, the first run on a fresh tab is always
|
||||||
|
much faster than the second run, but subsequent runs are still fairly stable
|
||||||
|
in processing time.
|
||||||
|
|
BIN
img/blog/allthemods.jpg
Executable file
After Width: | Height: | Size: 47 KiB |
BIN
img/blog/modmapper-added-plugins.jpg
Executable file
After Width: | Height: | Size: 265 KiB |
BIN
img/blog/modmapper-bad-terrain.jpg
Executable file
After Width: | Height: | Size: 380 KiB |
BIN
img/blog/modmapper-cell-sidebar.jpg
Executable file
After Width: | Height: | Size: 64 KiB |
BIN
img/blog/modmapper-clipping-example1.jpg
Executable file
After Width: | Height: | Size: 631 KiB |
BIN
img/blog/modmapper-clipping-example2.jpg
Executable file
After Width: | Height: | Size: 670 KiB |
BIN
img/blog/modmapper-clipping-example3.jpg
Executable file
After Width: | Height: | Size: 880 KiB |
BIN
img/blog/modmapper-clipping-example4.jpg
Executable file
After Width: | Height: | Size: 1.4 MiB |
BIN
img/blog/modmapper-comment1.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
img/blog/modmapper-comment10.png
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
img/blog/modmapper-comment11.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
img/blog/modmapper-comment12.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
img/blog/modmapper-comment13.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
img/blog/modmapper-comment15.png
Executable file
After Width: | Height: | Size: 17 KiB |
BIN
img/blog/modmapper-comment16.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
img/blog/modmapper-comment2.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
img/blog/modmapper-comment3.png
Executable file
After Width: | Height: | Size: 23 KiB |
BIN
img/blog/modmapper-comment4.png
Executable file
After Width: | Height: | Size: 50 KiB |
BIN
img/blog/modmapper-comment5.png
Executable file
After Width: | Height: | Size: 34 KiB |
BIN
img/blog/modmapper-comment6.png
Executable file
After Width: | Height: | Size: 13 KiB |
BIN
img/blog/modmapper-comment7.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
img/blog/modmapper-comment8.png
Executable file
After Width: | Height: | Size: 21 KiB |
BIN
img/blog/modmapper-conflicted-cell.jpg
Executable file
After Width: | Height: | Size: 164 KiB |
BIN
img/blog/modmapper-heatmap-closeup.jpg
Executable file
After Width: | Height: | Size: 327 KiB |
BIN
img/blog/modmapper-loaded-plugins.jpg
Executable file
After Width: | Height: | Size: 476 KiB |
BIN
img/blog/modmapper-mod-sidebar.jpg
Executable file
After Width: | Height: | Size: 60 KiB |
BIN
img/blog/modmapper-pcgamer.jpg
Executable file
After Width: | Height: | Size: 133 KiB |
BIN
img/blog/modmapper-pluginstxt-dialog.jpg
Executable file
After Width: | Height: | Size: 65 KiB |
BIN
img/blog/modmapper-search.jpg
Executable file
After Width: | Height: | Size: 61 KiB |
BIN
img/blog/modmapper-terrain-cliff.jpg
Executable file
After Width: | Height: | Size: 597 KiB |
BIN
img/blog/modmapper-vortex-mod-menu.jpg
Executable file
After Width: | Height: | Size: 223 KiB |
BIN
img/blog/modmapper-vortex-plugin-button.jpg
Executable file
After Width: | Height: | Size: 237 KiB |
BIN
img/blog/modmapper.jpg
Executable file
After Width: | Height: | Size: 852 KiB |