pala M0: socle core (config/permissions/storage/jobs) + /pala /jobs + gate /nether, jar serveur redeploye

This commit is contained in:
Anaïs Saraiva
2026-10-18 23:38:52 +02:00
parent a1c86f8be9
commit 86cc852c81
10 changed files with 492 additions and 3 deletions
+11
View File
@@ -474,6 +474,17 @@ avant M4 (TODO:crafts-machines).
(biomesoplenty/clumps/glitchcore/patpat, message standard "ignorer hors dev").
Serveur arrete apres test (port 25565 libere), eula.txt restaure a false,
server.properties/packetfixer.properties restaures (le boot ne change que leurs timestamps).
- M0 LIVRE 2026-10-18 23:35 (commit a suivre) : package core (PalaConfig JSON rechargeable
22 entrees minerXp wiki, PalaStorage joueurs JSON + flush 5 s, PalaPermissions rangs+noeuds,
PalaJobs moteur XP/niveaux, PalaCore lifecycle), /pala reload|info|rank, /jobs,
/nether gate mineur niv.3, hook XP dans PalaToolEvents. VERIFIE : ./gradlew compileJava+build
SUCCESSFUL, boot serveur Done (0.355s), "Pala core charge (config creee par defaut)",
config/pala.json generee (6 cles), 0 ERROR/Exception au log. Deploys : server/mods/pala-0.2.1.jar.
- Build mod : TOUJOURS via shell racine avec JAVA_HOME redirige (le shell.nix positionne
JAVA_HOME sur un JDK 8, Gradle 9 exige 17+). Commande VERIFIE :
cd mods/pala && nix-shell <repo>/shell.nix --run "export JAVA_HOME=$(dirname $(dirname
$(readlink -f $(which java)))); ./gradlew build". TODO:java-home : fixer shell.nix racine
(export JAVA_HOME jdk25) pour ne plus bricoler. Raison : decouvert en testant (3 essais).
- Client/mod : ./gradlew runClient dans mods/pala/ : NON LANCE (session : pas d'affichage ; screenshots
ocular impossibles ici). TODO:runclient : 1 screenshot/feature dans mods/pala/docs/screens/ avant M10.
Raison : environnement headless, a faire sur poste avec X.
@@ -11,6 +11,7 @@ public class Pala implements ModInitializer {
@Override
public void onInitialize() {
fr.surviepote.pala.core.PalaCore.register();
PalaRegistries.registerAll();
fr.surviepote.pala.events.PalaEvents.registerAll();
LOGGER.info("[SurviePote] Pala initialisé.");
@@ -1,10 +1,13 @@
package fr.surviepote.pala;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -15,7 +18,13 @@ import net.minecraft.world.level.Level;
import net.minecraft.world.level.portal.TeleportTransition;
import net.minecraft.world.phys.Vec3;
// Commandes Pala : /end /nether /overworld (permission niveau 4, legacy).
import fr.surviepote.pala.core.PalaConfig;
import fr.surviepote.pala.core.PalaJobs;
import fr.surviepote.pala.core.PalaPermissions;
import fr.surviepote.pala.core.PalaStorage;
// Commandes Pala : /end /overworld (permission niveau 4, legacy), /nether (gate metier
// mineur niv.3, M0), /pala reload|info|rank (admin), /jobs (metiers). Voir CDC §3.8.
// Origine : CommandEnd/Nether/Overworld + procedures *CommandExecuted qui
// teleportaient le joueur au spawn de la dimension visee. Ici via
// TeleportTransition (API 26.x), sans le Teleporter custom du legacy.
@@ -30,14 +39,96 @@ public final class PalaCommands {
.requires(source -> Commands.LEVEL_OWNERS.check(source.permissions()))
.executes(command -> teleport(command.getSource(), Level.END)));
dispatcher.register(Commands.literal("nether")
.requires(source -> Commands.LEVEL_OWNERS.check(source.permissions()))
.executes(command -> teleport(command.getSource(), Level.NETHER)));
.executes(command -> nether(command.getSource())));
dispatcher.register(Commands.literal("overworld")
.requires(source -> Commands.LEVEL_OWNERS.check(source.permissions()))
.executes(command -> teleport(command.getSource(), Level.OVERWORLD)));
com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> rankArg =
Commands.argument("rang", StringArgumentType.word())
.executes(command -> rank(command.getSource(),
EntityArgument.getPlayer(command, "joueur"),
StringArgumentType.getString(command, "rang")));
com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> joueurArg =
Commands.argument("joueur", EntityArgument.player()).then(rankArg);
dispatcher.register(Commands.literal("pala")
.then(Commands.literal("reload")
.requires(source -> isAdmin(source))
.executes(command -> reload(command.getSource())))
.then(Commands.literal("info")
.requires(source -> isAdmin(source))
.executes(command -> info(command.getSource())))
.then(Commands.literal("rank")
.requires(source -> isAdmin(source))
.then(joueurArg)));
dispatcher.register(Commands.literal("jobs")
.executes(command -> jobs(command.getSource())));
});
}
private static boolean isAdmin(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
return true;
}
return PalaPermissions.has(source, player, "pala.admin");
}
private static int reload(CommandSourceStack source) {
PalaConfig.load();
source.sendSuccess(() -> Component.literal(
"§6[Pala] §7Config rechargee (" + PalaStorage.get().playerCount() + " joueurs connus)."),
false);
return Command.SINGLE_SUCCESS;
}
private static int info(CommandSourceStack source) {
source.sendSuccess(() -> Component.literal("§6[Pala] §7Joueurs connus : "
+ PalaStorage.get().playerCount()), false);
return Command.SINGLE_SUCCESS;
}
private static int rank(CommandSourceStack source, ServerPlayer target, String rank) {
if (!PalaConfig.get().rankNodes.containsKey(rank)) {
source.sendFailure(Component.literal("§cRang inconnu : " + rank));
return 0;
}
PalaPermissions.setRank(target, rank);
source.sendSuccess(() -> Component.literal("§6[Pala] §7" + target.getScoreboardName()
+ " -> rang " + rank + "."), false);
return Command.SINGLE_SUCCESS;
}
private static int jobs(CommandSourceStack source) {
try {
ServerPlayer player = source.getPlayerOrException();
StringBuilder lines = new StringBuilder("§6[Metiers]");
for (String job : PalaJobs.JOBS) {
lines.append("\n§e").append(job).append(" §7niv. §a")
.append(PalaJobs.levelOf(player, job)).append(" §8(")
.append((long) PalaJobs.xpOf(player, job)).append(" xp)");
}
source.sendSuccess(() -> Component.literal(lines.toString()), false);
return Command.SINGLE_SUCCESS;
} catch (com.mojang.brigadier.exceptions.CommandSyntaxException e) {
return 0;
}
}
// /nether : deblocage metier mineur niv.3 (wiki recompenses mineur, VERIFIE), OP bypass.
private static int nether(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player != null) {
int required = PalaConfig.get().netherMinerLevel;
if (!Commands.LEVEL_ADMINS.check(source.permissions())
&& PalaJobs.levelOf(player, "mineur") < required) {
player.sendSystemMessage(Component.literal("§cNether verrouille : mineur niveau "
+ required + " requis (actuel : " + PalaJobs.levelOf(player, "mineur") + ")."));
return 0;
}
}
return teleport(source, Level.NETHER);
}
private static int teleport(CommandSourceStack source, ResourceKey<Level> dimension) {
try {
ServerPlayer player = source.getPlayerOrException();
@@ -0,0 +1,124 @@
package fr.surviepote.pala.core;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.fabricmc.loader.api.FabricLoader;
import fr.surviepote.pala.Pala;
// Config rechargeable du mod (M0). JSON via Gson (fourni par Mojang au runtime).
//
// Choix JSON plutot que YAML en 3 lignes :
// 1. Zero dependance (pas de snakeyaml a embarquer/patcher au build) ;
// 2. Gson est deja au classpath compile (gson-2.14.0.jar) et au runtime Mojang ;
// 3. /pala reload re-parse le fichier sans restart, valeurs par defaut si cles absentes.
// Les valeurs wiki VERIFIEES sont en Maka ; le reste est UNVERIFIED (balance staff, M5).
public final class PalaConfig {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
// XP metier mineur par bloc casse (wiki "Evolution du metier Mineur", VERIFIE).
// Cles = ids de registres (vanilla ou pala:). Map.ofEntries : Map.of limite a 10 paires.
public Map<String, Double> minerXp = new HashMap<>(Map.ofEntries(
Map.entry("minecraft:stone", 0.5),
Map.entry("minecraft:diorite", 3.0), Map.entry("minecraft:andesite", 3.0),
Map.entry("minecraft:coal_ore", 4.0), Map.entry("minecraft:deepslate_coal_ore", 4.0),
Map.entry("minecraft:quartz_ore", 6.0), Map.entry("minecraft:obsidian", 6.0),
Map.entry("minecraft:lapis_ore", 15.0), Map.entry("minecraft:redstone_ore", 15.0),
Map.entry("minecraft:emerald_ore", 50.0),
Map.entry("minecraft:iron_ore", 2.0), Map.entry("minecraft:deepslate_iron_ore", 2.0),
Map.entry("minecraft:gold_ore", 4.0), Map.entry("minecraft:deepslate_gold_ore", 4.0),
Map.entry("minecraft:diamond_ore", 8.0), Map.entry("minecraft:deepslate_diamond_ore", 8.0),
Map.entry("pala:amethyst_ore", 12.0), Map.entry("pala:titane_ore", 16.0),
Map.entry("pala:paladium_ore", 20.0),
Map.entry("pala:paladium_green_ore", 20.0), Map.entry("pala:trixium_ore", 10.0),
Map.entry("pala:findium_ore", 10.0)));
// Courbe XP : cout du niveau n. UNVERIFIED (formule placeholder, a caler M5).
public double xpBase = 100.0;
public double xpExponent = 1.5;
// Niveau mineur requis pour /nether (wiki recompenses mineur niv.3, VERIFIE).
public int netherMinerLevel = 3;
// Cooldowns sticks PvP en secondes. UNVERIFIED (durees balance staff, M5).
public Map<String, Integer> stickCooldowns = new HashMap<>(Map.of(
"heal_stick", 60, "speed_stick", 30, "hyper_jump_stick", 30,
"strenght_stick", 120, "stick_of_god", 600));
// Rangs -> noeuds de permission (extrait page "par grade", partiel UNVERIFIED).
public Map<String, java.util.List<String>> rankNodes = new HashMap<>(Map.of(
"default", java.util.List.of("pala.job", "pala.trade", "pala.market"),
"heros", java.util.List.of("pala.*")));
private static PalaConfig instance = new PalaConfig();
private PalaConfig() {
}
public static PalaConfig get() {
return instance;
}
public static Path file() {
return FabricLoader.getInstance().getConfigDir().resolve("pala.json");
}
/** Charge (ou cree avec defauts). Retourne true si un fichier existant a ete lu. */
public static boolean load() {
Path path = file();
if (!Files.exists(path)) {
save();
return false;
}
try (Reader reader = Files.newBufferedReader(path)) {
PalaConfig loaded = GSON.fromJson(reader, PalaConfig.class);
if (loaded != null) {
if (loaded.minerXp != null && !loaded.minerXp.isEmpty()) {
instance.minerXp = loaded.minerXp;
}
if (loaded.xpBase > 0) {
instance.xpBase = loaded.xpBase;
}
if (loaded.xpExponent > 0) {
instance.xpExponent = loaded.xpExponent;
}
if (loaded.netherMinerLevel >= 0) {
instance.netherMinerLevel = loaded.netherMinerLevel;
}
if (loaded.stickCooldowns != null && !loaded.stickCooldowns.isEmpty()) {
instance.stickCooldowns = loaded.stickCooldowns;
}
if (loaded.rankNodes != null && !loaded.rankNodes.isEmpty()) {
instance.rankNodes = loaded.rankNodes;
}
}
return true;
} catch (IOException | com.google.gson.JsonParseException e) {
Pala.LOGGER.warn("[SurviePote] Config pala.json illisible, defauts gardes : {}", e.toString());
return false;
}
}
public static void save() {
try (Writer writer = Files.newBufferedWriter(file())) {
GSON.toJson(instance, writer);
} catch (IOException e) {
Pala.LOGGER.warn("[SurviePote] Ecriture pala.json impossible : {}", e.toString());
}
}
/** Cout XP total pour atteindre le niveau n (n >= 1). */
public long xpForLevel(int level) {
return (long) (xpBase * Math.pow(level, xpExponent));
}
}
@@ -0,0 +1,37 @@
package fr.surviepote.pala.core;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import fr.surviepote.pala.Pala;
// Cycle de vie du socle M0 : config + storage au boot, flush periodique, save a l'arret.
// Le compteur modulo 100 sur END_SERVER_TICK = ~5 s (20 ticks/s) : zero I/O si pas dirty.
public final class PalaCore {
private static int tickCounter = 0;
private PalaCore() {
}
public static void register() {
boolean hadFile = PalaConfig.load();
PalaStorage.get().load();
Pala.LOGGER.info("[SurviePote] Pala core charge (config {}).",
hadFile ? "existante" : "creee par defaut");
ServerTickEvents.END_SERVER_TICK.register(server -> {
tickCounter++;
if (tickCounter >= 100) {
tickCounter = 0;
PalaStorage.get().saveIfDirty();
}
});
ServerLifecycleEvents.SERVER_STOPPING.register(server -> {
PalaStorage.get().save();
Pala.LOGGER.info("[SurviePote] Pala core sauvegarde ({} joueurs).",
PalaStorage.get().playerCount());
});
}
}
@@ -0,0 +1,64 @@
package fr.surviepote.pala.core;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState;
// Metiers : XP et niveaux (M0 = moteur minimal, tables completes en M5).
// Wiki VERIFIE : 4 metiers (mineur, fermier, chasseur, alchimiste), ~100 niveaux,
// table XP mineur par bloc (PalaConfig.minerXp). Courbe et recompenses : UNVERIFIED (M5).
public final class PalaJobs {
public static final List<String> JOBS = List.of("mineur", "fermier", "chasseur", "alchimiste");
private PalaJobs() {
}
public static int levelOf(ServerPlayer player, String job) {
return PalaStorage.get().data(player.getUUID()).jobs
.getOrDefault(job, new PalaStorage.JobData()).level;
}
public static double xpOf(ServerPlayer player, String job) {
return PalaStorage.get().data(player.getUUID()).jobs
.getOrDefault(job, new PalaStorage.JobData()).xp;
}
/** Ajoute de l'XP, monte les niveaux franchis, annonce. Retourne le niveau final. */
public static int addXp(ServerPlayer player, String job, double amount) {
if (amount <= 0) {
return levelOf(player, job);
}
PalaStorage.JobData data = PalaStorage.get().data(player.getUUID()).jobs
.computeIfAbsent(job, k -> new PalaStorage.JobData());
data.xp += amount;
PalaConfig config = PalaConfig.get();
while (data.level < 100 && data.xp >= config.xpForLevel(data.level + 1)) {
data.level++;
player.sendSystemMessage(Component.literal(
"§6[Metier] §e" + job + " §7niveau §a" + data.level + " §7atteint !"));
// TODO:recompenses-m5 : lots par palier (voidstone niv.2, /nether niv.3...). Raison : M5.
}
PalaStorage.get().markDirty();
return data.level;
}
/** Hook casse-bloc : XP mineur selon la table config. Appele depuis PalaToolEvents. */
public static void onBlockBreak(ServerPlayer player, ServerLevel level, BlockPos pos, BlockState state) {
Identifier id = level.registryAccess()
.lookupOrThrow(net.minecraft.core.registries.Registries.BLOCK)
.getKey(state.getBlock());
if (id == null) {
return;
}
Double xp = PalaConfig.get().minerXp.get(id.toString());
if (xp != null) {
addXp(player, "mineur", xp);
}
}
}
@@ -0,0 +1,47 @@
package fr.surviepote.pala.core;
import java.util.List;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.level.ServerPlayer;
// Permissions internes (M0) : rangs + noeuds pala.*.
//
// Choix systeme interne plutot que LuckPerms en 3 lignes :
// 1. Zero dependance : le serveur actuel n'a pas LuckPerms, M0 doit booter tel quel ;
// 2. Les grades boutique se resolvent en 2 rangs + noeuds explicites, suffisant pour M0-M6 ;
// 3. Si LuckPerms est ajoute plus tard, cette classe devient un adaptateur (1 seul point de contact).
// TODO:luckperms : re-evaluer en M7 (CDC §3.8). Raison : reconciliation avec les grades boutique.
public final class PalaPermissions {
private PalaPermissions() {
}
public static String rankOf(ServerPlayer player) {
return PalaStorage.get().data(player.getUUID()).rank;
}
public static void setRank(ServerPlayer player, String rank) {
PalaStorage.PlayerData data = PalaStorage.get().data(player.getUUID());
data.rank = rank;
PalaStorage.get().markDirty();
}
/** OP niveau 2+ (LEVEL_ADMINS) bypass tout ; sinon noeud exact ou wildcard pala.* du rang.
* Prend la source (PermissionSet vanilla 26.3, verifie au javap) + le joueur (rang interne). */
public static boolean has(CommandSourceStack source, ServerPlayer player, String node) {
if (net.minecraft.commands.Commands.LEVEL_ADMINS.check(source.permissions())) {
return true;
}
List<String> nodes = PalaConfig.get().rankNodes.getOrDefault(rankOf(player), List.of());
if (nodes.contains(node) || nodes.contains("pala.*")) {
return true;
}
// Wildcard partiel : pala.job.* couvre pala.job.mineur, etc.
int dot = node.lastIndexOf('.');
if (dot > 0 && nodes.contains(node.substring(0, dot) + ".*")) {
return true;
}
return false;
}
}
@@ -0,0 +1,110 @@
package fr.surviepote.pala.core;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.fabricmc.loader.api.FabricLoader;
import fr.surviepote.pala.Pala;
// Donnees joueurs persistantes (M0) : jobs XP/niveaux, rang, monnaie.
//
// Choix JSON fichier unique en 3 lignes :
// 1. M0 n'a besoin que de lectures/ecritures simples (pas de jointures) ;
// 2. Flush differe hors tick (dirty + save 5 s) = zero I/O sur le thread tick ;
// 3. SQLite arrive en M7 avec l'economie (listings, historique) — le schema est prevu
// pour migrer tel quel (1 ligne joueur = 1 document JSON aujourd'hui).
// TODO:sqlite : migrer vers pala.db (JDBC) en M7. Raison : requetes market/ELO hors de portee JSON.
public final class PalaStorage {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static final class JobData {
public double xp = 0.0;
public int level = 0;
}
public static final class PlayerData {
public Map<String, JobData> jobs = new HashMap<>();
public String rank = "default";
public double money = 0.0;
}
private final Map<String, PlayerData> players = new HashMap<>();
private boolean dirty = false;
private static final PalaStorage INSTANCE = new PalaStorage();
private PalaStorage() {
}
public static PalaStorage get() {
return INSTANCE;
}
public static Path file() {
return FabricLoader.getInstance().getConfigDir().resolve("pala-players.json");
}
public PlayerData data(UUID uuid) {
return players.computeIfAbsent(uuid.toString(), k -> new PlayerData());
}
public void markDirty() {
dirty = true;
}
public void load() {
Path path = file();
if (!Files.exists(path)) {
return;
}
try (Reader reader = Files.newBufferedReader(path)) {
Map<?, ?> raw = GSON.fromJson(reader, Map.class);
if (raw == null) {
return;
}
for (Map.Entry<?, ?> entry : raw.entrySet()) {
PlayerData data = GSON.fromJson(GSON.toJson(entry.getValue()), PlayerData.class);
if (data != null) {
if (data.jobs == null) {
data.jobs = new HashMap<>();
}
players.put(String.valueOf(entry.getKey()), data);
}
}
} catch (IOException | com.google.gson.JsonParseException e) {
Pala.LOGGER.warn("[SurviePote] pala-players.json illisible, demarrage vide : {}", e.toString());
}
}
public void saveIfDirty() {
if (!dirty) {
return;
}
dirty = false;
save();
}
public void save() {
try (Writer writer = Files.newBufferedWriter(file())) {
GSON.toJson(players, writer);
} catch (IOException e) {
Pala.LOGGER.warn("[SurviePote] Ecriture pala-players.json impossible : {}", e.toString());
}
}
/** Nombre de joueurs connus (debug /pala). */
public int playerCount() {
return players.size();
}
}
@@ -25,6 +25,7 @@ import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import fr.surviepote.pala.PalaBlocks;
import fr.surviepote.pala.PalaItems;
import fr.surviepote.pala.core.PalaJobs;
// Portage des procedures "bloc detruit avec l'outil / par le joueur" MCreator :
// - ProcedurePickaxeOfTheGodBlockDestroyedWithTool : compteur "level" en CustomData,
@@ -104,6 +105,9 @@ public final class PalaToolEvents {
ItemStack hand = sp.getMainHandItem();
Item held = hand.isEmpty() ? null : hand.getItem();
// --- Metiers : XP mineur (M0) ---
PalaJobs.onBlockBreak(sp, sl, pos, state);
// --- Pioche divine : compteur + evolution ---
if (held != null && GOD_PICKAXES.contains(held)) {
if (held == PalaItems.PICKAXE_OF_THE_GOD_LEVEL25) {
Binary file not shown.