@@ -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;
|
||||
Reference in New Issue
Block a user