Skip to content

Conversation

@KingTimer12
Copy link

@KingTimer12 KingTimer12 commented Aug 23, 2025

Currently, my proposal is to leverage the same database used for world storage (LMDB) and persist player data there. This follows the general idea from Bukkit/Spigot/Paper, where player data is stored inside the world folder.

But I'm still considering whether storing all player data inside data.lmdb is the best approach, or if a dedicated playerdata folder inside the world directory—where each player's data would be stored separately by UUID—might provide better organization and flexibility.

For now, the implementation will move forward with LMDB, but I’m open to feedback on which design would be preferable long-term.

Todo (work in progress, will update as needed):

  • Implement player data serialization/deserialization.
  • Store player data in LMDB under a dedicated namespace.
  • Load initial data and create the first player entry.
  • Update data of player.
  • Implement SQLite.
    • Add SQLite support.
    • Change database to SQLite.
  • Player Data
    • Position
    • Rotation
    • OnGround
    • Health, Hunger, Saturation
    • Inventory & Ender Chest contents
    • Abilities (flying, invulnerable, instabuild, etc.)
    • Attributes (movement speed, attack damage, max health, etc.)
    • Spawn point (bed spawn, forced spawn)
    • Status effects (potion effects, fire, air, absorption, etc.)
    • Advancements / Statistics
    • Recipe book state
    • Misc (score, shoulder entities, last death location, etc.)

@ReCore-sys
Copy link
Collaborator

Hey thanks for getting started on this! My general plan for storing entities as a whole is to use sqlite (or turso depending on it's progress) to store them all in a normal sql database, as opposed to the kv format of lmdb. If you'd be comfortable starting on that that'd be great, otherwise just use lmdb for now and I can move it over later.

@KingTimer12
Copy link
Author

Ok, I'll switch to SQLite and use the idea of ​​the playerdata folder but in a SQL database (playerdata.db) 👍

Copy link
Collaborator

@ReCore-sys ReCore-sys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly good changes, but a lot of OOP patterns that don't really fit the project, especially the PlayerData stuff since that can actively harm bevy's parallel processing capabilities.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementing this as a trait doesn't make much sense since it has quite a different use-case as the KV store, especially since we'd like be storing entities in here as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I separated them because both are banks and have, in my view, common functions and, if they decided in the future to change the bank that stores the players, I think it would make things easier...

@KingTimer12 KingTimer12 marked this pull request as ready for review October 26, 2025 15:13
Copy link
Member

@Sweattypalms Sweattypalms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed what I could. Not that experienced with database, @ReCore-sys would be able to review that section properly. But is SQL Injection a thing we should be worried about? I reckon not, since we have control over the database. And also traits for database seems interesting, again, not an expert so recore will have to check that out.

Comment on lines +1 to +20
use bevy_ecs::{event::EventReader, prelude::Res, system::Commands};
use ferrumc_core::conn::player_disconnect_event::PlayerDisconnectEvent;
use ferrumc_state::GlobalStateResource;

pub fn handle(
mut cmd: Commands,
mut events: EventReader<PlayerDisconnectEvent>,
state: Res<GlobalStateResource>,
) {
for event in events.read() {
let uuid = event.identity.uuid.as_u128();
let username = &event.identity.username;
if let Err(e) = state.0.world.save_player_state(uuid, &event.data) {
tracing::error!("Failed to save player state for {}: {}", username, e);
} else {
tracing::info!("Player state saved for {}", username);
}
cmd.entity(event.entity).despawn();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from what i see, this is an event handler, why is this in the packet handler dir? is confusing if its the disconnect event received from connection killer and server kill.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I didn't know where to put it, after all, we didn't have a folder called events and I didn't know whether to put it inside systems or packets (the reason for putting packets is because of the "head_rot" event)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head rot event is most likely being called from a packet. on the other hand the event you made has nothing to do with packets. why not make a separate module specifically for events? i reckon it'd be best?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, having it here makes little sense

Comment on lines +1 to +20
use bevy_ecs::{event::EventReader, prelude::Res, system::Commands};
use ferrumc_core::conn::player_disconnect_event::PlayerDisconnectEvent;
use ferrumc_state::GlobalStateResource;

pub fn handle(
mut cmd: Commands,
mut events: EventReader<PlayerDisconnectEvent>,
state: Res<GlobalStateResource>,
) {
for event in events.read() {
let uuid = event.identity.uuid.as_u128();
let username = &event.identity.username;
if let Err(e) = state.0.world.save_player_state(uuid, &event.data) {
tracing::error!("Failed to save player state for {}: {}", username, e);
} else {
tracing::info!("Player state saved for {}", username);
}
cmd.entity(event.entity).despawn();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, having it here makes little sense

for (entity, conn, identity, position, on_ground, rotation) in query.iter() {
if state.0.players.is_connected(entity) {
let player_disconnect = PlayerDisconnectEvent {
data: PlayerData::new(position, on_ground.0, "overworld", rotation),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we including this data here? The event handler can just fetch this data if you give it the entity

@@ -0,0 +1,33 @@
use crate::errors::StorageError;

pub trait Database {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trait is largely not going to work when we need to start doing more complicated SQL queries and we won't be doing any dynamic dispatch shenanigans with multiple DB backends, so I'm not sure if using a trait here is the right move


fn insert(&self, table: &str, key: Self::Key, value: Self::Value) -> Result<(), StorageError> {
let conn = self.open_conn()?;
let json_val: Value =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using json here? we know the fields, you can just store those as columns in the table, that kinda the whole point of SQL databases


impl World {
pub fn save_player_state(&self, key: u128, state: &PlayerData) -> Result<(), PlayerDataError> {
self.player_state_backend.create_table(TABLE_NAME)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to create a table each time you call, just generate the tables in the setup/init stage and assume it's there, sqlite will error out if it's not

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

3 participants