saved-data
Building a DataWrapperβ
DataWrapper
has different initialization methods depending what type of object you want to attach data to.
GlobalDataWrapper<ExampleData> DATA = DataWrapper.global(ExampleData.class)
- one instance of
ExampleData
is stored per server
- one instance of
LevelDataWrapper<ExampleData> DATA = DataWrapper.level(ExampleData.class)
- one instance of
ExampleData
is stored per dimension
- one instance of
PlayerDataWrapper<ExampleData> DATA = DataWrapper.player(ExampleData.class)
- one instance of
ExampleData
is stored per player
- one instance of
The return value of these can just be stored in a static variable for later use. The methods below are setup like a Builder so they can be chained.
Even above 1.16, the data class can NOT be a record
(the version of Gson shipped with Minecraft does not support them) or have an final fields. Since you need to have a no parameter constructor that sets default values anyway, the record limitation doesn't really add more code. See Serialization for more information on what types of data are supported in your data class.
savedβ
Call DataWrapper#saved
to save the data to disk when the world is saved so it will persist across server restarts.
syncedβ
Call DataWrapper#synced
to send the data to all clients on load and when you call DataWrapper#setDirty
.
File Locationβ
Even if you aren't saving to disk, you must set a unique file location to be used for syncing.
- You must call the
DataWrapper#named
method to set the name of the file to save data to. - You can put your config file in a subdirectory of its normal location by calling
DataWrapper#dir
- If you call
DataWrapper#named
with a resource location, instead of just a string, the path will be used as the file name and the namespace will be used for the subdirectory. - The file extension can be set by calling
DataWrapper#ext
(this does not actually change the data format!) but generally leaving it as the default (.json5) is fine.
The file will be at world/data/[dir]/[name].[ext]
.
MapDataWrappersβ
Most data wrapper types (level
, player
) store a map of keys to values. These have some additional methods you may call to configure them.
splitFilesβ
This method store each object in a separate file. Instead of a single file storing a map of (resource locations or uuids) to data, there will be many files whose names are (resource locations or uuids) containing one piece of data. This option does nothing if the data is not also saved
.
The files will be at world/data/[dir]/[name]/[id].[ext]
lazyβ
This method will have the same effect as splitFiles
. Additionally, it will not load any of the data when the server starts. An entry's data will only be loaded from the file system when specifically requested by MapDataWrapper#get
(see Reading Data Values).
This means even if synced
, retrieving an entry on the client will return default values until it is retrieved at least once on the server side.
Data Loadingβ
As long as your DataWrapper
is constructed in time (ie. it is in your mod initializer class or its parent class gets class loaded during your mod initializer), the values will automatically be loaded during ServerStartingEvent
/ ServerLifecycleEvents.SERVER_STARTING
with no extra work from you. You can safely retrieve your config anytime after these events.
When a player joins a server, any synced
data wrappers will be sent their client.
If you try to retrieve your data value before it gets loaded, no MinecraftServer
instance is available so it won't know where to find the world folder. Default values will be returned and an error will be logged.
Reading Data Valuesβ
Simply call DataWrapper#get
to retrieve an object of your data class. The key
parameter required will depend on the type of DataWrapper
. The first type you call this for a given key, a new object will be created with your default values.
global
:DATA.get()
level
:DATA.get(level)
player
:DATA.get(player)
Writing Data Valuesβ
Simply retrieve the data object as above and change the values of its fields.
After changing a value you must call DataWrapper#setDirty
. This will cause the data to save with the world (if saved
) and immediately sync to all clients (if synced
). You can pass a player/level, depending on the type of DataWrapper
, to only resync that one value; which is more efficient that resending everything. setDirty
can only be called on the logical server, the client cannot sync or save anything.
Removing Data Valuesβ
- Calling
DataWrapper#clear
will DELETE ALL DATA, including the files saved to disk. Be careful! - Calling
MapDataWrapper#remove
and passing in a specific key will delete the data of only that one entry (including its file on the disk).
In both cases the change will be synced to the client by sending the default value.
Simple Exampleβ
This creates an item that will deal an extra heart of damage per Wither killed by the player (item registration omitted).
// ExampleModMain.java
public static final PlayerDataWrapper<ExampleData> DATA = DataWrapper.player(ExampleData.class).saved().named(MOD_ID);
// ExampleData.java
public class ExampleData {
public int withersKilled = 0;
}
// ForgeEventHandlers.java
@SubscribeEvent
public static void onDeath(LivingDeathEvent event){
if (event.getEntity().level.isClientSide()) return;
if (!(event.getEntity() instanceof WitherBoss)) return;
if (event.getSource().getEntity() instanceof Player player){
ExampleModMain.DATA.get(player).withersKilled++;
ExampleModMain.DATA.setDirty(player);
}
}
// ScalingAttackItem.java
@Override
public boolean hurtEnemy(ItemStack stack, LivingEntity target, LivingEntity attacker) {
if (!target.level.isClientSide() && attacker instanceof Player player) {
int damage = ExampleModMain.DATA.get(player).withersKilled * 2;
target.hurt(DamageSource.GENERIC, damage);
}
}
NbtDataWrapperβ
WIP.