selector
Build / build (push) Has been cancelled

This commit is contained in:
gpatruno
2026-06-06 23:28:59 +02:00
parent 48a1b900c7
commit 211779785d
29 changed files with 2679 additions and 323 deletions
@@ -118,7 +118,11 @@ public final class MmorpgPlugin extends JavaPlugin {
OpenCustomUIInteraction.registerSimple(this, PlayerInfoPage.class, "mmorpg_player_info", playerRef -> { OpenCustomUIInteraction.registerSimple(this, PlayerInfoPage.class, "mmorpg_player_info", playerRef -> {
PlayerProfile profile = sessions.getSession(playerRef.getUuid()); PlayerProfile profile = sessions.getSession(playerRef.getUuid());
if (profile == null) { if (profile == null) {
profile = new PlayerProfile(playerRef.getUuid(), playerRef.getUsername(), System.currentTimeMillis()); profile = new PlayerProfile(
java.util.UUID.randomUUID(),
playerRef.getUuid(),
playerRef.getUsername(),
System.currentTimeMillis());
} }
return new PlayerInfoPage(playerRef, profile); return new PlayerInfoPage(playerRef, profile);
}); });
@@ -11,8 +11,12 @@ import com.disklexar.mmorpg.combat.StunService;
import com.disklexar.mmorpg.core.config.MmorpgConfig; import com.disklexar.mmorpg.core.config.MmorpgConfig;
import com.disklexar.mmorpg.economy.EconomyService; import com.disklexar.mmorpg.economy.EconomyService;
import com.disklexar.mmorpg.persistence.DatabaseManager; import com.disklexar.mmorpg.persistence.DatabaseManager;
import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository;
import com.disklexar.mmorpg.persistence.repository.CharacterRepository;
import com.disklexar.mmorpg.persistence.repository.GroupRepository; import com.disklexar.mmorpg.persistence.repository.GroupRepository;
import com.disklexar.mmorpg.persistence.repository.PlayerProfileRepository; import com.disklexar.mmorpg.persistence.repository.PlayerAccountRepository;
import com.disklexar.mmorpg.player.CharacterInventoryService;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry; import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService; import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.progression.ProgressionService; import com.disklexar.mmorpg.progression.ProgressionService;
@@ -46,12 +50,12 @@ public final class Bootstrap {
public void initialize() throws IOException { public void initialize() throws IOException {
config = MmorpgConfig.load(plugin.getDataDirectory()); config = MmorpgConfig.load(plugin.getDataDirectory());
// Persistence (DatabaseManager must start first so repositories can query).
DatabaseManager databaseManager = new DatabaseManager(plugin, config); DatabaseManager databaseManager = new DatabaseManager(plugin, config);
PlayerProfileRepository profileRepository = new PlayerProfileRepository(databaseManager); PlayerAccountRepository accountRepository = new PlayerAccountRepository(databaseManager);
CharacterRepository characterRepository = new CharacterRepository(databaseManager);
CharacterInventoryRepository inventoryRepository = new CharacterInventoryRepository(databaseManager);
GroupRepository groupRepository = new GroupRepository(databaseManager); GroupRepository groupRepository = new GroupRepository(databaseManager);
// Stateless domain services.
ProgressionService progression = new ProgressionService(config.getBaseXpPerLevel()); ProgressionService progression = new ProgressionService(config.getBaseXpPerLevel());
EconomyService economy = new EconomyService(); EconomyService economy = new EconomyService();
ClassService classService = new ClassService(); ClassService classService = new ClassService();
@@ -63,9 +67,17 @@ public final class Bootstrap {
StunService stunService = new StunService(); StunService stunService = new StunService();
OnlinePlayerRegistry onlinePlayers = new OnlinePlayerRegistry(); OnlinePlayerRegistry onlinePlayers = new OnlinePlayerRegistry();
// Session and lifecycle services. PlayerSessionService sessionService = new PlayerSessionService(
PlayerSessionService sessionService = plugin, config, accountRepository, characterRepository);
new PlayerSessionService(plugin, config, profileRepository); CharacterInventoryService characterInventoryService =
new CharacterInventoryService(inventoryRepository);
CharacterService characterService = new CharacterService(
plugin,
config,
characterRepository,
accountRepository,
characterInventoryService,
sessionService);
MmorpgScheduler scheduler = new MmorpgScheduler(plugin); MmorpgScheduler scheduler = new MmorpgScheduler(plugin);
PoisonService poisonService = new PoisonService(scheduler); PoisonService poisonService = new PoisonService(scheduler);
GroupService groupService = GroupService groupService =
@@ -79,7 +91,9 @@ public final class Bootstrap {
registry.register(MmorpgConfig.class, config); registry.register(MmorpgConfig.class, config);
registry.register(DatabaseManager.class, databaseManager); registry.register(DatabaseManager.class, databaseManager);
registry.register(PlayerProfileRepository.class, profileRepository); registry.register(PlayerAccountRepository.class, accountRepository);
registry.register(CharacterRepository.class, characterRepository);
registry.register(CharacterInventoryRepository.class, inventoryRepository);
registry.register(GroupRepository.class, groupRepository); registry.register(GroupRepository.class, groupRepository);
registry.register(ProgressionService.class, progression); registry.register(ProgressionService.class, progression);
registry.register(EconomyService.class, economy); registry.register(EconomyService.class, economy);
@@ -93,6 +107,8 @@ public final class Bootstrap {
registry.register(StunService.class, stunService); registry.register(StunService.class, stunService);
registry.register(OnlinePlayerRegistry.class, onlinePlayers); registry.register(OnlinePlayerRegistry.class, onlinePlayers);
registry.register(PlayerSessionService.class, sessionService); registry.register(PlayerSessionService.class, sessionService);
registry.register(CharacterInventoryService.class, characterInventoryService);
registry.register(CharacterService.class, characterService);
registry.register(MmorpgScheduler.class, scheduler); registry.register(MmorpgScheduler.class, scheduler);
registry.register(GroupService.class, groupService); registry.register(GroupService.class, groupService);
registry.register(AbilityService.class, abilityService); registry.register(AbilityService.class, abilityService);
@@ -11,9 +11,13 @@ import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.MovementSettings; import com.hypixel.hytale.protocol.MovementSettings;
import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager; import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
/** /**
@@ -71,8 +75,12 @@ public final class PassiveEffectService implements Service {
} }
private void applySpeed(@Nonnull OnlinePlayer player, boolean buffed) { private void applySpeed(@Nonnull OnlinePlayer player, boolean buffed) {
Ref<EntityStore> ref = resolveEntityRef(player.world(), player.uuid());
if (ref == null) {
return;
}
try {
Store<EntityStore> store = player.world().getEntityStore().getStore(); Store<EntityStore> store = player.world().getEntityStore().getStore();
Ref<EntityStore> ref = player.entityRef();
MovementManager movement = store.getComponent(ref, MovementManager.getComponentType()); MovementManager movement = store.getComponent(ref, MovementManager.getComponentType());
if (movement == null) { if (movement == null) {
return; return;
@@ -88,5 +96,22 @@ public final class PassiveEffectService implements Service {
current.baseSpeed = target; current.baseSpeed = target;
movement.update(player.playerRef().getPacketHandler()); movement.update(player.playerRef().getPacketHandler());
} }
} catch (IllegalStateException ignored) {
// Player entity was removed while the reconcile task was queued.
}
}
@Nullable
private static Ref<EntityStore> resolveEntityRef(@Nonnull World world, @Nonnull UUID playerUuid) {
for (PlayerRef playerRef : world.getPlayerRefs()) {
if (!playerRef.getUuid().equals(playerUuid) || !playerRef.isValid()) {
continue;
}
Ref<EntityStore> entityRef = playerRef.getReference();
if (entityRef != null && entityRef.isValid()) {
return entityRef;
}
}
return null;
} }
} }
@@ -0,0 +1,227 @@
package com.disklexar.mmorpg.command;
import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.disklexar.mmorpg.ui.AbilityBarService;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
/**
* {@code /mmorpg character <liste|creer|choisir|selecteur>}
*/
public final class CharacterCommand extends AbstractCommandCollection {
public CharacterCommand(@Nonnull MmorpgPlugin plugin) {
super("character", "Gérer vos personnages");
addSubCommand(new ListCmd(plugin));
addSubCommand(new Create(plugin));
addSubCommand(new Choose(plugin));
addSubCommand(new Selector(plugin));
}
private static final class ListCmd extends AbstractPlayerCommand {
private final MmorpgPlugin plugin;
ListCmd(@Nonnull MmorpgPlugin plugin) {
super("liste", "Lister vos personnages");
this.plugin = plugin;
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
UUID accountUuid = playerRef.getUuid();
CharacterService characters = CommandSupport.service(plugin, CharacterService.class);
if (characters == null) {
context.sendMessage(Message.raw("Service personnages indisponible."));
return;
}
try {
List<PlayerProfile> profiles = characters.listCharacters(accountUuid);
if (profiles.isEmpty()) {
context.sendMessage(Message.raw("Aucun personnage. Utilisez /mmorpg character creer <nom>."));
return;
}
context.sendMessage(Message.raw("=== Vos personnages ==="));
PlayerProfile active = CommandSupport.profile(plugin, store, ref);
for (PlayerProfile profile : profiles) {
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
String className = playerClass == null ? "Sans classe" : playerClass.displayName();
String marker = active != null
&& active.getCharacterId().equals(profile.getCharacterId()) ? " [actif]" : "";
context.sendMessage(Message.raw(String.format(
"• %s — Nv.%d — %s%s",
profile.getDisplayName(),
profile.getLevel(),
className,
marker)));
}
} catch (SQLException e) {
context.sendMessage(Message.raw("Erreur lors du chargement des personnages."));
}
}
}
private static final class Create extends AbstractPlayerCommand {
private final MmorpgPlugin plugin;
private final OptionalArg<String> nameArg;
Create(@Nonnull MmorpgPlugin plugin) {
super("creer", "Créer un personnage");
this.plugin = plugin;
this.nameArg = withOptionalArg("nom", "Nom du personnage", ArgTypes.STRING);
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
CharacterService characters = CommandSupport.service(plugin, CharacterService.class);
if (characters == null) {
context.sendMessage(Message.raw("Service personnages indisponible."));
return;
}
try {
String name = context.get(nameArg);
if (name == null || name.isBlank()) {
name = characters.nextDefaultName(playerRef.getUuid());
}
CharacterService.CharacterCreationResult result =
characters.createCharacter(playerRef.getUuid(), name);
if (result.ok() && result.profile() != null) {
context.sendMessage(Message.raw(
"Personnage créé : " + result.profile().getDisplayName()));
} else {
context.sendMessage(Message.raw(
result.errorMessage() != null ? result.errorMessage() : "Création impossible."));
}
} catch (SQLException e) {
plugin.getLogger().at(Level.WARNING).log("Character create failed: %s", e.getMessage());
context.sendMessage(Message.raw("Erreur lors de la création du personnage."));
}
}
}
private static final class Choose extends AbstractPlayerCommand {
private final MmorpgPlugin plugin;
private final RequiredArg<String> targetArg;
Choose(@Nonnull MmorpgPlugin plugin) {
super("choisir", "Changer de personnage actif");
this.plugin = plugin;
this.targetArg = withRequiredArg("personnage", "Nom ou UUID du personnage", ArgTypes.STRING);
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
CharacterService characters = CommandSupport.service(plugin, CharacterService.class);
PlayerSessionService sessions = CommandSupport.service(plugin, PlayerSessionService.class);
AbilityBarService abilityBar = CommandSupport.service(plugin, AbilityBarService.class);
if (characters == null || sessions == null) {
context.sendMessage(Message.raw("Service personnages indisponible."));
return;
}
UUID accountUuid = playerRef.getUuid();
try {
var found = characters.findCharacter(accountUuid, context.get(targetArg));
if (found.isEmpty()) {
context.sendMessage(Message.raw("Personnage introuvable."));
return;
}
PlayerProfile current = sessions.getSession(accountUuid);
if (current != null && current.getCharacterId().equals(found.get().getCharacterId())) {
context.sendMessage(Message.raw("Ce personnage est déjà actif."));
return;
}
if (abilityBar != null) {
abilityBar.dismiss(accountUuid);
}
PlayerSessionService.AccountSession session = sessions.requireSession(accountUuid);
characters.persistActiveCharacter(session, store, ref);
world.execute(() -> {
try {
characters.activateCharacter(
playerRef, ref, store, accountUuid, found.get().getCharacterId());
context.sendMessage(Message.raw(
"Personnage actif : " + found.get().getDisplayName()));
} catch (SQLException e) {
plugin.getLogger().at(Level.WARNING).log("Character switch failed: %s", e.getMessage());
context.sendMessage(Message.raw("Impossible de changer de personnage."));
}
});
} catch (SQLException e) {
context.sendMessage(Message.raw("Erreur lors du changement de personnage."));
}
}
}
private static final class Selector extends AbstractPlayerCommand {
private final MmorpgPlugin plugin;
Selector(@Nonnull MmorpgPlugin plugin) {
super("selecteur", "Ouvrir le sélecteur de personnages");
this.plugin = plugin;
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
CharacterService characters = CommandSupport.service(plugin, CharacterService.class);
if (characters == null) {
context.sendMessage(Message.raw("Service personnages indisponible."));
return;
}
world.execute(() -> {
try {
characters.openCharacterSelector(playerRef, ref, store, playerRef.getUuid());
} catch (SQLException e) {
plugin.getLogger().at(Level.WARNING).log(
"Failed to open character selector: %s", e.getMessage());
context.sendMessage(Message.raw("Impossible d'ouvrir le selecteur de personnages."));
}
});
}
}
}
@@ -22,6 +22,7 @@ public final class MmorpgCommand extends AbstractCommandCollection {
addSubCommand(new PowerCommand(plugin)); addSubCommand(new PowerCommand(plugin));
addSubCommand(new JobCommand(plugin)); addSubCommand(new JobCommand(plugin));
addSubCommand(new GroupCommand(plugin)); addSubCommand(new GroupCommand(plugin));
addSubCommand(new CharacterCommand(plugin));
addSubCommand(new AbilityCommand(plugin)); addSubCommand(new AbilityCommand(plugin));
addSubCommand(new PasswordCommand(plugin)); addSubCommand(new PasswordCommand(plugin));
} }
@@ -32,6 +32,7 @@ public final class MmorpgHelpCommand extends AbstractAsyncCommand {
/mmorpg power <recevoir|retirer|info> [pouvoir] /mmorpg power <recevoir|retirer|info> [pouvoir]
/mmorpg job <rejoindre|quitter|info> [metier] /mmorpg job <rejoindre|quitter|info> [metier]
/mmorpg group <inviter|accepter|quitter|kick|info> [joueur] /mmorpg group <inviter|accepter|quitter|kick|info> [joueur]
/mmorpg character <liste|creer|choisir|selecteur> [nom]
/mmorpg ability <1|2|3> — utiliser une capacité de classe /mmorpg ability <1|2|3> — utiliser une capacité de classe
/mmorpg password <mdp> — mot de passe pour le site web Orion.ovh /mmorpg password <mdp> — mot de passe pour le site web Orion.ovh
(ou utilisez directement les touches « Use ability 1/2/3 » en jeu) (ou utilisez directement les touches « Use ability 1/2/3 » en jeu)
@@ -2,7 +2,7 @@ package com.disklexar.mmorpg.command;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.persistence.PasswordHasher; import com.disklexar.mmorpg.persistence.PasswordHasher;
import com.disklexar.mmorpg.persistence.repository.PlayerProfileRepository; import com.disklexar.mmorpg.persistence.repository.PlayerAccountRepository;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.Message;
@@ -44,7 +44,7 @@ public final class PasswordCommand extends AbstractPlayerCommand {
@Nonnull PlayerRef playerRef, @Nonnull PlayerRef playerRef,
@Nonnull World world) { @Nonnull World world) {
UUID uuid = CommandSupport.uuid(store, ref); UUID uuid = CommandSupport.uuid(store, ref);
PlayerProfileRepository profiles = CommandSupport.service(plugin, PlayerProfileRepository.class); PlayerAccountRepository profiles = CommandSupport.service(plugin, PlayerAccountRepository.class);
if (uuid == null || profiles == null) { if (uuid == null || profiles == null) {
context.sendMessage(Message.raw("Profil indisponible.")); context.sendMessage(Message.raw("Profil indisponible."));
return; return;
@@ -26,6 +26,8 @@ public final class MmorpgConfig {
private int baseXpPerLevel = 100; private int baseXpPerLevel = 100;
@SerializedName("KillExperience") @SerializedName("KillExperience")
private int killExperience = 10; private int killExperience = 10;
@SerializedName("MaxCharactersPerAccount")
private int maxCharactersPerAccount = 5;
private DatabaseConfig database = new DatabaseConfig(); private DatabaseConfig database = new DatabaseConfig();
private FeaturesConfig features = new FeaturesConfig(); private FeaturesConfig features = new FeaturesConfig();
@@ -75,6 +77,10 @@ public final class MmorpgConfig {
return killExperience; return killExperience;
} }
public int getMaxCharactersPerAccount() {
return maxCharactersPerAccount;
}
public DatabaseConfig getDatabase() { public DatabaseConfig getDatabase() {
return database; return database;
} }
@@ -2,21 +2,21 @@ package com.disklexar.mmorpg.events;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.bootstrap.Bootstrap; import com.disklexar.mmorpg.bootstrap.Bootstrap;
import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.combat.MmorpgScheduler; import com.disklexar.mmorpg.combat.MmorpgScheduler;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry; import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService; import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassService;
import com.disklexar.mmorpg.ui.AbilityBarService; import com.disklexar.mmorpg.ui.AbilityBarService;
import com.disklexar.mmorpg.ui.CharacterSelectPage;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.entity.UUIDComponent; import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent; import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
@@ -50,41 +50,28 @@ public final class PlayerConnectionHandler {
Store<EntityStore> store = player.getWorld().getEntityStore().getStore(); Store<EntityStore> store = player.getWorld().getEntityStore().getStore();
PlayerRef universePlayerRef = store.getComponent(entityRef, PlayerRef.getComponentType()); PlayerRef universePlayerRef = store.getComponent(entityRef, PlayerRef.getComponentType());
PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class); PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class);
CharacterService characters = bootstrap.getRegistry().require(CharacterService.class);
MmorpgScheduler scheduler = bootstrap.getRegistry().require(MmorpgScheduler.class);
try { try {
UUID uuid = resolveUuid(store, entityRef, player); UUID uuid = resolveUuid(store, entityRef, player);
String displayName = resolveDisplayName(store, entityRef, player); String displayName = resolveDisplayName(store, entityRef, player);
PlayerProfile profile = sessions.loadOrCreate(uuid, displayName); sessions.beginSession(uuid, displayName);
InventoryUtils.clear(entityRef, store);
if (universePlayerRef != null) { if (universePlayerRef != null) {
OnlinePlayerRegistry onlinePlayers = scheduler.runLater(() -> openCharacterSelector(
bootstrap.getRegistry().require(OnlinePlayerRegistry.class); player, entityRef, store, universePlayerRef, characters, uuid), 500L);
OnlinePlayer online = new OnlinePlayer(uuid, universePlayerRef, entityRef, player.getWorld());
onlinePlayers.add(online);
ClassService classService = bootstrap.getRegistry().require(ClassService.class);
AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
MmorpgScheduler scheduler = bootstrap.getRegistry().require(MmorpgScheduler.class);
universePlayerRef.sendMessage(Message.raw(String.format(
"Bienvenue sur le serveur MMORPG — Niveau %d | Race: %s",
profile.getLevel(),
com.disklexar.mmorpg.rpg.race.RaceCatalog.byId(profile.getRaceId()).displayName())));
universePlayerRef.sendMessage(Message.raw(
"Commandes : /mmorpg menu (profil) | /mmorpg inventory | /mmorpg class info"));
if (classService.hasClass(profile)) {
scheduler.runLater(() -> abilityBar.sync(uuid, profile), 5_000L);
}
} }
} catch (SQLException e) { } catch (SQLException e) {
logger.at(Level.SEVERE).log( logger.at(Level.SEVERE).log(
"Failed to load profile for %s: %s", "Failed to load account for %s: %s",
resolveDisplayName(store, entityRef, player), resolveDisplayName(store, entityRef, player),
e.getMessage()); e.getMessage());
if (universePlayerRef != null) { if (universePlayerRef != null) {
universePlayerRef.sendMessage(Message.raw( universePlayerRef.sendMessage(com.hypixel.hytale.server.core.Message.raw(
"Erreur lors du chargement de votre profil MMORPG.")); "Erreur lors du chargement de votre compte MMORPG."));
} }
} }
} }
@@ -96,11 +83,50 @@ public final class PlayerConnectionHandler {
} }
PlayerRef playerRef = event.getPlayerRef(); PlayerRef playerRef = event.getPlayerRef();
UUID accountUuid = playerRef.getUuid();
OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class); OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class);
onlinePlayers.remove(playerRef.getUuid()); AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
bootstrap.getRegistry().require(AbilityBarService.class).dismiss(playerRef.getUuid()); CharacterService characters = bootstrap.getRegistry().require(CharacterService.class);
PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class); PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class);
sessions.unload(playerRef.getUuid());
OnlinePlayer online = onlinePlayers.get(accountUuid);
PlayerSessionService.AccountSession session = sessions.getAccountSession(accountUuid);
Ref<EntityStore> entityRef = playerRef.getReference();
if (session != null
&& session.activeCharacter() != null
&& online != null
&& entityRef != null
&& entityRef.isValid()) {
Store<EntityStore> store = online.world().getEntityStore().getStore();
characters.persistActiveCharacter(session, store, entityRef);
}
onlinePlayers.remove(accountUuid);
abilityBar.dismiss(accountUuid);
sessions.unload(accountUuid);
}
private void openCharacterSelector(
@Nonnull Player player,
@Nonnull Ref<EntityStore> entityRef,
@Nonnull Store<EntityStore> store,
@Nonnull PlayerRef playerRef,
@Nonnull CharacterService characters,
@Nonnull UUID accountUuid) {
player.getWorld().execute(() -> {
try {
player.getPageManager().openCustomPage(entityRef, store, new CharacterSelectPage(
playerRef,
accountUuid,
characters,
plugin.getBootstrap().getConfig().getMaxCharactersPerAccount()));
} catch (Throwable t) {
logger.at(Level.WARNING).log(
"Failed to open character selector for %s: %s",
playerRef.getUsername(),
t.getMessage());
}
});
} }
@Nonnull @Nonnull
@@ -10,7 +10,7 @@ import javax.annotation.Nonnull;
public final class PasswordHasher { public final class PasswordHasher {
private static final int COST = 10; private static final int COST = 10;
public static final int MIN_LENGTH = 6; public static final int MIN_LENGTH = 3;
public static final int MAX_LENGTH = 72; public static final int MAX_LENGTH = 72;
private PasswordHasher() { private PasswordHasher() {
@@ -0,0 +1,117 @@
package com.disklexar.mmorpg.persistence.repository;
import com.disklexar.mmorpg.persistence.DatabaseManager;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* SQLite persistence for per-character inventory slots.
*/
public final class CharacterInventoryRepository {
private final DatabaseManager databaseManager;
public CharacterInventoryRepository(@Nonnull DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
}
public record InventorySlotRecord(
int sectionId,
int slotIndex,
@Nonnull String itemId,
int quantity,
@Nullable Double durability,
@Nullable Double maxDurability,
@Nullable String metadataJson) {
}
@Nonnull
public List<InventorySlotRecord> load(@Nonnull UUID characterId) throws SQLException {
String sql = """
SELECT section_id, slot_index, item_id, quantity, durability, max_durability, metadata
FROM character_inventory
WHERE character_id = ?
ORDER BY section_id, slot_index
""";
List<InventorySlotRecord> slots = new ArrayList<>();
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, characterId.toString());
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
slots.add(new InventorySlotRecord(
resultSet.getInt("section_id"),
resultSet.getInt("slot_index"),
resultSet.getString("item_id"),
resultSet.getInt("quantity"),
readNullableDouble(resultSet, "durability"),
readNullableDouble(resultSet, "max_durability"),
resultSet.getString("metadata")));
}
}
}
return slots;
}
public void replaceAll(@Nonnull UUID characterId, @Nonnull List<InventorySlotRecord> slots)
throws SQLException {
Connection connection = connection();
try (PreparedStatement delete = connection.prepareStatement(
"DELETE FROM character_inventory WHERE character_id = ?")) {
delete.setString(1, characterId.toString());
delete.executeUpdate();
}
if (slots.isEmpty()) {
return;
}
String sql = """
INSERT INTO character_inventory (
character_id, section_id, slot_index, item_id, quantity,
durability, max_durability, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement insert = connection.prepareStatement(sql)) {
for (InventorySlotRecord slot : slots) {
insert.setString(1, characterId.toString());
insert.setInt(2, slot.sectionId());
insert.setInt(3, slot.slotIndex());
insert.setString(4, slot.itemId());
insert.setInt(5, slot.quantity());
if (slot.durability() != null) {
insert.setDouble(6, slot.durability());
} else {
insert.setNull(6, java.sql.Types.REAL);
}
if (slot.maxDurability() != null) {
insert.setDouble(7, slot.maxDurability());
} else {
insert.setNull(7, java.sql.Types.REAL);
}
insert.setString(8, slot.metadataJson());
insert.addBatch();
}
insert.executeBatch();
}
}
@Nullable
private static Double readNullableDouble(@Nonnull ResultSet resultSet, @Nonnull String column)
throws SQLException {
double value = resultSet.getDouble(column);
return resultSet.wasNull() ? null : value;
}
@Nonnull
private Connection connection() {
return databaseManager.getConnection();
}
}
@@ -0,0 +1,169 @@
package com.disklexar.mmorpg.persistence.repository;
import com.disklexar.mmorpg.persistence.DatabaseManager;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* SQLite persistence for playable characters.
*/
public final class CharacterRepository {
private static final Gson GSON = new Gson();
private static final Type STRING_LIST = new TypeToken<List<String>>() {
}.getType();
private final DatabaseManager databaseManager;
public CharacterRepository(@Nonnull DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
}
@Nonnull
public Optional<PlayerProfile> findById(@Nonnull UUID characterId) throws SQLException {
return findByQuery("WHERE id = ?", characterId.toString()).stream().findFirst();
}
@Nonnull
public List<PlayerProfile> findByAccountUuid(@Nonnull UUID accountUuid) throws SQLException {
return findByQuery("WHERE account_uuid = ? ORDER BY date_creation ASC", accountUuid.toString());
}
public int countByAccount(@Nonnull UUID accountUuid) throws SQLException {
String sql = "SELECT COUNT(*) FROM characters WHERE account_uuid = ?";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, accountUuid.toString());
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? resultSet.getInt(1) : 0;
}
}
}
public boolean nameExists(@Nonnull UUID accountUuid, @Nonnull String name) throws SQLException {
String sql = """
SELECT 1 FROM characters
WHERE account_uuid = ? AND name = ? COLLATE NOCASE
LIMIT 1
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, accountUuid.toString());
statement.setString(2, name.trim());
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next();
}
}
}
@Nonnull
public PlayerProfile save(@Nonnull PlayerProfile profile) throws SQLException {
String sql = """
INSERT INTO characters (
id, account_uuid, name, level, experience, class_id, powers, jobs,
guild_id, group_id, money, race_id, date_creation, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
level = excluded.level,
experience = excluded.experience,
class_id = excluded.class_id,
powers = excluded.powers,
jobs = excluded.jobs,
guild_id = excluded.guild_id,
group_id = excluded.group_id,
money = excluded.money,
race_id = excluded.race_id,
updated_at = excluded.updated_at
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, profile.getCharacterId().toString());
statement.setString(2, profile.getAccountUuid().toString());
statement.setString(3, profile.getDisplayName());
statement.setInt(4, profile.getLevel());
statement.setLong(5, profile.getExperience());
statement.setString(6, profile.getClassId());
statement.setString(7, GSON.toJson(profile.getPowers()));
statement.setString(8, GSON.toJson(profile.getJobs()));
statement.setString(9, profile.getGuildId());
statement.setString(10, profile.getGroupId());
statement.setLong(11, profile.getMoney());
statement.setString(12, profile.getRaceId());
statement.setLong(13, profile.getDateCreation());
statement.setLong(14, profile.getUpdatedAt());
statement.executeUpdate();
}
return profile;
}
@Nonnull
private List<PlayerProfile> findByQuery(@Nonnull String whereClause, @Nonnull String... params)
throws SQLException {
String sql = """
SELECT id, account_uuid, name, level, experience, class_id, powers, jobs,
guild_id, group_id, money, race_id, date_creation, updated_at
FROM characters
""" + whereClause;
List<PlayerProfile> profiles = new ArrayList<>();
try (PreparedStatement statement = connection().prepareStatement(sql)) {
for (int i = 0; i < params.length; i++) {
statement.setString(i + 1, params[i]);
}
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
profiles.add(mapRow(resultSet));
}
}
}
return profiles;
}
@Nonnull
private PlayerProfile mapRow(@Nonnull ResultSet resultSet) throws SQLException {
PlayerProfile profile = new PlayerProfile(
UUID.fromString(resultSet.getString("id")),
UUID.fromString(resultSet.getString("account_uuid")),
resultSet.getString("name"),
resultSet.getLong("date_creation"));
profile.setLevel(resultSet.getInt("level"));
profile.setExperience(resultSet.getLong("experience"));
profile.setClassId(resultSet.getString("class_id"));
profile.getPowers().addAll(parseList(resultSet.getString("powers")));
profile.getJobs().addAll(parseList(resultSet.getString("jobs")));
profile.setGuildId(resultSet.getString("guild_id"));
profile.setGroupId(resultSet.getString("group_id"));
profile.setMoney(resultSet.getLong("money"));
String raceId = resultSet.getString("race_id");
profile.setRaceId(raceId != null ? raceId : "human");
profile.setUpdatedAt(resultSet.getLong("updated_at"));
return profile;
}
@Nonnull
private List<String> parseList(@Nullable String json) {
if (json == null || json.isBlank()) {
return List.of();
}
List<String> parsed = GSON.fromJson(json, STRING_LIST);
return parsed != null ? parsed : List.of();
}
@Nonnull
private Connection connection() {
return databaseManager.getConnection();
}
}
@@ -0,0 +1,166 @@
package com.disklexar.mmorpg.persistence.repository;
import com.disklexar.mmorpg.persistence.DatabaseManager;
import com.disklexar.mmorpg.player.model.PlayerAccount;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
/**
* SQLite persistence for {@link PlayerAccount} rows ({@code player_profiles} table).
*/
public final class PlayerAccountRepository {
private final DatabaseManager databaseManager;
public PlayerAccountRepository(@Nonnull DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
}
@Nonnull
public Optional<PlayerAccount> findByUuid(@Nonnull UUID uuid) throws SQLException {
String sql = """
SELECT uuid, display_name, active_character_id, is_connected, total_time_play,
last_date_connected, date_creation, updated_at
FROM player_profiles
WHERE uuid = ?
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, uuid.toString());
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
return Optional.of(mapRow(resultSet));
}
}
}
@Nonnull
public PlayerAccount save(@Nonnull PlayerAccount account) throws SQLException {
String sql = """
INSERT INTO player_profiles (
uuid, display_name, active_character_id, is_connected, total_time_play,
last_date_connected, date_creation, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(uuid) DO UPDATE SET
display_name = excluded.display_name,
active_character_id = excluded.active_character_id,
is_connected = excluded.is_connected,
total_time_play = excluded.total_time_play,
last_date_connected = excluded.last_date_connected,
updated_at = excluded.updated_at
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, account.getUuid().toString());
statement.setString(2, account.getDisplayName());
statement.setString(3, account.getActiveCharacterId() == null
? null
: account.getActiveCharacterId().toString());
statement.setInt(4, account.isConnected() ? 1 : 0);
statement.setLong(5, account.getTotalTimePlay());
statement.setLong(6, account.getLastDateConnected());
statement.setLong(7, account.getDateCreation());
statement.setLong(8, account.getDateCreation());
statement.setLong(9, account.getUpdatedAt());
statement.executeUpdate();
}
return account;
}
public void setActiveCharacter(@Nonnull UUID accountUuid, @Nullable UUID characterId) throws SQLException {
String sql = """
UPDATE player_profiles
SET active_character_id = ?, updated_at = ?
WHERE uuid = ?
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, characterId == null ? null : characterId.toString());
statement.setLong(2, System.currentTimeMillis());
statement.setString(3, accountUuid.toString());
statement.executeUpdate();
}
}
public void setPasswordHash(@Nonnull UUID uuid, @Nonnull String passwordHash) throws SQLException {
String sql = """
UPDATE player_profiles
SET password_hash = ?, updated_at = ?
WHERE uuid = ?
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, passwordHash);
statement.setLong(2, System.currentTimeMillis());
statement.setString(3, uuid.toString());
if (statement.executeUpdate() == 0) {
throw new SQLException("Player account not found: " + uuid);
}
}
}
@Nonnull
public Optional<PlayerAuthRecord> findAuthByDisplayName(@Nonnull String displayName) throws SQLException {
String sql = """
SELECT uuid, display_name, password_hash
FROM player_profiles
WHERE display_name = ? COLLATE NOCASE
LIMIT 1
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, displayName.trim());
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
return Optional.of(new PlayerAuthRecord(
UUID.fromString(resultSet.getString("uuid")),
resultSet.getString("display_name"),
resultSet.getString("password_hash")));
}
}
}
public void clearConnectedFlags() throws SQLException {
try (PreparedStatement statement = connection().prepareStatement(
"UPDATE player_profiles SET is_connected = 0 WHERE is_connected <> 0")) {
statement.executeUpdate();
}
}
public record PlayerAuthRecord(
@Nonnull UUID uuid,
@Nonnull String displayName,
@Nullable String passwordHash) {
}
@Nonnull
private PlayerAccount mapRow(@Nonnull ResultSet resultSet) throws SQLException {
PlayerAccount account = new PlayerAccount(
UUID.fromString(resultSet.getString("uuid")),
resultSet.getString("display_name"),
resultSet.getLong("date_creation"));
String activeId = resultSet.getString("active_character_id");
if (activeId != null && !activeId.isBlank()) {
account.setActiveCharacterId(UUID.fromString(activeId));
}
account.setConnected(resultSet.getInt("is_connected") != 0);
account.setTotalTimePlay(resultSet.getLong("total_time_play"));
account.setLastDateConnected(resultSet.getLong("last_date_connected"));
account.setUpdatedAt(resultSet.getLong("updated_at"));
return account;
}
@Nonnull
private Connection connection() {
return databaseManager.getConnection();
}
}
@@ -1,193 +0,0 @@
package com.disklexar.mmorpg.persistence.repository;
import com.disklexar.mmorpg.persistence.DatabaseManager;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* SQLite persistence for {@link PlayerProfile}.
*/
public final class PlayerProfileRepository {
private static final Gson GSON = new Gson();
private static final Type STRING_LIST = new TypeToken<List<String>>() {
}.getType();
private final DatabaseManager databaseManager;
public PlayerProfileRepository(@Nonnull DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
}
@Nonnull
public Optional<PlayerProfile> findByUuid(@Nonnull UUID uuid) throws SQLException {
String sql = """
SELECT uuid, display_name, level, experience, class_id, powers, jobs,
guild_id, group_id, is_connected, total_time_play, last_date_connected,
date_creation, money, race_id, updated_at
FROM player_profiles
WHERE uuid = ?
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, uuid.toString());
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
return Optional.of(mapRow(resultSet));
}
}
}
@Nonnull
public PlayerProfile save(@Nonnull PlayerProfile profile) throws SQLException {
String sql = """
INSERT INTO player_profiles (
uuid, display_name, level, experience, class_id, powers, jobs,
guild_id, group_id, is_connected, total_time_play, last_date_connected,
date_creation, money, race_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(uuid) DO UPDATE SET
display_name = excluded.display_name,
level = excluded.level,
experience = excluded.experience,
class_id = excluded.class_id,
powers = excluded.powers,
jobs = excluded.jobs,
guild_id = excluded.guild_id,
group_id = excluded.group_id,
is_connected = excluded.is_connected,
total_time_play = excluded.total_time_play,
last_date_connected = excluded.last_date_connected,
money = excluded.money,
race_id = excluded.race_id,
updated_at = excluded.updated_at
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, profile.getUuid().toString());
statement.setString(2, profile.getDisplayName());
statement.setInt(3, profile.getLevel());
statement.setLong(4, profile.getExperience());
statement.setString(5, profile.getClassId());
statement.setString(6, GSON.toJson(profile.getPowers()));
statement.setString(7, GSON.toJson(profile.getJobs()));
statement.setString(8, profile.getGuildId());
statement.setString(9, profile.getGroupId());
statement.setInt(10, profile.isConnected() ? 1 : 0);
statement.setLong(11, profile.getTotalTimePlay());
statement.setLong(12, profile.getLastDateConnected());
statement.setLong(13, profile.getDateCreation());
statement.setLong(14, profile.getMoney());
statement.setString(15, profile.getRaceId());
// created_at mirrors date_creation to keep the original 001 column populated.
statement.setLong(16, profile.getDateCreation());
statement.setLong(17, profile.getUpdatedAt());
statement.executeUpdate();
}
return profile;
}
/** Resets {@code is_connected} for every profile (called on boot to recover from crashes). */
public void clearConnectedFlags() throws SQLException {
try (PreparedStatement statement = connection().prepareStatement(
"UPDATE player_profiles SET is_connected = 0 WHERE is_connected <> 0")) {
statement.executeUpdate();
}
}
public void setPasswordHash(@Nonnull UUID uuid, @Nonnull String passwordHash) throws SQLException {
String sql = """
UPDATE player_profiles
SET password_hash = ?, updated_at = ?
WHERE uuid = ?
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, passwordHash);
statement.setLong(2, System.currentTimeMillis());
statement.setString(3, uuid.toString());
if (statement.executeUpdate() == 0) {
throw new SQLException("Player profile not found: " + uuid);
}
}
}
@Nonnull
public Optional<PlayerAuthRecord> findAuthByDisplayName(@Nonnull String displayName) throws SQLException {
String sql = """
SELECT uuid, display_name, password_hash
FROM player_profiles
WHERE display_name = ? COLLATE NOCASE
LIMIT 1
""";
try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, displayName.trim());
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
return Optional.of(new PlayerAuthRecord(
UUID.fromString(resultSet.getString("uuid")),
resultSet.getString("display_name"),
resultSet.getString("password_hash")));
}
}
}
public record PlayerAuthRecord(
@Nonnull UUID uuid,
@Nonnull String displayName,
@Nullable String passwordHash) {
}
@Nonnull
private PlayerProfile mapRow(@Nonnull ResultSet resultSet) throws SQLException {
PlayerProfile profile = new PlayerProfile(
UUID.fromString(resultSet.getString("uuid")),
resultSet.getString("display_name"),
resultSet.getLong("date_creation"));
profile.setLevel(resultSet.getInt("level"));
profile.setExperience(resultSet.getLong("experience"));
profile.setClassId(resultSet.getString("class_id"));
profile.getPowers().addAll(parseList(resultSet.getString("powers")));
profile.getJobs().addAll(parseList(resultSet.getString("jobs")));
profile.setGuildId(resultSet.getString("guild_id"));
profile.setGroupId(resultSet.getString("group_id"));
profile.setConnected(resultSet.getInt("is_connected") != 0);
profile.setTotalTimePlay(resultSet.getLong("total_time_play"));
profile.setLastDateConnected(resultSet.getLong("last_date_connected"));
profile.setMoney(resultSet.getLong("money"));
String raceId = resultSet.getString("race_id");
profile.setRaceId(raceId != null ? raceId : "human");
profile.setUpdatedAt(resultSet.getLong("updated_at"));
return profile;
}
@Nonnull
private List<String> parseList(String json) {
if (json == null || json.isBlank()) {
return List.of();
}
List<String> parsed = GSON.fromJson(json, STRING_LIST);
return parsed != null ? parsed : List.of();
}
@Nonnull
private Connection connection() {
return databaseManager.getConnection();
}
}
@@ -26,7 +26,8 @@ public final class SchemaInitializer {
private static final List<String> MIGRATIONS = List.of( private static final List<String> MIGRATIONS = List.of(
"001_init.sql", "001_init.sql",
"002_rpg.sql", "002_rpg.sql",
"003_web_password.sql"); "003_web_password.sql",
"004_characters.sql");
private SchemaInitializer() { private SchemaInitializer() {
} }
@@ -73,10 +74,10 @@ public final class SchemaInitializer {
} }
private static void runMigration(@Nonnull Connection connection, @Nonnull String migration) throws SQLException { private static void runMigration(@Nonnull Connection connection, @Nonnull String migration) throws SQLException {
String sql = loadMigration("/db/migrations/" + migration); String sql = stripComments(loadMigration("/db/migrations/" + migration));
try (Statement statement = connection.createStatement()) { try (Statement statement = connection.createStatement()) {
for (String chunk : sql.split(";")) { for (String chunk : sql.split(";")) {
String trimmed = stripComments(chunk).trim(); String trimmed = chunk.trim();
if (!trimmed.isEmpty()) { if (!trimmed.isEmpty()) {
statement.execute(trimmed); statement.execute(trimmed);
} }
@@ -88,8 +89,11 @@ public final class SchemaInitializer {
private static String stripComments(@Nonnull String sql) { private static String stripComments(@Nonnull String sql) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for (String line : sql.split("\n")) { for (String line : sql.split("\n")) {
String trimmed = line.trim(); int commentStart = line.indexOf("--");
if (trimmed.startsWith("--")) { if (commentStart >= 0) {
line = line.substring(0, commentStart);
}
if (line.trim().isEmpty()) {
continue; continue;
} }
builder.append(line).append('\n'); builder.append(line).append('\n');
@@ -0,0 +1,128 @@
package com.disklexar.mmorpg.player;
import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository;
import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository.InventorySlotRecord;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.inventory.InventoryComponent;
import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Saves and restores player inventory per character in SQLite.
*/
public final class CharacterInventoryService {
private static final int[] SECTION_IDS = {
InventoryComponent.STORAGE_SECTION_ID,
InventoryComponent.HOTBAR_SECTION_ID,
InventoryComponent.ARMOR_SECTION_ID,
InventoryComponent.UTILITY_SECTION_ID
};
private final CharacterInventoryRepository repository;
public CharacterInventoryService(@Nonnull CharacterInventoryRepository repository) {
this.repository = repository;
}
public void saveFromPlayer(
@Nonnull UUID characterId,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) throws SQLException {
repository.replaceAll(characterId, captureFromPlayer(store, ref));
}
public void loadToPlayer(
@Nonnull UUID characterId,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) throws SQLException {
applyToPlayer(repository.load(characterId), store, ref);
}
@Nonnull
public List<InventorySlotRecord> captureFromPlayer(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
List<InventorySlotRecord> slots = new ArrayList<>();
for (int sectionId : SECTION_IDS) {
ItemContainer container = sectionContainer(store, ref, sectionId);
if (container == null) {
continue;
}
for (short slot = 0; slot < container.getCapacity(); slot++) {
ItemStack stack = container.getItemStack(slot);
if (ItemStack.isEmpty(stack)) {
continue;
}
String itemId = stack.getItemId();
if (itemId == null || itemId.isBlank()) {
continue;
}
Double durability = stack.getMaxDurability() > 0 ? stack.getDurability() : null;
Double maxDurability = stack.getMaxDurability() > 0 ? stack.getMaxDurability() : null;
slots.add(new InventorySlotRecord(
sectionId,
slot,
itemId,
stack.getQuantity(),
durability,
maxDurability,
null));
}
}
return slots;
}
public void applyToPlayer(
@Nonnull List<InventorySlotRecord> slots,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
InventoryUtils.clear(ref, store);
for (InventorySlotRecord slot : slots) {
ItemContainer container = sectionContainer(store, ref, slot.sectionId());
if (container == null || slot.slotIndex() < 0 || slot.slotIndex() >= container.getCapacity()) {
continue;
}
ItemStack stack = buildStack(slot);
if (ItemStack.isEmpty(stack)) {
continue;
}
container.setItemStackForSlot((short) slot.slotIndex(), stack);
}
}
@Nonnull
private static ItemStack buildStack(@Nonnull InventorySlotRecord slot) {
ItemStack stack = new ItemStack(slot.itemId(), slot.quantity());
if (slot.maxDurability() != null && slot.maxDurability() > 0) {
stack = stack.withMaxDurability(slot.maxDurability());
if (slot.durability() != null) {
stack = stack.withDurability(slot.durability());
}
}
return stack;
}
@Nullable
private static ItemContainer sectionContainer(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
int sectionId) {
var type = InventoryComponent.getComponentTypeById(sectionId);
if (type == null) {
return null;
}
InventoryComponent component = store.getComponent(ref, type);
return component == null ? null : component.getInventory();
}
}
@@ -0,0 +1,272 @@
package com.disklexar.mmorpg.player;
import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.combat.MmorpgScheduler;
import com.disklexar.mmorpg.core.config.MmorpgConfig;
import com.disklexar.mmorpg.persistence.repository.CharacterRepository;
import com.disklexar.mmorpg.persistence.repository.PlayerAccountRepository;
import com.disklexar.mmorpg.player.model.PlayerAccount;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassService;
import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.disklexar.mmorpg.ui.AbilityBarService;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.protocol.packets.interface_.Page;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
/**
* Character creation, selection and switching.
*/
public final class CharacterService {
private static final int MIN_NAME_LENGTH = 3;
private static final int MAX_NAME_LENGTH = 16;
private final MmorpgPlugin plugin;
private final MmorpgConfig config;
private final CharacterRepository characters;
private final PlayerAccountRepository accounts;
private final CharacterInventoryService inventory;
private final PlayerSessionService sessions;
private final HytaleLogger logger;
public CharacterService(
@Nonnull MmorpgPlugin plugin,
@Nonnull MmorpgConfig config,
@Nonnull CharacterRepository characters,
@Nonnull PlayerAccountRepository accounts,
@Nonnull CharacterInventoryService inventory,
@Nonnull PlayerSessionService sessions) {
this.plugin = plugin;
this.config = config;
this.characters = characters;
this.accounts = accounts;
this.inventory = inventory;
this.sessions = sessions;
this.logger = plugin.getLogger();
}
@Nonnull
public List<PlayerProfile> listCharacters(@Nonnull UUID accountUuid) throws SQLException {
return characters.findByAccountUuid(accountUuid);
}
@Nonnull
public Optional<PlayerProfile> findCharacter(
@Nonnull UUID accountUuid,
@Nonnull String query) throws SQLException {
String trimmed = query.trim();
for (PlayerProfile profile : characters.findByAccountUuid(accountUuid)) {
if (profile.getCharacterId().toString().equalsIgnoreCase(trimmed)
|| profile.getDisplayName().equalsIgnoreCase(trimmed)) {
return Optional.of(profile);
}
}
return Optional.empty();
}
@Nonnull
public CharacterCreationResult createCharacter(@Nonnull UUID accountUuid, @Nonnull String rawName)
throws SQLException {
String name = normalizeName(rawName);
if (name == null) {
return CharacterCreationResult.invalidName();
}
if (characters.countByAccount(accountUuid) >= config.getMaxCharactersPerAccount()) {
return CharacterCreationResult.limitReached(config.getMaxCharactersPerAccount());
}
if (characters.nameExists(accountUuid, name)) {
return CharacterCreationResult.duplicateName(name);
}
long now = System.currentTimeMillis();
PlayerProfile profile = new PlayerProfile(UUID.randomUUID(), accountUuid, name, now);
profile.setLevel(config.getDefaultLevel());
profile.setRaceId(RaceCatalog.DEFAULT_RACE_ID);
profile.touch();
characters.save(profile);
logger.at(Level.INFO).log("Created character %s for account %s", name, accountUuid);
return CharacterCreationResult.success(profile);
}
@Nonnull
public String nextDefaultName(@Nonnull UUID accountUuid) throws SQLException {
int count = characters.countByAccount(accountUuid);
return "Perso " + (count + 1);
}
public void openCharacterSelector(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID accountUuid) throws SQLException {
PlayerSessionService.AccountSession session = sessions.getAccountSession(accountUuid);
if (session != null && session.activeCharacter() != null) {
persistActiveCharacter(session, store, ref);
}
sessions.beginCharacterSelection(accountUuid);
var bootstrap = plugin.getBootstrap();
if (bootstrap != null) {
bootstrap.getRegistry().require(AbilityBarService.class).dismiss(accountUuid);
}
InventoryUtils.clear(ref, store);
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
player.getPageManager().openCustomPage(ref, store, new com.disklexar.mmorpg.ui.CharacterSelectPage(
playerRef,
accountUuid,
this,
config.getMaxCharactersPerAccount()));
}
}
public void activateCharacter(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID accountUuid,
@Nonnull UUID characterId) throws SQLException {
Optional<PlayerProfile> found = characters.findById(characterId);
if (found.isEmpty() || !found.get().getAccountUuid().equals(accountUuid)) {
throw new SQLException("Character not found for account: " + characterId);
}
PlayerSessionService.AccountSession session = sessions.requireSession(accountUuid);
if (session.activeCharacter() != null
&& !session.activeCharacter().getCharacterId().equals(characterId)) {
persistActiveCharacter(session, store, ref);
}
PlayerProfile profile = found.get();
PlayerAccount account = session.account();
profile.applyAccountSession(account);
profile.touch();
InventoryUtils.clear(ref, store);
inventory.loadToPlayer(characterId, store, ref);
account.setActiveCharacterId(characterId);
account.touch();
accounts.setActiveCharacter(accountUuid, characterId);
accounts.save(account);
sessions.activateCharacter(accountUuid, profile);
completeLogin(playerRef, ref, store, profile);
}
public void persistActiveCharacter(
@Nonnull PlayerSessionService.AccountSession session,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
PlayerProfile active = session.activeCharacter();
if (active == null) {
return;
}
try {
active.touch();
characters.save(active);
inventory.saveFromPlayer(active.getCharacterId(), store, ref);
} catch (SQLException e) {
logger.at(Level.WARNING).log(
"Failed to persist character %s: %s", active.getCharacterId(), e.getMessage());
}
}
private void completeLogin(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull PlayerProfile profile) {
var bootstrap = plugin.getBootstrap();
if (bootstrap == null) {
return;
}
OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class);
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
onlinePlayers.add(new OnlinePlayer(
profile.getUuid(), playerRef, ref, player.getWorld()));
player.getPageManager().setPage(ref, store, Page.None);
}
playerRef.sendMessage(Message.raw(String.format(
"Bienvenue — %s | Niveau %d | Race: %s",
profile.getDisplayName(),
profile.getLevel(),
RaceCatalog.byId(profile.getRaceId()).displayName())));
playerRef.sendMessage(Message.raw(
"Commandes : /mmorpg menu | /mmorpg character | /mmorpg class info"));
ClassService classService = bootstrap.getRegistry().require(ClassService.class);
AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
MmorpgScheduler scheduler = bootstrap.getRegistry().require(MmorpgScheduler.class);
if (classService.hasClass(profile)) {
scheduler.runLater(() -> abilityBar.sync(profile.getUuid(), profile), 5_000L);
}
}
@Nullable
private static String normalizeName(@Nonnull String rawName) {
String name = rawName.trim();
if (name.length() < MIN_NAME_LENGTH || name.length() > MAX_NAME_LENGTH) {
return null;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == ' ') {
continue;
}
return null;
}
return name;
}
public record CharacterCreationResult(
boolean ok,
@Nullable PlayerProfile profile,
@Nullable String errorMessage) {
static CharacterCreationResult success(@Nonnull PlayerProfile profile) {
return new CharacterCreationResult(true, profile, null);
}
static CharacterCreationResult invalidName() {
return new CharacterCreationResult(
false,
null,
String.format(
Locale.FRENCH,
"Nom invalide (%d%d caractères, lettres/chiffres/espaces/_/-).",
MIN_NAME_LENGTH,
MAX_NAME_LENGTH));
}
static CharacterCreationResult duplicateName(@Nonnull String name) {
return new CharacterCreationResult(false, null, "Un personnage nommé « " + name + " » existe déjà.");
}
static CharacterCreationResult limitReached(int max) {
return new CharacterCreationResult(
false, null, "Limite de " + max + " personnages atteinte.");
}
}
}
@@ -36,7 +36,7 @@ public final class PlayerInfoView {
@Nonnull @Nonnull
public static List<Entry> entries(@Nonnull PlayerProfile profile) { public static List<Entry> entries(@Nonnull PlayerProfile profile) {
List<Entry> entries = new ArrayList<>(); List<Entry> entries = new ArrayList<>();
entries.add(new Entry("Nom", profile.getDisplayName())); entries.add(new Entry("Personnage", profile.getDisplayName()));
entries.add(new Entry("Niveau", String.valueOf(profile.getLevel()))); entries.add(new Entry("Niveau", String.valueOf(profile.getLevel())));
entries.add(new Entry("Expérience", String.valueOf(profile.getExperience()))); entries.add(new Entry("Expérience", String.valueOf(profile.getExperience())));
entries.add(new Entry("Argent", profile.getMoney() + " money")); entries.add(new Entry("Argent", profile.getMoney() + " money"));
@@ -3,9 +3,10 @@ package com.disklexar.mmorpg.player;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.core.config.MmorpgConfig; import com.disklexar.mmorpg.core.config.MmorpgConfig;
import com.disklexar.mmorpg.core.service.Service; import com.disklexar.mmorpg.core.service.Service;
import com.disklexar.mmorpg.persistence.repository.PlayerProfileRepository; import com.disklexar.mmorpg.persistence.repository.CharacterRepository;
import com.disklexar.mmorpg.persistence.repository.PlayerAccountRepository;
import com.disklexar.mmorpg.player.model.PlayerAccount;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.HytaleLogger;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@@ -18,31 +19,50 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level; import java.util.logging.Level;
/** /**
* Tracks online player sessions and synchronizes profiles with SQLite. * Tracks online account sessions and synchronizes characters with SQLite.
*/ */
public final class PlayerSessionService implements Service { public final class PlayerSessionService implements Service {
public record AccountSession(
@Nonnull PlayerAccount account,
@Nullable PlayerProfile activeCharacter,
boolean awaitingCharacterSelection) {
@Nullable
public PlayerProfile activeCharacter() {
return activeCharacter;
}
@Nonnull
public PlayerAccount account() {
return account;
}
}
private final MmorpgPlugin plugin; private final MmorpgPlugin plugin;
private final MmorpgConfig config; private final MmorpgConfig config;
private final PlayerProfileRepository repository; private final PlayerAccountRepository accountRepository;
private final CharacterRepository characterRepository;
private final HytaleLogger logger; private final HytaleLogger logger;
private final Map<UUID, PlayerProfile> sessions = new ConcurrentHashMap<>(); private final Map<UUID, AccountSession> sessions = new ConcurrentHashMap<>();
private final Map<UUID, Long> connectedAt = new ConcurrentHashMap<>(); private final Map<UUID, Long> connectedAt = new ConcurrentHashMap<>();
public PlayerSessionService( public PlayerSessionService(
@Nonnull MmorpgPlugin plugin, @Nonnull MmorpgPlugin plugin,
@Nonnull MmorpgConfig config, @Nonnull MmorpgConfig config,
@Nonnull PlayerProfileRepository repository) { @Nonnull PlayerAccountRepository accountRepository,
@Nonnull CharacterRepository characterRepository) {
this.plugin = plugin; this.plugin = plugin;
this.config = config; this.config = config;
this.repository = repository; this.accountRepository = accountRepository;
this.characterRepository = characterRepository;
this.logger = plugin.getLogger(); this.logger = plugin.getLogger();
} }
@Override @Override
public void start() { public void start() {
try { try {
repository.clearConnectedFlags(); accountRepository.clearConnectedFlags();
} catch (SQLException e) { } catch (SQLException e) {
logger.at(Level.WARNING).log("Failed to reset connection flags: %s", e.getMessage()); logger.at(Level.WARNING).log("Failed to reset connection flags: %s", e.getMessage());
} }
@@ -51,10 +71,14 @@ public final class PlayerSessionService implements Service {
@Override @Override
public void shutdown() { public void shutdown() {
for (PlayerProfile profile : sessions.values()) { for (AccountSession session : sessions.values()) {
accumulatePlaytime(profile.getUuid(), profile); accumulatePlaytime(session.account().getUuid());
profile.setConnected(false); PlayerAccount account = session.account();
saveQuietly(profile); account.setConnected(false);
saveAccountQuietly(account);
if (session.activeCharacter() != null) {
saveCharacterQuietly(session.activeCharacter());
}
} }
sessions.clear(); sessions.clear();
connectedAt.clear(); connectedAt.clear();
@@ -62,61 +86,132 @@ public final class PlayerSessionService implements Service {
} }
/** /**
* Loads (or creates) a profile and marks the player connected. * Loads (or creates) the account and opens the character selection flow.
*/ */
@Nonnull @Nonnull
public PlayerProfile loadOrCreate(@Nonnull UUID uuid, @Nonnull String displayName) throws SQLException { public AccountSession beginSession(@Nonnull UUID accountUuid, @Nonnull String displayName)
throws SQLException {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
Optional<PlayerProfile> existing = repository.findByUuid(uuid); Optional<PlayerAccount> existing = accountRepository.findByUuid(accountUuid);
PlayerProfile profile; PlayerAccount account;
if (existing.isPresent()) { if (existing.isPresent()) {
profile = existing.get(); account = existing.get();
profile.setDisplayName(displayName); account.setDisplayName(displayName);
} else { } else {
profile = new PlayerProfile(uuid, displayName, now); account = new PlayerAccount(accountUuid, displayName, now);
profile.setLevel(config.getDefaultLevel()); logger.at(Level.INFO).log("Created new MMORPG account for %s", displayName);
profile.setRaceId(RaceCatalog.DEFAULT_RACE_ID);
logger.at(Level.INFO).log("Created new MMORPG profile for %s", displayName);
} }
profile.setConnected(true); account.setConnected(true);
profile.setLastDateConnected(now); account.setLastDateConnected(now);
profile.touch(); account.touch();
sessions.put(uuid, profile);
connectedAt.put(uuid, now); AccountSession session = new AccountSession(account, null, true);
saveQuietly(profile); sessions.put(accountUuid, session);
return profile; connectedAt.put(accountUuid, now);
saveAccountQuietly(account);
return session;
}
public void activateCharacter(@Nonnull UUID accountUuid, @Nonnull PlayerProfile profile) {
AccountSession current = sessions.get(accountUuid);
if (current == null) {
return;
}
profile.applyAccountSession(current.account());
AccountSession updated = new AccountSession(current.account(), profile, false);
sessions.put(accountUuid, updated);
}
/**
* Puts the account back into character-selection mode (active character cleared from session).
*/
public void beginCharacterSelection(@Nonnull UUID accountUuid) {
AccountSession current = sessions.get(accountUuid);
if (current == null) {
return;
}
sessions.put(accountUuid, new AccountSession(current.account(), null, true));
}
public boolean isAwaitingCharacterSelection(@Nonnull UUID accountUuid) {
AccountSession session = sessions.get(accountUuid);
return session != null && session.awaitingCharacterSelection();
} }
@Nullable @Nullable
public PlayerProfile getSession(@Nonnull UUID uuid) { public PlayerProfile getSession(@Nonnull UUID accountUuid) {
return sessions.get(uuid); AccountSession session = sessions.get(accountUuid);
if (session == null || session.awaitingCharacterSelection()) {
return null;
}
return session.activeCharacter();
} }
public void unload(@Nonnull UUID uuid) { @Nullable
PlayerProfile profile = sessions.remove(uuid); public AccountSession getAccountSession(@Nonnull UUID accountUuid) {
if (profile == null) { return sessions.get(accountUuid);
}
@Nonnull
public AccountSession requireSession(@Nonnull UUID accountUuid) throws SQLException {
AccountSession session = sessions.get(accountUuid);
if (session == null) {
throw new SQLException("No active session for account: " + accountUuid);
}
return session;
}
public void unload(@Nonnull UUID accountUuid) {
AccountSession session = sessions.remove(accountUuid);
if (session == null) {
return; return;
} }
accumulatePlaytime(uuid, profile); accumulatePlaytime(accountUuid);
profile.setConnected(false); PlayerAccount account = session.account();
profile.touch(); account.setConnected(false);
saveQuietly(profile); account.touch();
} saveAccountQuietly(account);
if (session.activeCharacter() != null) {
private void accumulatePlaytime(@Nonnull UUID uuid, @Nonnull PlayerProfile profile) { saveCharacterQuietly(session.activeCharacter());
Long start = connectedAt.remove(uuid);
if (start != null) {
profile.addTimePlay(System.currentTimeMillis() - start);
} }
} }
private void saveQuietly(@Nonnull PlayerProfile profile) { public void saveActiveCharacter(@Nonnull UUID accountUuid) {
AccountSession session = sessions.get(accountUuid);
if (session == null || session.activeCharacter() == null) {
return;
}
saveCharacterQuietly(session.activeCharacter());
}
private void accumulatePlaytime(@Nonnull UUID accountUuid) {
Long start = connectedAt.remove(accountUuid);
AccountSession session = sessions.get(accountUuid);
if (start == null || session == null) {
return;
}
session.account().addTimePlay(System.currentTimeMillis() - start);
if (session.activeCharacter() != null) {
session.activeCharacter().applyAccountSession(session.account());
}
}
private void saveAccountQuietly(@Nonnull PlayerAccount account) {
try { try {
repository.save(profile); accountRepository.save(account);
} catch (SQLException e) { } catch (SQLException e) {
logger.at(Level.WARNING).log( logger.at(Level.WARNING).log(
"Failed to save profile for %s: %s", profile.getUuid(), e.getMessage()); "Failed to save account for %s: %s", account.getUuid(), e.getMessage());
}
}
private void saveCharacterQuietly(@Nonnull PlayerProfile profile) {
try {
characterRepository.save(profile);
} catch (SQLException e) {
logger.at(Level.WARNING).log(
"Failed to save character for %s: %s", profile.getCharacterId(), e.getMessage());
} }
} }
} }
@@ -0,0 +1,95 @@
package com.disklexar.mmorpg.player.model;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.UUID;
/**
* Persistent MMORPG account (one Hytale player UUID).
*/
public final class PlayerAccount {
private final UUID uuid;
private String displayName;
@Nullable
private UUID activeCharacterId;
private boolean connected;
private long totalTimePlay;
private long lastDateConnected;
private final long dateCreation;
private long updatedAt;
public PlayerAccount(@Nonnull UUID uuid, @Nonnull String displayName, long dateCreation) {
this.uuid = uuid;
this.displayName = displayName;
this.dateCreation = dateCreation;
this.updatedAt = dateCreation;
}
@Nonnull
public UUID getUuid() {
return uuid;
}
@Nonnull
public String getDisplayName() {
return displayName;
}
public void setDisplayName(@Nonnull String displayName) {
this.displayName = displayName;
}
@Nullable
public UUID getActiveCharacterId() {
return activeCharacterId;
}
public void setActiveCharacterId(@Nullable UUID activeCharacterId) {
this.activeCharacterId = activeCharacterId;
}
public boolean isConnected() {
return connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
public long getTotalTimePlay() {
return totalTimePlay;
}
public void setTotalTimePlay(long totalTimePlay) {
this.totalTimePlay = totalTimePlay;
}
public void addTimePlay(long deltaMillis) {
this.totalTimePlay += Math.max(0L, deltaMillis);
}
public long getLastDateConnected() {
return lastDateConnected;
}
public void setLastDateConnected(long lastDateConnected) {
this.lastDateConnected = lastDateConnected;
}
public long getDateCreation() {
return dateCreation;
}
public long getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(long updatedAt) {
this.updatedAt = updatedAt;
}
public void touch() {
this.updatedAt = System.currentTimeMillis();
}
}
@@ -7,15 +7,12 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
/** /**
* Persistent MMORPG player profile. * Persistent MMORPG character profile (one playable persona per account).
* <p>
* Mutable so gameplay services can update progression in-session; the
* {@code updatedAt} timestamp is refreshed by {@link #touch()} on every change and the whole
* profile is flushed to SQLite on unload / shutdown.
*/ */
public final class PlayerProfile { public final class PlayerProfile {
private final UUID uuid; private final UUID characterId;
private final UUID accountUuid;
private String displayName; private String displayName;
private int level; private int level;
private long experience; private long experience;
@@ -37,16 +34,32 @@ public final class PlayerProfile {
private final long dateCreation; private final long dateCreation;
private long updatedAt; private long updatedAt;
public PlayerProfile(@Nonnull UUID uuid, @Nonnull String displayName, long dateCreation) { public PlayerProfile(
this.uuid = uuid; @Nonnull UUID characterId,
@Nonnull UUID accountUuid,
@Nonnull String displayName,
long dateCreation) {
this.characterId = characterId;
this.accountUuid = accountUuid;
this.displayName = displayName; this.displayName = displayName;
this.dateCreation = dateCreation; this.dateCreation = dateCreation;
this.updatedAt = dateCreation; this.updatedAt = dateCreation;
} }
/** Account UUID (Hytale player id) — used for session lookup. */
@Nonnull @Nonnull
public UUID getUuid() { public UUID getUuid() {
return uuid; return accountUuid;
}
@Nonnull
public UUID getCharacterId() {
return characterId;
}
@Nonnull
public UUID getAccountUuid() {
return accountUuid;
} }
@Nonnull @Nonnull
@@ -168,7 +181,13 @@ public final class PlayerProfile {
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
/** Refreshes {@link #updatedAt} to now; call after mutating any field. */ /** Copies account-level session fields onto this character view. */
public void applyAccountSession(@Nonnull PlayerAccount account) {
setConnected(account.isConnected());
setTotalTimePlay(account.getTotalTimePlay());
setLastDateConnected(account.getLastDateConnected());
}
public void touch() { public void touch() {
this.updatedAt = System.currentTimeMillis(); this.updatedAt = System.currentTimeMillis();
} }
@@ -0,0 +1,247 @@
package com.disklexar.mmorpg.ui;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.EventData;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Mandatory character selector shown on join before gameplay begins.
* <p>
* Character rows are predefined in {@code MmorpgCharacterSelect.ui} — {@code appendInline}
* only supports simple widgets (e.g. {@code Label}), not nested {@code Group}/{@code TextButton}.
*/
public final class CharacterSelectPage extends InteractiveCustomUIPage<CharacterSelectPage.UiEvent> {
private static final String DOCUMENT = "Pages/MmorpgCharacterSelect.ui";
private static final int SLOT_COUNT = 5;
private static final Logger LOGGER = Logger.getLogger(CharacterSelectPage.class.getName());
private final PlayerRef playerRef;
private final UUID accountUuid;
private final CharacterService characters;
private final int maxCharacters;
public CharacterSelectPage(
@Nonnull PlayerRef playerRef,
@Nonnull UUID accountUuid,
@Nonnull CharacterService characters,
int maxCharacters) {
super(playerRef, CustomPageLifetime.CantClose, UiEvent.CODEC);
this.playerRef = playerRef;
this.accountUuid = accountUuid;
this.characters = characters;
this.maxCharacters = Math.min(maxCharacters, SLOT_COUNT);
}
@Override
public void build(
@Nonnull Ref<EntityStore> ref,
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> store) {
ui.append(DOCUMENT);
ui.set("#Title.Text", escape("Sélection du personnage"));
ui.set("#Subtitle.Text", escape("Compte : " + playerRef.getUsername()));
bindActions(events);
renderCharacterList(ui, events, loadCharacters());
}
@Override
public void handleDataEvent(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull String raw) {
ParsedEvent event = parseEvent(raw);
if (event == null) {
return;
}
if ("refresh".equals(event.action())) {
refresh(ref, store);
return;
}
if ("create".equals(event.action())) {
handleCreate(ref, store);
return;
}
if ("select".equals(event.action()) && event.characterId() != null) {
handleSelect(ref, store, event.characterId());
}
}
private void handleCreate(@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store) {
try {
List<PlayerProfile> existing = characters.listCharacters(accountUuid);
if (existing.size() >= maxCharacters) {
setStatus(ref, store, "Limite de " + maxCharacters + " personnages atteinte.");
return;
}
String name = characters.nextDefaultName(accountUuid);
CharacterService.CharacterCreationResult result = characters.createCharacter(accountUuid, name);
if (!result.ok()) {
setStatus(ref, store, result.errorMessage() != null ? result.errorMessage() : "Creation impossible.");
return;
}
refresh(ref, store);
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "Character creation failed", e);
setStatus(ref, store, "Erreur lors de la creation du personnage.");
}
}
private void handleSelect(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID characterId) {
try {
characters.activateCharacter(playerRef, ref, store, accountUuid, characterId);
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "Character selection failed", e);
setStatus(ref, store, "Impossible de charger ce personnage.");
}
}
private void refresh(@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store) {
UICommandBuilder ui = new UICommandBuilder();
UIEventBuilder events = new UIEventBuilder();
bindActions(events);
renderCharacterList(ui, events, loadCharacters());
sendUpdate(ui, events, false);
}
private void setStatus(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull String message) {
UICommandBuilder ui = new UICommandBuilder();
UIEventBuilder events = new UIEventBuilder();
bindActions(events);
renderCharacterList(ui, events, loadCharacters());
ui.set("#FooterHint.Text", escape(message));
sendUpdate(ui, events, false);
}
@Nonnull
private List<PlayerProfile> loadCharacters() {
try {
return characters.listCharacters(accountUuid);
} catch (SQLException e) {
LOGGER.log(Level.WARNING, "Failed to list characters", e);
return List.of();
}
}
private void renderCharacterList(
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull List<PlayerProfile> profiles) {
for (int slot = 0; slot < SLOT_COUNT; slot++) {
ui.set("#CharRow" + slot + ".Visible", false);
}
if (profiles.isEmpty()) {
ui.set("#EmptyLabel.Visible", true);
ui.set("#FooterHint.Text", escape("Cliquez sur « Nouveau personnage »"));
return;
}
ui.set("#EmptyLabel.Visible", false);
ui.set("#FooterHint.Text", escape(profiles.size() + " / " + maxCharacters + " personnages"));
int visible = Math.min(profiles.size(), maxCharacters);
for (int slot = 0; slot < visible; slot++) {
PlayerProfile profile = profiles.get(slot);
ui.set("#CharRow" + slot + ".Visible", true);
ui.set("#CharName" + slot + ".Text", escape(formatSummary(profile)));
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#CharSelect" + slot,
EventData.of("Action", "select")
.append("CharacterId", profile.getCharacterId().toString()),
false);
}
}
private void bindActions(@Nonnull UIEventBuilder events) {
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#CreateCharacterButton",
EventData.of("Action", "create"),
false);
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#RefreshButton",
EventData.of("Action", "refresh"),
false);
}
@Nonnull
private static String formatSummary(@Nonnull PlayerProfile profile) {
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
String className = playerClass == null ? "Sans classe" : playerClass.displayName();
String raceName = RaceCatalog.byId(profile.getRaceId()).displayName();
return profile.getDisplayName()
+ " - Nv." + profile.getLevel()
+ " | " + raceName
+ " | " + className;
}
@Nullable
private static ParsedEvent parseEvent(@Nonnull String raw) {
try {
JsonObject json = JsonParser.parseString(raw).getAsJsonObject();
String action = json.has("Action") ? json.get("Action").getAsString().toLowerCase() : null;
if (action == null) {
return null;
}
UUID characterId = null;
if (json.has("CharacterId")) {
characterId = UUID.fromString(json.get("CharacterId").getAsString());
}
return new ParsedEvent(action, characterId);
} catch (RuntimeException ignored) {
return null;
}
}
@Nonnull
private static String escape(@Nonnull String text) {
return text.replace("\\", "\\\\").replace("\"", "\\\"");
}
private record ParsedEvent(@Nonnull String action, @Nullable UUID characterId) {
}
public static final class UiEvent {
public static final BuilderCodec<UiEvent> CODEC =
BuilderCodec.builder(UiEvent.class, UiEvent::new).build();
private UiEvent() {
}
}
}
@@ -0,0 +1,153 @@
@MmorpgBg = #0d0f14(0.98);
@MmorpgPanel = #1a1c20;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
Group {
LayoutMode: Center;
Group #CharacterSelectRoot {
LayoutMode: Top;
Anchor: (Width: 640, Height: 520);
Background: @MmorpgBg;
Padding: (Full: 20);
Label #Title {
Style: (FontSize: 22, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center);
Text: "Selection du personnage";
Anchor: (Bottom: 6);
}
Label #Subtitle {
Style: (FontSize: 14, TextColor: @MmorpgText, HorizontalAlignment: Center);
Text: "Choisissez un personnage pour entrer en jeu";
Anchor: (Bottom: 14);
}
Group #CharacterListHost {
LayoutMode: Top;
FlexWeight: 1;
Background: @MmorpgPanel;
Padding: (Full: 12);
Label #EmptyLabel {
Style: (FontSize: 14, TextColor: @MmorpgMuted, HorizontalAlignment: Center, Wrap: true);
Text: "Aucun personnage — creez-en un pour commencer.";
Visible: false;
}
Group #CharRow0 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName0 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 1";
FlexWeight: 1;
}
TextButton #CharSelect0 {
Text: "Jouer";
Anchor: (Width: 100);
}
}
Group #CharRow1 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName1 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 2";
FlexWeight: 1;
}
TextButton #CharSelect1 {
Text: "Jouer";
Anchor: (Width: 100);
}
}
Group #CharRow2 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName2 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 3";
FlexWeight: 1;
}
TextButton #CharSelect2 {
Text: "Jouer";
Anchor: (Width: 100);
}
}
Group #CharRow3 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName3 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 4";
FlexWeight: 1;
}
TextButton #CharSelect3 {
Text: "Jouer";
Anchor: (Width: 100);
}
}
Group #CharRow4 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName4 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 5";
FlexWeight: 1;
}
TextButton #CharSelect4 {
Text: "Jouer";
Anchor: (Width: 100);
}
}
}
Group #Actions {
LayoutMode: Left;
Anchor: (Top: 12);
TextButton #CreateCharacterButton {
Text: "Nouveau personnage";
FlexWeight: 1;
Anchor: (Right: 8);
}
TextButton #RefreshButton {
Text: "Actualiser";
FlexWeight: 1;
}
}
Label #FooterHint {
Style: (FontSize: 11, TextColor: @MmorpgMuted, HorizontalAlignment: Center);
Text: "Vous devez selectionner un personnage pour jouer";
Anchor: (Top: 10);
}
}
}
@@ -4,6 +4,7 @@
"MaxPlayers": 500, "MaxPlayers": 500,
"BaseXpPerLevel": 100, "BaseXpPerLevel": 100,
"KillExperience": 10, "KillExperience": 10,
"MaxCharactersPerAccount": 5,
"Database": { "Database": {
"FileName": "mmorpg.db", "FileName": "mmorpg.db",
"UseMonorepoRoot": true "UseMonorepoRoot": true
@@ -0,0 +1,67 @@
-- Multi-character support: characters + per-character inventory.
-- player_profiles becomes the account row, RPG data moves to characters.
ALTER TABLE player_profiles ADD COLUMN active_character_id TEXT;
CREATE TABLE IF NOT EXISTS characters (
id TEXT PRIMARY KEY,
account_uuid TEXT NOT NULL,
name TEXT NOT NULL,
level INTEGER NOT NULL DEFAULT 1,
experience INTEGER NOT NULL DEFAULT 0,
class_id TEXT,
powers TEXT NOT NULL DEFAULT '[]',
jobs TEXT NOT NULL DEFAULT '[]',
guild_id TEXT,
group_id TEXT,
money INTEGER NOT NULL DEFAULT 0,
race_id TEXT NOT NULL DEFAULT 'human',
date_creation INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (account_uuid) REFERENCES player_profiles(uuid) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_characters_account ON characters(account_uuid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_characters_account_name ON characters(account_uuid, name COLLATE NOCASE);
CREATE TABLE IF NOT EXISTS character_inventory (
character_id TEXT NOT NULL,
section_id INTEGER NOT NULL,
slot_index INTEGER NOT NULL,
item_id TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
durability REAL,
max_durability REAL,
metadata TEXT,
PRIMARY KEY (character_id, section_id, slot_index),
FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE
);
-- Migrate existing single-profile players: account uuid becomes the first character id.
INSERT INTO characters (
id, account_uuid, name, level, experience, class_id, powers, jobs,
guild_id, group_id, money, race_id, date_creation, updated_at
)
SELECT
uuid,
uuid,
display_name,
level,
experience,
class_id,
powers,
jobs,
guild_id,
group_id,
money,
race_id,
COALESCE(NULLIF(date_creation, 0), created_at),
updated_at
FROM player_profiles
WHERE NOT EXISTS (
SELECT 1 FROM characters WHERE characters.id = player_profiles.uuid
);
UPDATE player_profiles
SET active_character_id = uuid
WHERE active_character_id IS NULL;
+1 -1
View File
@@ -47,7 +47,7 @@ Site accessible sur [http://localhost:3000](http://localhost:3000).
## Connexion joueur ## Connexion joueur
1. Rejoindre le serveur Hytale (création automatique du profil) 1. Rejoindre le serveur Hytale (création automatique du profil)
2. En jeu : `/mmorpg password MonMotDePasse` (672 caractères) 2. En jeu : `/mmorpg password MonMotDePasse` (372 caractères)
3. Sur le site : **Mon profil** → pseudo + mot de passe 3. Sur le site : **Mon profil** → pseudo + mot de passe
Pour modifier le mot de passe, réutilisez la commande en jeu. Pour modifier le mot de passe, réutilisez la commande en jeu.
+609 -1
View File
@@ -8,15 +8,43 @@
"name": "disklexar-web", "name": "disklexar-web",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.21.2" "express": "^4.21.2",
"express-session": "^1.18.1"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -30,6 +58,70 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aproba": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -68,6 +160,20 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/better-sqlite3": { "node_modules/better-sqlite3": {
"version": "11.10.0", "version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
@@ -200,6 +306,27 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -269,6 +396,12 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -344,6 +477,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -462,6 +601,29 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-session": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz",
"integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==",
"license": "MIT",
"dependencies": {
"cookie": "~0.7.2",
"cookie-signature": "~1.0.7",
"debug": "~2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.1.0",
"parseurl": "~1.3.3",
"safe-buffer": "~5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/file-uri-to-path": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -519,6 +681,36 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -528,6 +720,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -571,6 +784,49 @@
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -595,6 +851,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -627,6 +889,42 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -659,6 +957,17 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -680,6 +989,15 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jake": { "node_modules/jake": {
"version": "10.9.4", "version": "10.9.4",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
@@ -697,6 +1015,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -799,6 +1141,52 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mkdirp-classic": { "node_modules/mkdirp-classic": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -838,6 +1226,69 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -862,6 +1313,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -880,6 +1340,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
@@ -957,6 +1426,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1010,6 +1488,22 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1093,6 +1587,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -1171,6 +1671,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/simple-concat": { "node_modules/simple-concat": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
@@ -1234,6 +1740,32 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@@ -1243,6 +1775,24 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar-fs": { "node_modules/tar-fs": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
@@ -1271,6 +1821,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tar/node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1280,6 +1839,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tunnel-agent": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -1305,6 +1870,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"license": "MIT",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1338,11 +1915,42 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
const bcrypt = require("bcrypt"); const bcrypt = require("bcrypt");
const MIN_PASSWORD_LENGTH = 6; const MIN_PASSWORD_LENGTH = 3;
const MAX_PASSWORD_LENGTH = 72; const MAX_PASSWORD_LENGTH = 72;
async function verifyPassword(plainPassword, passwordHash) { async function verifyPassword(plainPassword, passwordHash) {
+103 -1
View File
@@ -43,6 +43,17 @@ function queryOne(sql, params = []) {
return connection.prepare(sql).get(...params) ?? null; return connection.prepare(sql).get(...params) ?? null;
} }
function hasCharactersTable() {
const connection = getDb();
if (!connection) {
return false;
}
const row = queryOne(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'characters'",
);
return Boolean(row);
}
function getDashboardStats() { function getDashboardStats() {
const connection = getDb(); const connection = getDb();
if (!connection) { if (!connection) {
@@ -55,7 +66,16 @@ function getDashboardStats() {
}; };
} }
const row = queryOne(` const usesCharacters = hasCharactersTable();
const row = usesCharacters
? queryOne(`
SELECT
(SELECT COUNT(*) FROM player_profiles) AS totalPlayers,
(SELECT COUNT(*) FROM player_profiles WHERE is_connected = 1) AS onlinePlayers,
COALESCE((SELECT SUM(money) FROM characters), 0) AS totalMoney,
COALESCE((SELECT AVG(level) FROM characters), 0) AS averageLevel
`)
: queryOne(`
SELECT SELECT
COUNT(*) AS totalPlayers, COUNT(*) AS totalPlayers,
COALESCE(SUM(CASE WHEN is_connected = 1 THEN 1 ELSE 0 END), 0) AS onlinePlayers, COALESCE(SUM(CASE WHEN is_connected = 1 THEN 1 ELSE 0 END), 0) AS onlinePlayers,
@@ -76,6 +96,22 @@ function getDashboardStats() {
} }
function getOnlinePlayers() { function getOnlinePlayers() {
if (hasCharactersTable()) {
return queryAll(`
SELECT
p.uuid,
COALESCE(c.name, p.display_name) AS display_name,
COALESCE(c.level, 1) AS level,
c.class_id,
c.race_id,
COALESCE(c.money, 0) AS money,
p.last_date_connected
FROM player_profiles p
LEFT JOIN characters c ON c.id = p.active_character_id
WHERE p.is_connected = 1
ORDER BY display_name COLLATE NOCASE ASC
`);
}
return queryAll(` return queryAll(`
SELECT uuid, display_name, level, class_id, race_id, money, last_date_connected SELECT uuid, display_name, level, class_id, race_id, money, last_date_connected
FROM player_profiles FROM player_profiles
@@ -85,6 +121,23 @@ function getOnlinePlayers() {
} }
function getRecentPlayers(limit = 8) { function getRecentPlayers(limit = 8) {
if (hasCharactersTable()) {
return queryAll(
`
SELECT
p.uuid,
COALESCE(c.name, p.display_name) AS display_name,
COALESCE(c.level, 1) AS level,
p.is_connected,
p.last_date_connected
FROM player_profiles p
LEFT JOIN characters c ON c.id = p.active_character_id
ORDER BY p.last_date_connected DESC
LIMIT ?
`,
[limit],
);
}
return queryAll( return queryAll(
` `
SELECT uuid, display_name, level, is_connected, last_date_connected SELECT uuid, display_name, level, is_connected, last_date_connected
@@ -97,6 +150,26 @@ function getRecentPlayers(limit = 8) {
} }
function getAllPlayers() { function getAllPlayers() {
if (hasCharactersTable()) {
return queryAll(`
SELECT
c.id AS character_id,
p.uuid,
c.name AS display_name,
c.level,
c.experience,
c.class_id,
c.race_id,
c.money,
p.is_connected,
p.total_time_play,
p.last_date_connected,
c.date_creation
FROM characters c
JOIN player_profiles p ON p.uuid = c.account_uuid
ORDER BY c.name COLLATE NOCASE ASC
`);
}
return queryAll(` return queryAll(`
SELECT SELECT
uuid, uuid,
@@ -116,6 +189,35 @@ function getAllPlayers() {
} }
function getPlayerByUuid(uuid) { function getPlayerByUuid(uuid) {
if (hasCharactersTable()) {
return queryOne(
`
SELECT
p.uuid,
COALESCE(c.name, p.display_name) AS display_name,
COALESCE(c.level, 1) AS level,
COALESCE(c.experience, 0) AS experience,
c.class_id,
c.powers,
c.jobs,
c.guild_id,
c.group_id,
p.is_connected,
p.total_time_play,
p.last_date_connected,
COALESCE(c.date_creation, p.date_creation) AS date_creation,
COALESCE(c.money, 0) AS money,
COALESCE(c.race_id, 'human') AS race_id,
p.created_at,
COALESCE(c.updated_at, p.updated_at) AS updated_at,
c.id AS character_id
FROM player_profiles p
LEFT JOIN characters c ON c.id = p.active_character_id
WHERE p.uuid = ?
`,
[uuid],
);
}
return queryOne( return queryOne(
` `
SELECT SELECT