Git Product home page Git Product logo

playerdb's Introduction

PlayerDB - 1.16+ Minecraft Versions

An NBT Custom Player Database

This datapack provides a player database for you to utilize! What is this database? Let me explain:

Each player has an enderchest where the player can store and retrieve items. With commands, we can also place items, clear items, and even put loot tables in there! Each player has their own individual enderchest storage which is separate from everyone else's. Essentially, this datapack implements a more feature-filled version of a player enderchest. Using the new global nbt storage that was introduced in 1.15, we can now store nbt data in the 'cloud'. This datapack maintains it's own space in this 'cloud' storing each player individually in a list. Like how scoreboard stores numbers per player, here you can store nbt per player allowing you to manage and manipulate it as you please (even access it when they are offline!)

Why?

At the moment, the only thing we really have per player are scoreboards which only store numbers and enderchests which store items (mostly for survival). This new system adds another per player storage... custom nbt! This ranges from numbers, floats, strings, lists, and compounds of all these elements combined!

This datapack has a couple main advantages to enderchests:

  • Accessing enderchest data requires the player to be online. This datapack has offline access.
  • The enderchest can be accessed in survival. storage is a separate entity w/ commands only access
  • This datapack should be much faster than accessing the enderchest. This is because accessing the enderchest requires serialization which incurs steep performance costs. The nbt storage does not store serialized, only compressed as nbt.

There are a couple of alternative implementations of player-specific nbt storage:

  • Using a single array and iterating through all the elements until you find the player.

This is particularly laggy and scales O(n) for the number of players in the list while PlayerDB scales O(log n) and it much more optimized with lower uid numbers.

  • Creating a custom dimension (or isolating a chunk) for an array of jukeboxes.

Since jukeboxes are BlockEntities, they incur extra lag as well. This is one of the best other solutions which you can check out here: EntityDB From my slight testing, PlayerDB does beat out in terms of lag, but this is based off my limited testing.

Lag?

As previously mentioned, this solution is an alternative to other solutions which may be laggier. To be specific, at worst, it is O(log(n)) which while not at O(1) like a normal array, is pretty damn close. In terms of Minecraft, you can think of this being about 50% less laggy than getting the player nbt via data get entity @s (which has been moderately tested so do try yourself). More technical bits on lag will be discussed in a later section.

How to use

This datapack assigns a unique id to each player via a scoreboard: rx.uid. To access the data, you need an uid to search the database with. Secondly, the database only creates an entry when it is requested or explicitly created. I've listed some examples ranging from easy to complex.

Storing simple data

This will get @s's database entry. If it does not exist, it'll dynamically create it.

function rx.playerdb:api/get_self

Our data is available at rx:io playerdb.player.data. We should write some data, notice how we organized our data by author.cool_pack.

data modify storage rx:io playerdb.player.data.author.cool_pack set value {eggs: 3b}

Note that we stored our data in author.cool_pack. Namespacing our data allows us to have better compatibility with other packs!

Finally, let's save our data!

function rx.playerdb:api/save_self

Getting some previously stored data
function rx.playerdb:api/get_self
execute store result score @s eggs run data get storage rx:io playerdb.player.data.author.cool_pack.eggs

# No need to save, we are just reading

Getting someone else's data

Sometimes, we don't want to get our own data, but someone else's. Let's say that we have someone else's id stored in author.obj.

scoreboard players operation $in.uid rx.io = @s author.obj

# Note that we are using `api/get` here, `get` will **not** make a new entry on the fly
function rx.playerdb:api/get

# Let's hope they had some eggs stored, I was running out
execute store result score @s eggs run data get storage rx:io playerdb.player.data.author.cool_pack.eggs

# No need to save, we are just reading someone else's data ;)
# If we were to... steal some eggs, we could save that change via:
#     function rx.playerdb:api/save

Misc Things

We can manually add a player entry for @s via:

function rx.playerdb:api/add_entry

We can also 'select' our data for $in.uid rx:io This exposing some internal logic, essentially outputs an entry @ rx:global playerdb.players[{selected:1b}]. This does interface with the live database, so you'll wanna be careful if you are using this.

scoreboard players operation $in.uid rx.io = @s rx.uid
function rx.playerdb:api/select

# (Note: select is much faster than a `get`/`save` which is why I've included in the api)

We can check whether or not a player has a database entry. This incentivizes not creating a db entry for every player, but rather creating them dynamically when you need them. You can hook into the #api/on_entry_add function tag which is talked about in a later section.

execute if score @s rx.pdb.HasEntry matches 1 run ...

Finally, if you want to see some debug info..

tag @s add rx.admin

This will provide more debug information and a cool load message (Note that I use this for all my packs)!


Listing all the players stored in the database

This just pulls up a clickable tellraw menu for player @s in chat.

function rx.playerdb:admin/list

Note that anyone can click these buttons, but only an operator can run this function.


Admin tools for testing and for servers
function rx.playerdb:admin/reset_all       # This will nuke the database and all ids, no warning!
function rx.playerdb:admin/delete_player   # This will remove `@s`'s entry from the database
function rx.playerdb:admin/remove_entry    # This will remove `$in.uid rx.io`'s entry from the database

function rx.playerdb:admin/migrate_account
# This will take the data stored at rx:temp playerdb.admin.migrate.UUID
#  and 'migrate' the data to the entity called as @s

Dynamically prepping entries when they are added

The function tag, #rx.playerdb:api/on_entry_add, allows a function to be run when an entry is added. Just plop a function tag with the function you want to fire. This function will fire before a api/get_self completes allowing you to intercept the creation ;)

The player data will already be stored in rx:io playerdb.player.data and will automatically save for you. Do not call api/save_self, just modify the data!

data modify storage rx:io playerdb.player.data.author.cool_pack set value {eggs: 0b}  # No eggs :(

Name changes: how to handle them

PlayerDB has a nifty feature of having data persist on a name change (no leftover data, etc), This allows you to cache your scoreboard scores, which are name specific, in the database.

There are two great ways of doing this:

  1. Caching scores on a slow clock (maybe every 5 minutes)
  2. Caching scores when they change

I prefer 2 since it leaves little to no room for error. If you wish to see an example of this, check out Enderchest+.

# I like to store the scores in a specific `scores` object so they are easy to identify
function rx.playerdb:api/get_self
execute store result storage rx:io playerdb.player.data.author.cool_pack.scores.eggs int 1 run scoreboard players get @s eggs

Once we implement this system, we have to implement a system to retrieve these scores when a name is changed. The function tag, #rx.playerdb:api/on_name_change, allows a function to be ran when a player changes their name. This allows you to access the old name, rx:io playerdb.old_name and the data if it has been created.

execute if score @s rx.pdb.HasEntry matches 1 store result score @s eggs run data get storage rx:io playerdb.player.data.author.cool_pack.eggs
tellraw @a ["Yo, ", {"selector": "@s"}, " changed their name from ", {"storage": "rx:io", "nbt": "playerdb.old_name"}]

Make sure you prepend execute if score @s rx.pdb.HasEntry matches 1 to any data get you perform otherwise, you might just be getting null data (which automatically gives 0 in Minecraft).


Lantern Load

This project uses Lantern Load. This allows you to ensure your datapack loads after library to ensure you can use everything this datapack provides. You can also detect whether this library is loaded by checking if score PlayerDB load matches 1...

Do I have to use this?

Maybe. If you create an entry for every player that joins, there's a tick where your api/* command will run before any of PlayerDB's ticking commands. This will cause an inconsistency where PlayerDB has not yet set up the player (rx.uid + UUID Storage). There are ways to avoid this, but the best solution is to use Lantern Load in your project.

Example

Once you've copied Load into your datapack, navigate to the #load:load function tag. This should simulate the contents of the function tag (note, you can add more dependencies if you have them):

{
    "values": [
        "#load:rx/playerdb",
        "#load:<namespace>/<datapack>"
    ]
}

Then, make sure you have defined an empty #load:rx/playerdb and in your own #load tag, you should define your personal load function. This will ensure PlayerDB's load will occur before yours. If PlayerDB does not exist, load will continue and you can detect this by checking the PlayerDB load score. For an example of all of this, checkout EnderChest+ as an example of a datapack relying on PlayerDB.

Versioning

You can detect the version of the datapack via two methods:

a) The version is stored in storage. rx:info playerdb.version as major, minor, and patch.

b) As of V1.0.1, the version is also available in the load scoreboard under rx.pdb.major, rx.pdb.minor, rx.pdb.patch

Some examples

EnderChest+ This datapack allows for expandable EnderChests with complete multiplayer compatibility!

Technical bits

Every player is given a unique id scoreboard, rx.uid. This is a number that starts counting from 1, $uid.next rx.uid, and every player gets an incrementing number. When a player wants to create a new entry via api/add_entry or api/get_self (which creates an entry for you), a new nbt compound is added a list located at rx:global playerdb.players. Each player data is organized as so: {selected: 0b, info:{name: '<player name>', uid: <scoreboard uid>, UUID: <player UUID>}, data:{...}, bit0: xb, bit1: xb, ..., bitn: xb}. When a player is given a uid, bits will be generated inside the entry bit0: xb based on the binary breakdown of the uid. This is used for the selection/filtering algorithm.

When a get or save operation is called, the program will filter down the database to select the correct entry to the input uid via @s rx.uid or $in.uid rx.io. The filtering process is really unique and this is the crux of the entire library so I'll describe it in more detail.

When you run a get or save, you will most likely trigger a selection algorithm (impl/select). Essentially, this modifies every entry's selected nbt to 1b. The system will then call the bit0 filtering function which determines the first bit of the uid and modifies all entries selected nbt to 0b if they don't match. If there are more than 1 entries with selected:1b, it will continue to the next bit, else it will short-circuit and stop. At the end of the selection process, there should be either 0 or 1 entries in the database with selected:1b which u can select via rx:global playerdb.players[{selected:1b}].

Saving will usually filter (although there are some optimizations to skip that if you perform a get and a save right next to each other) and then just replace the entry while get just copies the entry into rx:io.

Shoutouts

  • You :) - For checking out my GH page :D!
  • Speedy2025#2025 - Helped me test it to make sure I never broke it.
  • the der discohund#8028 - Biggest boost on optimization (selection > copying)
  • nphhpn#0575 - Gave me some advice on optimizations!
  • AmberW#4615 - She threw out the initial idea and I've built on from that.
  • vdvman1#9510 - For helping me figure out some of the nbt manipulation nonsense (and some optimizations).
  • r/minecraftcommands discord - Pretty helpful w/ feedback and a great community, check it out!

Endnote

This datapack was a calling to one of the more annoying issues with custom nbt in Minecraft: dynamic list indexing. While 1.14 gave us an amazing command, data modify, allowing us to modify the deeper details of nbt, and 1.15 giving us data storage allowing us to store arbitrary nbt 'in the cloud', we still struggle with a couple of things, namely, indexing. There is no way to index into an NBT List based on a scoreboard value. This means if we needed to store a list of entries tied to a scoreboard, we would be forced to iterate through the entries which is an O(n) solution to a traditionally O(1) solution in most programming languages.

The smart folk over at r/minecraftcommands discord came up with various solutions to solve this problem and this datapack is one solution that I've grown quite fond over. While I like the usefulness of this library, I've mostly been wanting to understand some of the weird problems that Minecraft throws at us.

This stuff is pretty hard to work through and somewhat hard to envision, but essentially, this is a player database that you can easily use. It's a pretty technical thing, but hopefully, the API is not too hard to use. If you have any questions, create a GitHub issue or join my discord here

playerdb's People

Contributors

dorkorc avatar griftermage avatar jerozgen avatar ritikshah avatar vdvman1 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.