@@ -118,7 +118,11 @@ public final class MmorpgPlugin extends JavaPlugin {
|
||||
OpenCustomUIInteraction.registerSimple(this, PlayerInfoPage.class, "mmorpg_player_info", playerRef -> {
|
||||
PlayerProfile profile = sessions.getSession(playerRef.getUuid());
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -11,8 +11,12 @@ import com.disklexar.mmorpg.combat.StunService;
|
||||
import com.disklexar.mmorpg.core.config.MmorpgConfig;
|
||||
import com.disklexar.mmorpg.economy.EconomyService;
|
||||
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.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.PlayerSessionService;
|
||||
import com.disklexar.mmorpg.progression.ProgressionService;
|
||||
@@ -46,12 +50,12 @@ public final class Bootstrap {
|
||||
public void initialize() throws IOException {
|
||||
config = MmorpgConfig.load(plugin.getDataDirectory());
|
||||
|
||||
// Persistence (DatabaseManager must start first so repositories can query).
|
||||
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);
|
||||
|
||||
// Stateless domain services.
|
||||
ProgressionService progression = new ProgressionService(config.getBaseXpPerLevel());
|
||||
EconomyService economy = new EconomyService();
|
||||
ClassService classService = new ClassService();
|
||||
@@ -63,9 +67,17 @@ public final class Bootstrap {
|
||||
StunService stunService = new StunService();
|
||||
OnlinePlayerRegistry onlinePlayers = new OnlinePlayerRegistry();
|
||||
|
||||
// Session and lifecycle services.
|
||||
PlayerSessionService sessionService =
|
||||
new PlayerSessionService(plugin, config, profileRepository);
|
||||
PlayerSessionService sessionService = new PlayerSessionService(
|
||||
plugin, config, accountRepository, characterRepository);
|
||||
CharacterInventoryService characterInventoryService =
|
||||
new CharacterInventoryService(inventoryRepository);
|
||||
CharacterService characterService = new CharacterService(
|
||||
plugin,
|
||||
config,
|
||||
characterRepository,
|
||||
accountRepository,
|
||||
characterInventoryService,
|
||||
sessionService);
|
||||
MmorpgScheduler scheduler = new MmorpgScheduler(plugin);
|
||||
PoisonService poisonService = new PoisonService(scheduler);
|
||||
GroupService groupService =
|
||||
@@ -79,7 +91,9 @@ public final class Bootstrap {
|
||||
|
||||
registry.register(MmorpgConfig.class, config);
|
||||
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(ProgressionService.class, progression);
|
||||
registry.register(EconomyService.class, economy);
|
||||
@@ -93,6 +107,8 @@ public final class Bootstrap {
|
||||
registry.register(StunService.class, stunService);
|
||||
registry.register(OnlinePlayerRegistry.class, onlinePlayers);
|
||||
registry.register(PlayerSessionService.class, sessionService);
|
||||
registry.register(CharacterInventoryService.class, characterInventoryService);
|
||||
registry.register(CharacterService.class, characterService);
|
||||
registry.register(MmorpgScheduler.class, scheduler);
|
||||
registry.register(GroupService.class, groupService);
|
||||
registry.register(AbilityService.class, abilityService);
|
||||
|
||||
+39
-14
@@ -11,9 +11,13 @@ import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.protocol.MovementSettings;
|
||||
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 javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
/**
|
||||
@@ -71,22 +75,43 @@ public final class PassiveEffectService implements Service {
|
||||
}
|
||||
|
||||
private void applySpeed(@Nonnull OnlinePlayer player, boolean buffed) {
|
||||
Store<EntityStore> store = player.world().getEntityStore().getStore();
|
||||
Ref<EntityStore> ref = player.entityRef();
|
||||
MovementManager movement = store.getComponent(ref, MovementManager.getComponentType());
|
||||
if (movement == null) {
|
||||
Ref<EntityStore> ref = resolveEntityRef(player.world(), player.uuid());
|
||||
if (ref == null) {
|
||||
return;
|
||||
}
|
||||
MovementSettings defaults = movement.getDefaultSettings();
|
||||
MovementSettings current = movement.getSettings();
|
||||
if (defaults == null || current == null) {
|
||||
return;
|
||||
}
|
||||
float baseline = defaults.baseSpeed;
|
||||
float target = buffed ? baseline * (float) (1.0 + PowerCatalog.SPRINTER_SPEED_BONUS) : baseline;
|
||||
if (Math.abs(current.baseSpeed - target) > 0.0001f) {
|
||||
current.baseSpeed = target;
|
||||
movement.update(player.playerRef().getPacketHandler());
|
||||
try {
|
||||
Store<EntityStore> store = player.world().getEntityStore().getStore();
|
||||
MovementManager movement = store.getComponent(ref, MovementManager.getComponentType());
|
||||
if (movement == null) {
|
||||
return;
|
||||
}
|
||||
MovementSettings defaults = movement.getDefaultSettings();
|
||||
MovementSettings current = movement.getSettings();
|
||||
if (defaults == null || current == null) {
|
||||
return;
|
||||
}
|
||||
float baseline = defaults.baseSpeed;
|
||||
float target = buffed ? baseline * (float) (1.0 + PowerCatalog.SPRINTER_SPEED_BONUS) : baseline;
|
||||
if (Math.abs(current.baseSpeed - target) > 0.0001f) {
|
||||
current.baseSpeed = target;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+227
@@ -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 JobCommand(plugin));
|
||||
addSubCommand(new GroupCommand(plugin));
|
||||
addSubCommand(new CharacterCommand(plugin));
|
||||
addSubCommand(new AbilityCommand(plugin));
|
||||
addSubCommand(new PasswordCommand(plugin));
|
||||
}
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ public final class MmorpgHelpCommand extends AbstractAsyncCommand {
|
||||
/mmorpg power <recevoir|retirer|info> [pouvoir]
|
||||
/mmorpg job <rejoindre|quitter|info> [metier]
|
||||
/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 password <mdp> — mot de passe pour le site web Orion.ovh
|
||||
(ou utilisez directement les touches « Use ability 1/2/3 » en jeu)
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ package com.disklexar.mmorpg.command;
|
||||
|
||||
import com.disklexar.mmorpg.MmorpgPlugin;
|
||||
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.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
@@ -44,7 +44,7 @@ public final class PasswordCommand extends AbstractPlayerCommand {
|
||||
@Nonnull PlayerRef playerRef,
|
||||
@Nonnull World world) {
|
||||
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) {
|
||||
context.sendMessage(Message.raw("Profil indisponible."));
|
||||
return;
|
||||
|
||||
@@ -26,6 +26,8 @@ public final class MmorpgConfig {
|
||||
private int baseXpPerLevel = 100;
|
||||
@SerializedName("KillExperience")
|
||||
private int killExperience = 10;
|
||||
@SerializedName("MaxCharactersPerAccount")
|
||||
private int maxCharactersPerAccount = 5;
|
||||
private DatabaseConfig database = new DatabaseConfig();
|
||||
private FeaturesConfig features = new FeaturesConfig();
|
||||
|
||||
@@ -75,6 +77,10 @@ public final class MmorpgConfig {
|
||||
return killExperience;
|
||||
}
|
||||
|
||||
public int getMaxCharactersPerAccount() {
|
||||
return maxCharactersPerAccount;
|
||||
}
|
||||
|
||||
public DatabaseConfig getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
+56
-30
@@ -2,21 +2,21 @@ package com.disklexar.mmorpg.events;
|
||||
|
||||
import com.disklexar.mmorpg.MmorpgPlugin;
|
||||
import com.disklexar.mmorpg.bootstrap.Bootstrap;
|
||||
import com.disklexar.mmorpg.player.OnlinePlayer;
|
||||
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.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.CharacterSelectPage;
|
||||
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.server.core.entity.UUIDComponent;
|
||||
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.PlayerReadyEvent;
|
||||
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;
|
||||
|
||||
@@ -50,41 +50,28 @@ public final class PlayerConnectionHandler {
|
||||
Store<EntityStore> store = player.getWorld().getEntityStore().getStore();
|
||||
PlayerRef universePlayerRef = store.getComponent(entityRef, PlayerRef.getComponentType());
|
||||
PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class);
|
||||
CharacterService characters = bootstrap.getRegistry().require(CharacterService.class);
|
||||
MmorpgScheduler scheduler = bootstrap.getRegistry().require(MmorpgScheduler.class);
|
||||
|
||||
try {
|
||||
UUID uuid = resolveUuid(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) {
|
||||
OnlinePlayerRegistry onlinePlayers =
|
||||
bootstrap.getRegistry().require(OnlinePlayerRegistry.class);
|
||||
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);
|
||||
}
|
||||
scheduler.runLater(() -> openCharacterSelector(
|
||||
player, entityRef, store, universePlayerRef, characters, uuid), 500L);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.at(Level.SEVERE).log(
|
||||
"Failed to load profile for %s: %s",
|
||||
"Failed to load account for %s: %s",
|
||||
resolveDisplayName(store, entityRef, player),
|
||||
e.getMessage());
|
||||
if (universePlayerRef != null) {
|
||||
universePlayerRef.sendMessage(Message.raw(
|
||||
"Erreur lors du chargement de votre profil MMORPG."));
|
||||
universePlayerRef.sendMessage(com.hypixel.hytale.server.core.Message.raw(
|
||||
"Erreur lors du chargement de votre compte MMORPG."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,11 +83,50 @@ public final class PlayerConnectionHandler {
|
||||
}
|
||||
|
||||
PlayerRef playerRef = event.getPlayerRef();
|
||||
UUID accountUuid = playerRef.getUuid();
|
||||
OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class);
|
||||
onlinePlayers.remove(playerRef.getUuid());
|
||||
bootstrap.getRegistry().require(AbilityBarService.class).dismiss(playerRef.getUuid());
|
||||
AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
|
||||
CharacterService characters = bootstrap.getRegistry().require(CharacterService.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
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import javax.annotation.Nonnull;
|
||||
public final class PasswordHasher {
|
||||
|
||||
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;
|
||||
|
||||
private PasswordHasher() {
|
||||
|
||||
+117
@@ -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();
|
||||
}
|
||||
}
|
||||
+169
@@ -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();
|
||||
}
|
||||
}
|
||||
+166
@@ -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();
|
||||
}
|
||||
}
|
||||
-193
@@ -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();
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -26,7 +26,8 @@ public final class SchemaInitializer {
|
||||
private static final List<String> MIGRATIONS = List.of(
|
||||
"001_init.sql",
|
||||
"002_rpg.sql",
|
||||
"003_web_password.sql");
|
||||
"003_web_password.sql",
|
||||
"004_characters.sql");
|
||||
|
||||
private SchemaInitializer() {
|
||||
}
|
||||
@@ -73,10 +74,10 @@ public final class SchemaInitializer {
|
||||
}
|
||||
|
||||
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()) {
|
||||
for (String chunk : sql.split(";")) {
|
||||
String trimmed = stripComments(chunk).trim();
|
||||
String trimmed = chunk.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
statement.execute(trimmed);
|
||||
}
|
||||
@@ -88,8 +89,11 @@ public final class SchemaInitializer {
|
||||
private static String stripComments(@Nonnull String sql) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String line : sql.split("\n")) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.startsWith("--")) {
|
||||
int commentStart = line.indexOf("--");
|
||||
if (commentStart >= 0) {
|
||||
line = line.substring(0, commentStart);
|
||||
}
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
builder.append(line).append('\n');
|
||||
|
||||
+128
@@ -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();
|
||||
}
|
||||
}
|
||||
+272
@@ -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
|
||||
public static List<Entry> entries(@Nonnull PlayerProfile profile) {
|
||||
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("Expérience", String.valueOf(profile.getExperience())));
|
||||
entries.add(new Entry("Argent", profile.getMoney() + " money"));
|
||||
|
||||
+142
-47
@@ -3,9 +3,10 @@ package com.disklexar.mmorpg.player;
|
||||
import com.disklexar.mmorpg.MmorpgPlugin;
|
||||
import com.disklexar.mmorpg.core.config.MmorpgConfig;
|
||||
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.rpg.race.RaceCatalog;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
@@ -18,31 +19,50 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
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 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 MmorpgConfig config;
|
||||
private final PlayerProfileRepository repository;
|
||||
private final PlayerAccountRepository accountRepository;
|
||||
private final CharacterRepository characterRepository;
|
||||
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<>();
|
||||
|
||||
public PlayerSessionService(
|
||||
@Nonnull MmorpgPlugin plugin,
|
||||
@Nonnull MmorpgConfig config,
|
||||
@Nonnull PlayerProfileRepository repository) {
|
||||
@Nonnull PlayerAccountRepository accountRepository,
|
||||
@Nonnull CharacterRepository characterRepository) {
|
||||
this.plugin = plugin;
|
||||
this.config = config;
|
||||
this.repository = repository;
|
||||
this.accountRepository = accountRepository;
|
||||
this.characterRepository = characterRepository;
|
||||
this.logger = plugin.getLogger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
try {
|
||||
repository.clearConnectedFlags();
|
||||
accountRepository.clearConnectedFlags();
|
||||
} catch (SQLException e) {
|
||||
logger.at(Level.WARNING).log("Failed to reset connection flags: %s", e.getMessage());
|
||||
}
|
||||
@@ -51,10 +71,14 @@ public final class PlayerSessionService implements Service {
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
for (PlayerProfile profile : sessions.values()) {
|
||||
accumulatePlaytime(profile.getUuid(), profile);
|
||||
profile.setConnected(false);
|
||||
saveQuietly(profile);
|
||||
for (AccountSession session : sessions.values()) {
|
||||
accumulatePlaytime(session.account().getUuid());
|
||||
PlayerAccount account = session.account();
|
||||
account.setConnected(false);
|
||||
saveAccountQuietly(account);
|
||||
if (session.activeCharacter() != null) {
|
||||
saveCharacterQuietly(session.activeCharacter());
|
||||
}
|
||||
}
|
||||
sessions.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
|
||||
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();
|
||||
Optional<PlayerProfile> existing = repository.findByUuid(uuid);
|
||||
PlayerProfile profile;
|
||||
Optional<PlayerAccount> existing = accountRepository.findByUuid(accountUuid);
|
||||
PlayerAccount account;
|
||||
if (existing.isPresent()) {
|
||||
profile = existing.get();
|
||||
profile.setDisplayName(displayName);
|
||||
account = existing.get();
|
||||
account.setDisplayName(displayName);
|
||||
} else {
|
||||
profile = new PlayerProfile(uuid, displayName, now);
|
||||
profile.setLevel(config.getDefaultLevel());
|
||||
profile.setRaceId(RaceCatalog.DEFAULT_RACE_ID);
|
||||
logger.at(Level.INFO).log("Created new MMORPG profile for %s", displayName);
|
||||
account = new PlayerAccount(accountUuid, displayName, now);
|
||||
logger.at(Level.INFO).log("Created new MMORPG account for %s", displayName);
|
||||
}
|
||||
|
||||
profile.setConnected(true);
|
||||
profile.setLastDateConnected(now);
|
||||
profile.touch();
|
||||
sessions.put(uuid, profile);
|
||||
connectedAt.put(uuid, now);
|
||||
saveQuietly(profile);
|
||||
return profile;
|
||||
account.setConnected(true);
|
||||
account.setLastDateConnected(now);
|
||||
account.touch();
|
||||
|
||||
AccountSession session = new AccountSession(account, null, true);
|
||||
sessions.put(accountUuid, session);
|
||||
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
|
||||
public PlayerProfile getSession(@Nonnull UUID uuid) {
|
||||
return sessions.get(uuid);
|
||||
public PlayerProfile getSession(@Nonnull UUID accountUuid) {
|
||||
AccountSession session = sessions.get(accountUuid);
|
||||
if (session == null || session.awaitingCharacterSelection()) {
|
||||
return null;
|
||||
}
|
||||
return session.activeCharacter();
|
||||
}
|
||||
|
||||
public void unload(@Nonnull UUID uuid) {
|
||||
PlayerProfile profile = sessions.remove(uuid);
|
||||
if (profile == null) {
|
||||
@Nullable
|
||||
public AccountSession getAccountSession(@Nonnull UUID accountUuid) {
|
||||
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;
|
||||
}
|
||||
accumulatePlaytime(uuid, profile);
|
||||
profile.setConnected(false);
|
||||
profile.touch();
|
||||
saveQuietly(profile);
|
||||
}
|
||||
|
||||
private void accumulatePlaytime(@Nonnull UUID uuid, @Nonnull PlayerProfile profile) {
|
||||
Long start = connectedAt.remove(uuid);
|
||||
if (start != null) {
|
||||
profile.addTimePlay(System.currentTimeMillis() - start);
|
||||
accumulatePlaytime(accountUuid);
|
||||
PlayerAccount account = session.account();
|
||||
account.setConnected(false);
|
||||
account.touch();
|
||||
saveAccountQuietly(account);
|
||||
if (session.activeCharacter() != null) {
|
||||
saveCharacterQuietly(session.activeCharacter());
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
repository.save(profile);
|
||||
accountRepository.save(account);
|
||||
} catch (SQLException e) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+95
@@ -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();
|
||||
}
|
||||
}
|
||||
+29
-10
@@ -7,15 +7,12 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Persistent MMORPG player profile.
|
||||
* <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.
|
||||
* Persistent MMORPG character profile (one playable persona per account).
|
||||
*/
|
||||
public final class PlayerProfile {
|
||||
|
||||
private final UUID uuid;
|
||||
private final UUID characterId;
|
||||
private final UUID accountUuid;
|
||||
private String displayName;
|
||||
private int level;
|
||||
private long experience;
|
||||
@@ -37,16 +34,32 @@ public final class PlayerProfile {
|
||||
private final long dateCreation;
|
||||
private long updatedAt;
|
||||
|
||||
public PlayerProfile(@Nonnull UUID uuid, @Nonnull String displayName, long dateCreation) {
|
||||
this.uuid = uuid;
|
||||
public PlayerProfile(
|
||||
@Nonnull UUID characterId,
|
||||
@Nonnull UUID accountUuid,
|
||||
@Nonnull String displayName,
|
||||
long dateCreation) {
|
||||
this.characterId = characterId;
|
||||
this.accountUuid = accountUuid;
|
||||
this.displayName = displayName;
|
||||
this.dateCreation = dateCreation;
|
||||
this.updatedAt = dateCreation;
|
||||
}
|
||||
|
||||
/** Account UUID (Hytale player id) — used for session lookup. */
|
||||
@Nonnull
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
return accountUuid;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public UUID getCharacterId() {
|
||||
return characterId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public UUID getAccountUuid() {
|
||||
return accountUuid;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@@ -168,7 +181,13 @@ public final class PlayerProfile {
|
||||
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() {
|
||||
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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
+153
@@ -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,
|
||||
"BaseXpPerLevel": 100,
|
||||
"KillExperience": 10,
|
||||
"MaxCharactersPerAccount": 5,
|
||||
"Database": {
|
||||
"FileName": "mmorpg.db",
|
||||
"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
@@ -47,7 +47,7 @@ Site accessible sur [http://localhost:3000](http://localhost:3000).
|
||||
## Connexion joueur
|
||||
|
||||
1. Rejoindre le serveur Hytale (création automatique du profil)
|
||||
2. En jeu : `/mmorpg password MonMotDePasse` (6–72 caractères)
|
||||
2. En jeu : `/mmorpg password MonMotDePasse` (3–72 caractères)
|
||||
3. Sur le site : **Mon profil** → pseudo + mot de passe
|
||||
|
||||
Pour modifier le mot de passe, réutilisez la commande en jeu.
|
||||
|
||||
Generated
+609
-1
@@ -8,15 +8,43 @@
|
||||
"name": "disklexar-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.21.2"
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -30,6 +58,70 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -68,6 +160,20 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
@@ -200,6 +306,27 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"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": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -269,6 +396,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -344,6 +477,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -462,6 +601,29 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -528,6 +720,27 @@
|
||||
"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": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -571,6 +784,49 @@
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -595,6 +851,12 @@
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
@@ -627,6 +889,42 @@
|
||||
"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": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -659,6 +957,17 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -680,6 +989,15 @@
|
||||
"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": {
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
@@ -697,6 +1015,30 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -799,6 +1141,52 @@
|
||||
"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": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
@@ -838,6 +1226,69 @@
|
||||
"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": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -862,6 +1313,15 @@
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -880,6 +1340,15 @@
|
||||
"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": {
|
||||
"version": "0.1.13",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -1010,6 +1488,22 @@
|
||||
"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": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1093,6 +1587,12 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -1171,6 +1671,12 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
@@ -1234,6 +1740,32 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
@@ -1243,6 +1775,24 @@
|
||||
"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": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
@@ -1271,6 +1821,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1280,6 +1839,12 @@
|
||||
"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": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -1305,6 +1870,18 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -1338,11 +1915,42 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"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
@@ -1,6 +1,6 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
const MIN_PASSWORD_LENGTH = 3;
|
||||
const MAX_PASSWORD_LENGTH = 72;
|
||||
|
||||
async function verifyPassword(plainPassword, passwordHash) {
|
||||
|
||||
+110
-8
@@ -43,6 +43,17 @@ function queryOne(sql, params = []) {
|
||||
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() {
|
||||
const connection = getDb();
|
||||
if (!connection) {
|
||||
@@ -55,14 +66,23 @@ function getDashboardStats() {
|
||||
};
|
||||
}
|
||||
|
||||
const row = queryOne(`
|
||||
SELECT
|
||||
COUNT(*) AS totalPlayers,
|
||||
COALESCE(SUM(CASE WHEN is_connected = 1 THEN 1 ELSE 0 END), 0) AS onlinePlayers,
|
||||
COALESCE(SUM(money), 0) AS totalMoney,
|
||||
COALESCE(AVG(level), 0) AS averageLevel
|
||||
FROM player_profiles
|
||||
`);
|
||||
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
|
||||
COUNT(*) AS totalPlayers,
|
||||
COALESCE(SUM(CASE WHEN is_connected = 1 THEN 1 ELSE 0 END), 0) AS onlinePlayers,
|
||||
COALESCE(SUM(money), 0) AS totalMoney,
|
||||
COALESCE(AVG(level), 0) AS averageLevel
|
||||
FROM player_profiles
|
||||
`);
|
||||
|
||||
const groups = queryOne(`SELECT COUNT(*) AS count FROM groups`);
|
||||
|
||||
@@ -76,6 +96,22 @@ function getDashboardStats() {
|
||||
}
|
||||
|
||||
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(`
|
||||
SELECT uuid, display_name, level, class_id, race_id, money, last_date_connected
|
||||
FROM player_profiles
|
||||
@@ -85,6 +121,23 @@ function getOnlinePlayers() {
|
||||
}
|
||||
|
||||
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(
|
||||
`
|
||||
SELECT uuid, display_name, level, is_connected, last_date_connected
|
||||
@@ -97,6 +150,26 @@ function getRecentPlayers(limit = 8) {
|
||||
}
|
||||
|
||||
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(`
|
||||
SELECT
|
||||
uuid,
|
||||
@@ -116,6 +189,35 @@ function getAllPlayers() {
|
||||
}
|
||||
|
||||
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(
|
||||
`
|
||||
SELECT
|
||||
|
||||
Reference in New Issue
Block a user