Tabletop Simulator

Tabletop Simulator

评价数不足
Modding File Management - Creating mods that last forever
由 ulia 制作
In this guide I aim to cover everything of importance that has to do with creating, managing and uploading files for Tabletop Simulator mods. By following the practices outlined in this guide you will not only be able to ensure the longevity of your work, but you will also find methods that improve your work efficiency and flexibility during creation.

This guide may be a lengthy read and you will have to apply new and potentially unfamiliar steps to your modding practices, but the couple of minutes you spend setting up a reliable environment for your modding will save you hours of work in the future.
   
奖励
收藏
已收藏
取消收藏
Creating a modding environment using local files
As a modder you will need to familiarize yourself with the cached files folder that TTS creates for you on installation. This folder is by default located at:
X:/Users/User/Documents/My Games/Tabletop Simulator/Mods
You might already know that this directory contains all the cached files from mods you have loaded. The game downloads and caches files to speed up loading times the next time you load up the same mod or file. But what you might not know is that these folders have additional functionality for modding purposes.

All the subfolders in the Tabletop Simulator/Mods directory are tied to the game in a way where TTS can find the appropriate file by only providing a filename rather than the full absolute path.

In a typical scenario where you might import a file as a gameobject in TTS we would navigate to the file using the file browser and select it. Let's say we are importing the image called "Token.jpeg" as a token object. Having selected the file, the import window gives us a path that looks something like this:
"X:/Users/User/Documents/Pictures/Token.jpeg"
This is what is called an absolute path as it points to the entire location, including the drive and the folder tree.

Absolute paths come with a set of drawbacks, the most significant being that local files with absolute paths will often not be transferable between different computers unless the absolute path is exactly identical. Computer A may have the image on drive D:/ while Computer B has it on C:/, making TTS look for files in locations that do not exist when trying to load the file.

You may be wondering why local files on different computers is in any way relevant for a mod that will be hosted on an online filehost - I will cover the importance of this later in the Archiving & Collaboration section of this guide.

For now let's pivot back to the alternative, that being the Mods directory and why it is important for modding and archiving purposes.

Any files in the Tabletop Simulator\Mods directory can be imported and referenced in TTS using only the filename. This means that if you put the "Token.jpeg" image in to the Mods/Images folder, when importing a token you can simply input the filename "Token.jpeg" in to the Image field and TTS will find and import the file without relying on an absolute path.

More importantly, this allows you to create subfolders in these directories, which lets you structure your mods in a coherent and transferable way.

Our example mod is called "MyGame" and for our game we have created a token image named "Token.jpeg". To import this image as a token in TTS we provide "MyGame/Token.jpeg" in the Image field in the import window, and TTS will automatically find and import this image as the token we want.

Should we want to make changes to the image then we can edit it in-place (in the "Mods/Images/MyGame/" folder) and re-load our save in game to see the changes we've made to our token (you may need to exit out to the main menu first in order to clear things stored in RAM/VRAM).

The same principle can be applied to any other file type - Images, models, assetbundles and PDFs all go in to their respective folders and can be imported in the same way. When importing an assetbundle with just the filename, TTS will automatically look in to the "Mods/Assetbundles/" folder.

When creating a mod we should start by establishing a folder structure like the following:
📁 Mods/ 📁 Images/ 📁 MyGame/ 🗎 Token1.jpeg 🗎 Token2.jpeg 📁 AssetBundles/ 📁 MyGame/ 🗎 Assetbundle.unity3d 📁 PDF/ 📁 MyGame/ 🗎 MyGame_Rulebook.pdf
Doing this allows us to import all our files using only "MyGame/filename.jpeg" in the import window in addition to having the option to edit our files in-place without having to change any paths or re-upload files.

Do NOT upload any files to the Steam Cloud or other filehosts while you are working on your mod. Doing this will only complicate your process and workload and make a mess of things. I will cover how to get your mod online and published in the next section - Uploading & Publishing
Uploading & Publishing
Now that you've finished creating your mod using local assets as outlined in the previous section we can move on to how to upload and publish your mod and all of it's files in a way that allows you to reliably update and maintain it.

The way we want to structure this is having two save files, the first save being your permanent developer save that will only contain paths to local files. This is the save that you will be working on and editing when you have to make changes or additions to your mod.

At no point do we want to use any hosted or otherwise online files in this save as it significantly complicates and clutters the development process.

When this save is ready to be uploaded and published, create a copy of the save. I suggest you suffix the save name with something like "_live" or "_online" so that you can easily distinguish between it and your developer save.

From here you have to primary ways of uploading and hosting files depending on what tools you have access to - A) using the Steam Cloud or B) self-hosting on a HTTP server. The processes for these options differ slightly so I will cover them separately.

A) Using the Steam Cloud
The Steam Cloud option is very simple and straight forward as TTS has a built in tool to upload all local files to your cloud. Navigate to "Modding -> Cloud Manager" and to the right of the search bar you will find a button with an up-arrow icon labelled "Upload all loaded files".

If you followed the previous section of the guide, all your files should be local and using this function allows you to upload all of the save contents to a folder in your cloud. I will recommend you fill in the field to create a named folder when doing this to keep different mod files separate from each other.

After pressing the "Upload all loaded files" button, allow TTS to upload all the files to your cloud and save once completed. Remember to save this as the separate "_live" copy and never overwrite your local developer save with hosted files.

You can now proceed with publishing this to the workshop with the "Modding -> Workshop Upload" tool.

Repeat this process of creating a copy of your developer save and using the "Upload all loaded files" function every time you want to update the published version of your mod. You may make changes to the "_live" copy directly if the changes are minor, but I will advise against this as it will create a mismatch between your "_live" and developer save falling behind. Spending the couple of minutes to repeat this process now may save you upwards hours of additional work and headaches in the future, especially if you're making changes to files such as images or assetbundles.

Should you not want to re-upload the entire project for every update then you can carefully update individual files instead, making sure that your local developer save remains the most up to date revision.

B) Self-hosting on a HTTP server
This process is a little bit more complicated, but it comes with the benefit of never having to re-upload existing files, instead leveraging functions such as {verifycache} to always serve the most up to date files to users. If you do not already have access to a HTTP server and don't wish to purchase/rent/set one up then skip the remainder of this section.

In the following example I will assume that you have uploaded all the local files to a "MyGame" directory on your server, resulting in an online path like the following:
https://myserver.com/MyGame/Token1.jpeg"

As with the Steam Cloud process we start by creating a "_live" copy of the developer save and opening it up in a text editor such as Notepad++ or Visual Studio Code.

Then we want to use the "Search and Replace" function of the editor to make some changes to the references to local files.

Our starting strings will be something like the following:
"ImageURL": "MyGame/Token1.jpeg"
The important part being "MyGame/Token1.jpeg" which we will change in the following ways:
  • Replace the "MyGame/" location with your IP or domain name
  • Prefix the path with "{verifycache}"
We can do this by using "MyGame/" in the Find field and replacing it with "{verifycache}https://myserver.com/" resulting in all the file reference fields being changed to the following:
"ImageURL": "{verifycache}https://myserver.com/MyGame/Token1.jpeg"

With that the save file is ready to be published on the Steam Workshop. Should you at any point need to update your files then you can simply overwrite them on your HTTP server because the {verifycache} prefix will ensure that TTS downloads the latest version of the files if the cached files are older.

Like with the Steam Cloud option I will strongly suggest you do not directly modify a "_live" save but instead work in the developer save and repeat these steps each time you wish to update your mod.
Sharing, Collaborating & Archiving
Creating your mod using local files as outlined in the previous sections allows you to share and archive it without complicating the process with hosted and cached files.

For demonstration we will assume the same example structure and naming convention as used previously:
📁 Mods/ 📁 Images/ 📁 MyGame/ 🗎 Token1.jpeg 🗎 Token2.jpeg 📁 AssetBundles/ 📁 MyGame/ 🗎 Assetbundle.unity3d 📁 PDF/ 📁 MyGame/ 🗎 MyGame_Rulebook.pdf
In order to archive and share our developer save we want to create compressed archive (.zip .rar .tar.gz) with all of it's constituent parts. We can do this in a way that makes it easy to extract by structuring it in the following way:
💾 MyGame.zip 📁 Mods 📁 Images/ 📁 MyGame/ 📁 AssetBundles/ 📁 MyGame/ 📁 PDF/ 📁 MyGame/ 📁 Saves 📄MyGame_dev.json
I have truncated the example down to just the folders for readability reasons, each folder should contain all the images, assetbundles and PDFs that are part of your mod.

You can now upload this archive to the Steam Cloud, Google Drive or any other filehost of your preference, or otherwise send it directly to collaborators or anyone who wishes to archive a copy of your mod.

Following the steps outlined in the previous section Uploading & Publishing anyone can restore and upload the mod in a matter of minutes, and also continue working on it without having to think about conflicts with any previously cached files players may have.

- Google Drive (or similar file storage services)
Experienced modders working in a team may want to set up an environment that streamlines the process.

Services like Google Drive allow you to set up an equivalent folder structure without having to compress sets of folders and files. By creating the same folder structure as the previously demonstrated 💾 MyGame.zip in a Google Drive you are able to offer both a download of the entire archive and of individual files to save significant amounts of time when downloading revisions of individual assets and components.

A user who needs a new version of just one or a couple of files can simply download the updated files and put them in the respective folder in their Mods directory. Make sure to always update the save file accordingly, or optionally keep multiple saves of revisions should you need to go back to a previous state.
Filetypes, Compression & Optimization
In this section I want to cover best practices when creating assets for your mods and clarify some common misconceptions and misinformation. This section does assume that you are lightly familiar with creating graphical assets for TTS modding and will not necessarily explain any hows, but instead focus on the whys.

Images - Quantity or Resolution?
A "texture sheet" or "texture atlas" is a compound image comprised of many individual images packed in to one, typically laid out in a uniform grid pattern. For the purpose of modding in TTS you will most commonly see sheets being used for decks of cards as the game allows you to provide and slice one up with the built in deck importer tool.

There are valid applications for using individual images instead of a sheet for assets such as decks, but the recommended convention is to use a sheet as they are significantly more optimal for loading times, performance and filesizes. If you are not doing anything that strictly requires individual card images then create and use a sheet.

Throughout this section I will be using power of two (2 > 4 > 8 > 16 > 32 > 64 > 128 > 512 > 1024 > 2048 > 4096 > 8192) pixel resolutions in my examples as this is the standard convention we use for image textures in game development. The purpose of these values as examples is to provide you with an approximate set of values to work with. You do not need to strictly use these exact values when creating your images as the benefits are relatively minor and may become a hassle if you are not already intimately familiar with image processing.

The image resolution and quantity of cards in your sheet matters. The effective resolution of each card is equivalent to it's pixel dimensions in the sheet. In a 4096x4096 pixels sheet of 8x4 cards, each card has a resolution of 512x1024 pixels, equivalent to an individual card being imported with the same pixel dimensions. Adding more cards to the same resolution image will result in each individual card being a lower resolution, namely the image width and height divided by the amount of cards in the sheet.

I'm using 4096x4096 pixels as the example as this image resolution offers a good balance between performance and image quality. Four 4096x4096 pixel images with 8x4 cards on each sheet load more than twice as fast than a single 8192x8192 image with 32x16 cards while offering the equivalent pixel resolution.

As for what resolution you should aim for per card - it depends, but for most cards a resolution of around 512x1024 pixels is sufficient for legibility and fidelity. You may want to consider a higher resolution for cards with very small text or if you are willing to significantly increase the filesize for sharper images.

But where is this resolution convention derived from?
The overwhelming majority of players will be using a 1920x1080 pixels resolution monitor on which cards can at most be blown up to the vertical 1080 pixels available. As such an effective resolution of 512x1024 pixels comes close enough to the available pixels the monitor has to display the card.

A higher resolution image will appear sharper due to things like pixel interpolation when downsampling an image, and there is a fair amount of players who will be using higher resolution and size monitors. But because you run in to significant diminishing returns when your image resolution exceeds the amount of pixels the monitor has available to display it I will at most recommend an effective resolution of 1024x2048 per card, which is the maximum reasonable resolution per cards on 4K displays.

This section ended up having a lot of information with many tangents and caveats, so I will briefly summarize it so you can accurately take with you the things that are important:
  • You should use sheets for cards when you're not leveraging the utility of individual card images
  • Your sheet should ideally not significantly exceed a resolution of 4096x4096 pixels
  • You should use less cards per sheet instead of significantly increasing the resolution of the sheet if you want higher quality cards

Images - PNG or JPEG?
The primary difference between PNG and JPEG as image formats is that PNG offers lossless compression while JPEG has lossy compression, meaning that a JPEG will discard some data it deems unnecessary while trying it's best to retain fidelity.

PNGs are typically significantly larger in filesize as lossless compression preserves practically all necessary image data. There is however an exception to this and that is when an image is mostly flat, without gradients and noise but instead having large, discrete blocks of few colours.

The thumbnail for this guide comes out to 32.1KB as a PNG but 152KB as a high quality JPEG, as it is the type of image that lends itself well to the PNG compression algorithm. But you will observe opposite results with images such as photographs or high fidelity artwork, namely images that have a lot of noise. These images will often come out to be double to triple the filesize as a PNG when compared to a high quality JPEG.

With images that compress better as PNGs being the exception, for most other applications you will have to make the choice between lower filesizes (JPEG) and lossless image quality (PNG). But in reality and in most applications most players will not be able to distinguish any difference between the two.

AssetBundles
AssetBundles is a way of importing Unity gameobjects in to TTS. This comes with a multitude of benefits:
  • Control over compression and quality for images and models
  • No resolution or filesize limitations for images and models, and less filetype limitations
  • Input to output parity
  • Additional functionality for materials with a large set of default and custom shaders
  • Optimization practices such as using sheets for tokens and tiles
  • Animations
  • Hundreds of additional Unity components such as particles and lights

A common misconception is that using Unity and the TTS Modding Project is a difficult task that requires you to "learn Unity" and "write code". This is however not the case, and building assetbundles with the modding project is likely easier than anything you're doing in image editing or 3D modelling software.

If you are a 3D modeler or otherwise commonly use 3D models for TTS then you should strongly consider getting started with creating assetbundles https://kb.tabletopsimulator.com/custom-content/custom-assetbundle/

There are also significant benefits to creating assetbundle materials from images for use as cards or tiles, giving you control over many aspects such as compression, tiling and shaders.

External Filehosts
Commonly users use sites like Imgur, Discord and Pastebin to upload and serve their assets in TTS mods. And just as commonly these files break and go missing as these sites are not permanent filehosts but platforms for convenient but temporary sharing of images and text snippets. These platforms never promised to keep your files online and available, and you should assume that they can and will delete your material at any point in time.

The only two file hosting options I will recommend is the in-game Steam Cloud or a rented/self hosted HTTP server.
Automating file importing with Lua
By applying a bit of forward thinking and naming your files with numbered suffixes or similar you can automate the importing process of large sets of components using Lua scripting. This portion of the guide assumes light familiarity with Lua scripting and an understanding of code basics such as for loops, string concatenation and tables.

In the following example I will be importing a set of 15 assetbundles with the filenames "tile_1.unity3d" through to "tile_15.unity3d". These assetbundles are located in the "Mods/AssetBundles/MyGame" folder. I will also include a secondary assetbundle for the materials with the filename "tile_materials.unity3d".

function spawnAssetbundles() -- primary and secondary filenames local primary = "tile_" local secondary = "tile_materials" -- amount of files to import local amount = 15 -- in-game object type https://api.tabletopsimulator.com/custom-game-objects/#custom-assetbundle local type = 1 for i=1, amount do -- https://api.tabletopsimulator.com/base/#spawnobject local obj = spawnObject({ type="Custom_AssetBundle", position={0, 1 + (i / 5), 0}, scale={1,1,1} }) obj.setCustomObject({ type=type, assetbundle=primary .. int .. ".unity3d", assetbundle_secondary=secondary .. ".unity3d" }) end end
This script is a for loop[www.lua.org] that increments the integer i by 1 for each of the 15 (amount) cycles. In each cycle it appends this integer to a concatenated string that comes out to be equivalent to our integer suffixed assetbundle files. The resulting function that runs is the following, incrementing the filename suffix by 1 for each of the 15 times it loops:
obj.setCustomObject({ type=1, assetbundle="tile_1.unity3d", assetbundle_secondary="tile_materials.unity3d" })
obj.setCustomObject({ type=1, assetbundle="tile_2.unity3d", assetbundle_secondary="tile_materials.unity3d" }) e.t.c
This type of script can be modified and extended as needed for other component tiles such as tokens, cards or tiles.

An alternative to this is creating a table with pre-defined data and attributes. This method may be useful to reduce the amount of tedious manual steps such as positioning and adding tags to components. Being familiar with an IDE such as Visual Studio Code or similar that supports functions such as multicursor[code.visualstudio.com] can significantly speed up this process compared to doing everything manually.
-- filename, tag, position, rotation local tiles = { {file="tile_a0", tag="tilea0", pos=Vector(-16, 1, -3), rot=Vector(0, 120, 0)}, {file="tile_b0", tag="tileb0", pos=Vector(-16, 1, -3), rot=Vector(0, 120, 0)}, {file="tile_a1", tag="tilea1", pos=Vector(-14, 1.12, 0), rot=Vector(0, 90, 180)}, {file="tile_b1", tag="tileb1", pos=Vector(-14, 1.12, 0), rot=Vector(0, 90, 180)}, {file="tile_a2", tag="tilea2", pos=Vector(-18.8, 1.12, -0.9), rot=Vector(0, 60, 180)}, {file="tile_b2", tag="tileb2", pos=Vector(-18.8, 1.12, -0.9), rot=Vector(0, 60, 180)}, } local heightOffset = 1 for t, tile in ipairs(tiles) do if t%12 == 0 then heightOffset = 1 end heightOffset = heightOffset + 0.13 local primary = tile.name .. ".unity3d" local secondary = "MyGame/tile_materials.unity3d" local spawnedTile = spawnObject({ type="Custom_AssetBundle", position=tile.pos, rotation=tile.rot, }) spawnedTile.setCustomObject({ type=1, assetbundle=primary, assetbundle_secondary=secondary }) spawnedTile.setPosition(Vector(spawnedTile.getPosition():add(Vector(0,heightOffset,0)))) end
1 条留言
Bob 11 月 3 日 下午 11:03 
I've only just skimmed this, but it looks really good!