From 80e780c496d03a216775091edbcbf56e891b4169 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:53:29 +0200 Subject: [PATCH 01/33] fix(db): stop closing shared SessionFactory in flag service Each of the 9 flag operations wrapped the SessionFactory in try-with-resources, closing the shared singleton after the first call. Subsequent flag operations then failed with "SessionFactory is closed". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../service/flag/DatabaseLandFlagService.java | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/flag/DatabaseLandFlagService.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/flag/DatabaseLandFlagService.java index 3debf7e1..24fccd80 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/flag/DatabaseLandFlagService.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/flag/DatabaseLandFlagService.java @@ -20,7 +20,6 @@ import net.onelitefeather.pandorascluster.database.models.flag.LandRoleFlagEntity; import org.hibernate.HibernateException; import org.hibernate.Session; -import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.jetbrains.annotations.NotNull; @@ -40,8 +39,7 @@ public DatabaseLandFlagService(PandorasCluster pandorasCluster) { public void addRoleFlag(@NotNull RoleFlag roleFlag, FlagContainer flagContainer) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); @@ -58,8 +56,7 @@ public void addRoleFlag(@NotNull RoleFlag roleFlag, FlagContainer flagContainer) @Override public void updateRoleFlag(@NotNull LandRoleFlag roleFlag) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); session.merge(getRoleFlagEntity(roleFlag)); @@ -74,8 +71,7 @@ public void updateRoleFlag(@NotNull LandRoleFlag roleFlag) { @Override public void removeRoleFlag(@NotNull LandRoleFlag roleFlag, @NotNull FlagContainer flagContainer) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); @@ -93,8 +89,7 @@ public void removeRoleFlag(@NotNull LandRoleFlag roleFlag, @NotNull FlagContaine public void addNaturalFlag(@NotNull NaturalFlag naturalFlag, FlagContainer flagContainer) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); @@ -111,8 +106,7 @@ public void addNaturalFlag(@NotNull NaturalFlag naturalFlag, FlagContainer flagC @Override public void updateNaturalCapFlag(@NotNull LandNaturalFlag naturalFlag) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); session.merge(getNaturalFlagEntity(naturalFlag)); @@ -127,8 +121,7 @@ public void updateNaturalCapFlag(@NotNull LandNaturalFlag naturalFlag) { @Override public void removeNaturalFlag(@NotNull LandNaturalFlag naturalFlag, @NotNull FlagContainer flagContainer) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); flagContainer.removeNaturalFlag(naturalFlag); @@ -145,8 +138,7 @@ public void removeNaturalFlag(@NotNull LandNaturalFlag naturalFlag, @NotNull Fla public void addEntityCapFlag(@NotNull EntityCapFlag entityCapFlag, FlagContainer flagContainer) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); @@ -163,8 +155,7 @@ public void addEntityCapFlag(@NotNull EntityCapFlag entityCapFlag, FlagContainer @Override public void updateEntityCapFlag(@NotNull LandEntityCapFlag entityCapFlag) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); session.merge(getEntityCapFlagEntity(entityCapFlag)); @@ -179,8 +170,7 @@ public void updateEntityCapFlag(@NotNull LandEntityCapFlag entityCapFlag) { @Override public void removeEntityCapFlag(@NotNull LandEntityCapFlag entityCapFlag, @NotNull FlagContainer flagContainer) { Transaction transaction = null; - try (SessionFactory factory = this.databaseService.sessionFactory(); - Session session = factory.openSession()) { + try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); flagContainer.removeEntityCapFlag(entityCapFlag); From 044c12da14fa4c3218495540be2e6163d9878be2 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:53:36 +0200 Subject: [PATCH 02/33] fix(db): return HomePosition model from entityToModel mapper The entity-to-model direction was constructing a new HomePositionEntity instead of the HomePosition API model, so callers received an entity where they expected the DTO. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/mapper/position/HomePositionMappingStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/position/HomePositionMappingStrategy.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/position/HomePositionMappingStrategy.java index 1b5c9915..d2b9a6e3 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/position/HomePositionMappingStrategy.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/position/HomePositionMappingStrategy.java @@ -14,7 +14,7 @@ public Function entityToModel() { return entity -> { if(entity == null) return null; if(!(entity instanceof HomePositionEntity home)) return null; - return new HomePositionEntity(home.id(), home.world(), home.posX(), home.posY(), home.posZ(), home.yaw(), home.pitch()); + return new HomePosition(home.id(), home.world(), home.posX(), home.posY(), home.posZ(), home.yaw(), home.pitch()); }; } From 97ad743499f3326b154f174556b9f28bd09812ab Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:53:43 +0200 Subject: [PATCH 03/33] fix(db): fail fast when the Hibernate SessionFactory cannot be built Previously a HibernateException was swallowed, databaseService was left null, and the plugin continued to enable with null service references, NPEing on the first command. Throw IllegalStateException instead so the plugin entry point can log and disable cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pandorascluster/api/PandorasClusterImpl.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/common/src/main/java/net/onelitefeather/pandorascluster/api/PandorasClusterImpl.java b/common/src/main/java/net/onelitefeather/pandorascluster/api/PandorasClusterImpl.java index 90d3ecfc..2b5379cb 100644 --- a/common/src/main/java/net/onelitefeather/pandorascluster/api/PandorasClusterImpl.java +++ b/common/src/main/java/net/onelitefeather/pandorascluster/api/PandorasClusterImpl.java @@ -1,14 +1,11 @@ package net.onelitefeather.pandorascluster.api; import net.onelitefeather.pandorascluster.api.service.*; -import net.onelitefeather.pandorascluster.api.util.Constants; import net.onelitefeather.pandorascluster.database.service.*; import net.onelitefeather.pandorascluster.database.service.flag.DatabaseLandFlagService; import org.hibernate.HibernateException; import org.hibernate.cfg.Configuration; -import java.util.logging.Level; - public class PandorasClusterImpl implements PandorasCluster, ThreadHelper { private DatabaseService databaseService; @@ -25,12 +22,10 @@ public PandorasClusterImpl() { var sessionFactory = new Configuration().configure().configure("connection.cfg.xml").buildSessionFactory(); this.databaseService = new DatabaseServiceImpl(sessionFactory); } catch (HibernateException e) { - this.databaseService = null; - Constants.LOGGER.log(Level.SEVERE, "Cannot build session factory.", e); + throw new IllegalStateException("Failed to build Hibernate SessionFactory — plugin cannot start", e); } }); - if (databaseService == null) return; this.landPlayerService = new DatabaseLandPlayerService(databaseService); this.landFlagService = new DatabaseLandFlagService(this); this.landAreaService = new DatabaseLandAreaService(this, databaseService); From dad15bfbaac3a0525198f85f9ddbdeca50a547ef Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:53:49 +0200 Subject: [PATCH 04/33] fix(plugin): disable plugin cleanly when bootstrap throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the PandorasClusterImpl construction in onEnable so a failed Hibernate bootstrap no longer leaves the plugin half-initialized — the PluginManager disables the plugin and the error is surfaced in the log. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pandorascluster/PandorasClusterPlugin.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/onelitefeather/pandorascluster/PandorasClusterPlugin.java b/plugin/src/main/java/net/onelitefeather/pandorascluster/PandorasClusterPlugin.java index 21c15ad6..65378bfb 100644 --- a/plugin/src/main/java/net/onelitefeather/pandorascluster/PandorasClusterPlugin.java +++ b/plugin/src/main/java/net/onelitefeather/pandorascluster/PandorasClusterPlugin.java @@ -15,7 +15,13 @@ public class PandorasClusterPlugin extends JavaPlugin { @Override public void onEnable() { - this.pandorasCluster = new PandorasClusterImpl(); + try { + this.pandorasCluster = new PandorasClusterImpl(); + } catch (IllegalStateException e) { + getLogger().log(java.util.logging.Level.SEVERE, "PandorasCluster failed to initialize.", e); + getServer().getPluginManager().disablePlugin(this); + return; + } this.paperCommandService = new PaperCommandService(this); this.paperCommandService.registerCommands(); // Register the service for third-party plugins to use From f27ba6347a45fa6bfb65bb178dbcb6889af9decb Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:53:57 +0200 Subject: [PATCH 05/33] fix(db): correct chunk-removal return value and parameter binding - removeClaimedChunk hard-coded return false, so callers never saw a successful deletion. - getClaimedChunk declared a :chunkIndex named parameter but never bound it, making the query return an arbitrary row. - Document unclaimArea as a best-effort composition of already- transactional sub-operations so future readers do not assume atomicity. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../service/DatabaseLandAreaService.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java index ef81d370..da63e04d 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java @@ -60,13 +60,13 @@ public boolean removeClaimedChunk(long chunkIndex) { transaction = session.beginTransaction(); session.remove(toEntity(claimedChunk)); transaction.commit(); + return true; } catch (HibernateException e) { if (transaction != null) transaction.rollback(); Constants.LOGGER.log(Level.SEVERE, "Cannot delete claimed chunk", e); + return false; } - - return false; } @Override @@ -74,6 +74,7 @@ public boolean removeClaimedChunk(long chunkIndex) { try (Session session = this.databaseService.sessionFactory().openSession()) { var query = session.createQuery("SELECT cc FROM ClaimedChunkEntity cc WHERE cc.chunkIndex = :chunkIndex", ClaimedChunkEntity.class); + query.setParameter("chunkIndex", chunkIndex); return toModel(query.uniqueResult()); } catch (HibernateException e) { Constants.LOGGER.log(Level.SEVERE, "Could not find any chunk with chunkIndex %s".formatted(chunkIndex), e); @@ -85,7 +86,14 @@ public boolean removeClaimedChunk(long chunkIndex) { public @Nullable LandArea getLandArea(long chunkIndex) { try (Session session = this.databaseService.sessionFactory().openSession()) { - var query = session.createQuery("SELECT cc FROM ClaimedChunkEntity cc JOIN FETCH cc.landArea WHERE cc.chunkIndex = :chunkindex", ClaimedChunkEntity.class); + var query = session.createQuery( + "SELECT DISTINCT cc FROM ClaimedChunkEntity cc " + + "JOIN FETCH cc.landArea la " + + "LEFT JOIN FETCH la.members " + + "LEFT JOIN FETCH la.chunks " + + "LEFT JOIN FETCH la.land " + + "WHERE cc.chunkIndex = :chunkindex", + ClaimedChunkEntity.class); query.setParameter("chunkindex", chunkIndex); ClaimedChunkEntity claimedChunk = query.uniqueResult(); @@ -100,6 +108,12 @@ public boolean removeClaimedChunk(long chunkIndex) { } } + /** + * Best-effort composition of already-transactional sub-operations. Each call + * to {@code removeLandMember} and {@code removeClaimedChunk} opens its own + * Hibernate session and commits independently, so a partial failure leaves + * the area in an intermediate state. Do not rely on atomicity here. + */ @Override public void unclaimArea(LandArea landArea) { landArea.getMembers().forEach(this.pandorasCluster.getLandPlayerService()::removeLandMember); From 2706486dd808021af2a04fa946cbe1812db39c69 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:09 +0200 Subject: [PATCH 06/33] fix(db): tighten transaction boundaries in land service - createLand now persists the land, its home, its flag container, the default LandArea and its initial ClaimedChunk in a single transaction so a partial failure rolls back cleanly. Previously the area creation ran after the outer commit, leaving orphan rows on exception. - unclaimLand moves its cross-service sub-calls (removeFlagsFromLand, landAreaService::unclaimArea) outside the session, then loads the LandEntity by id and removes it along with its HomePositionEntity. The old code called session.remove(land) with the API model. - getLands eagerly fetches owner, home, flagContainer and areas to prevent LazyInitializationException after the session closes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/service/DatabaseLandService.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java index 33353abf..ccbba417 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java @@ -47,7 +47,13 @@ public DatabaseLandService(PandorasCluster cluster) { @Override public @NotNull List getLands() { try (Session session = this.databaseService.sessionFactory().openSession()) { - var query = session.createQuery("SELECT l FROM LandEntity l", LandEntity.class); + var query = session.createQuery( + "SELECT DISTINCT l FROM LandEntity l " + + "LEFT JOIN FETCH l.owner " + + "LEFT JOIN FETCH l.home " + + "LEFT JOIN FETCH l.flagContainerEntity " + + "LEFT JOIN FETCH l.areas", + LandEntity.class); var lands = query.list(); return lands.stream().map(this::toModel).toList(); } catch (HibernateException e) { @@ -131,12 +137,14 @@ public void addLandArea(Land land, String name, List chunks) { session.persist(landEntity); session.persist(landEntity.home()); - transaction.commit(); - var land = new Land(landEntity.id(), owner, home, Collections.emptyList(), FlagContainer.EMPTY); + var landAreaEntity = new LandAreaEntity(null, "default", Collections.emptyList(), Collections.emptyList(), landEntity); + var claimedChunkEntity = new ClaimedChunkEntity(null, chunk.getChunkIndex(), landAreaEntity); + session.persist(landAreaEntity); + session.persist(claimedChunkEntity); - addLandArea(land, "default", List.of(chunk)); + transaction.commit(); - return land; + return new Land(landEntity.id(), owner, home, Collections.emptyList(), FlagContainer.EMPTY); } catch (HibernateException e) { Constants.LOGGER.log(Level.SEVERE, "Cannot create land!", e); if (transaction != null) transaction.rollback(); @@ -147,19 +155,23 @@ public void addLandArea(Land land, String name, List chunks) { @Override public void unclaimLand(@NotNull Land land) { + removeFlagsFromLand(land); + land.getAreas().forEach(this.landAreaService::unclaimArea); Transaction transaction = null; try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); - removeFlagsFromLand(land); - land.getAreas().forEach(this.landAreaService::unclaimArea); - - session.remove(land); - session.remove(toEntity(land.getHome())); + LandEntity landEntity = session.byId(LandEntity.class).load(land.getId()); + if (landEntity != null) { + HomePositionEntity homeEntity = (HomePositionEntity) landEntity.home(); + session.remove(landEntity); + if (homeEntity != null) session.remove(homeEntity); + } transaction.commit(); } catch (HibernateException e) { if (transaction != null) transaction.rollback(); + Constants.LOGGER.log(Level.SEVERE, "Cannot unclaim land.", e); } } From 63ef306fe136c6ea7213016a57a107a2f7a1e857 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:17 +0200 Subject: [PATCH 07/33] fix(db): add explicit join columns and id-based equality to LandEntity Without @JoinColumn, Hibernate generated implementation-defined FK column names. Bind owner/home/flagContainerEntity to owner_id, home_id and flag_container_id respectively, all NOT NULL, and mark the flag_container side as the owning side of the OneToOne (the inverse mappedBy moves to FlagContainerEntity). Add id-based equals/hashCode with a null-id identity fallback. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/models/land/LandEntity.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandEntity.java index 2c6e8e7e..a6896f02 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandEntity.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; @Entity @Table(name = "lands") @@ -24,15 +25,18 @@ public final class LandEntity implements LandDto { private Long id; @ManyToOne + @JoinColumn(name = "owner_id", nullable = false) private LandPlayerEntity owner; @ManyToOne + @JoinColumn(name = "home_id", nullable = false) private HomePositionEntity home; @OneToMany(fetch = FetchType.LAZY, mappedBy = "land") private List areas; @OneToOne + @JoinColumn(name = "flag_container_id", nullable = false) private FlagContainerEntity flagContainerEntity; public LandEntity() { @@ -75,4 +79,17 @@ public LandEntity(Long id, public FlagContainerDto flagContainer() { return this.flagContainerEntity; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LandEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From 35101bfd71e06bf60adf26ccc7d6bea63d6c93e5 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:23 +0200 Subject: [PATCH 08/33] fix(db): make FlagContainerEntity the inverse side of the Land link Drop the unmapped OneToOne so the LandEntity side alone owns the FK column (flag_container_id). Add id-based equals/hashCode. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/flag/FlagContainerEntity.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/FlagContainerEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/FlagContainerEntity.java index c08f9d71..930a1f98 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/FlagContainerEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/FlagContainerEntity.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; @Entity @Table(name = "flag_containers") @@ -28,7 +29,7 @@ public final class FlagContainerEntity implements FlagContainerDto { @OneToMany(fetch = FetchType.LAZY, mappedBy = "flagContainer") private List entityCapFlags; - @OneToOne + @OneToOne(mappedBy = "flagContainerEntity") private LandEntity land; public static final FlagContainerEntity EMPTY = new FlagContainerEntity( @@ -83,4 +84,17 @@ public List roleFlags() { public LandDto land() { return this.land; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FlagContainerEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From 3c1dbd6a0193284030ba7e389549dc2d325f760e Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:29 +0200 Subject: [PATCH 09/33] fix(db): switch LandMemberEntity.member to ManyToOne MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A player can be a member of multiple land areas, so the OneToOne was semantically wrong — the second addLandMember for the same player would violate the uniqueness implied by OneToOne. Also add an explicit member_id join column. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/models/player/LandMemberEntity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandMemberEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandMemberEntity.java index c6465d65..a361e467 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandMemberEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandMemberEntity.java @@ -19,7 +19,8 @@ public final class LandMemberEntity implements LandMemberDto { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) private LandPlayerEntity member; @Enumerated(EnumType.STRING) From a2cd12edad29949eba92d1445c63693cc087fcbd Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:38 +0200 Subject: [PATCH 10/33] fix(db): add unique, NOT NULL and length constraints to LandPlayerEntity Replace dialect-specific columnDefinition with portable length / nullable / unique annotations. uuid becomes the natural unique key (length 36), name is length 16 NOT NULL. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/models/player/LandPlayerEntity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandPlayerEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandPlayerEntity.java index 814c3bbb..dfddc020 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandPlayerEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/player/LandPlayerEntity.java @@ -15,10 +15,10 @@ public final class LandPlayerEntity implements LandPlayerDto { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(columnDefinition = "VARCHAR(36)") + @Column(name = "uuid", length = 36, nullable = false, unique = true) private String uuid; - @Column(columnDefinition = "VARCHAR(16)") + @Column(name = "name", length = 16, nullable = false) private String name; public LandPlayerEntity() { From b55efe2caf56bd99354bcef8cfd6313b3e350ad3 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:44 +0200 Subject: [PATCH 11/33] fix(db): enforce NOT NULL on HomePositionEntity columns A position without world + coordinates has no meaning; declare every column non-nullable and give world a 64-char bound. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/models/position/HomePositionEntity.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/position/HomePositionEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/position/HomePositionEntity.java index 988beea4..116a1c37 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/position/HomePositionEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/position/HomePositionEntity.java @@ -15,22 +15,22 @@ public final class HomePositionEntity implements HomePositionDto { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(name = "world", length = 64, nullable = false) private String world; - @Column + @Column(name = "pos_x", nullable = false) private Double posX; - @Column + @Column(name = "pos_y", nullable = false) private Double posY; - @Column + @Column(name = "pos_z", nullable = false) private Double posZ; - @Column + @Column(name = "yaw", nullable = false) private Float yaw; - @Column + @Column(name = "pitch", nullable = false) private Float pitch; public HomePositionEntity() { From 9cc26f39cc0844678c507b4ad59f401a54e3618b Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:50 +0200 Subject: [PATCH 12/33] fix(db): add name column and id-based equality to LandAreaEntity name had no @Column annotation, so Hibernate generated an unbounded nullable column. Declare length 64 NOT NULL and add id-based equals/hashCode. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/models/land/LandAreaEntity.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandAreaEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandAreaEntity.java index 753f1ce0..6f23b75e 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandAreaEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/land/LandAreaEntity.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; @Entity @Table(name = "land_areas") @@ -21,6 +22,7 @@ public final class LandAreaEntity implements LandAreaDto { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "name", length = 64, nullable = false) private String name; @OneToMany(fetch = FetchType.LAZY, mappedBy = "landArea") @@ -73,4 +75,17 @@ public LandAreaEntity(Long id, public LandDto land() { return this.land; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LandAreaEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From 1958e95ff4d6c025de69326d5def1ddae5e47f1a Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:54:56 +0200 Subject: [PATCH 13/33] fix(db): prevent duplicate claims on the same chunk Rename chunkIndex to chunk_index and add a composite unique constraint on (landArea_id, chunk_index) so the same chunk can no longer be claimed twice in the same area. Also add id-based equals/hashCode. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/chunk/ClaimedChunkEntity.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/chunk/ClaimedChunkEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/chunk/ClaimedChunkEntity.java index ac3792ad..56edc92e 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/chunk/ClaimedChunkEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/chunk/ClaimedChunkEntity.java @@ -5,19 +5,27 @@ import net.onelitefeather.pandorascluster.dto.chunk.ClaimedChunkDto; import net.onelitefeather.pandorascluster.dto.land.LandAreaDto; +import java.util.Objects; + @Entity -@Table(name = "land_chunks") +@Table( + name = "land_chunks", + uniqueConstraints = @UniqueConstraint( + name = "uk_land_chunks_area_chunk", + columnNames = {"landArea_id", "chunk_index"} + ) +) public final class ClaimedChunkEntity implements ClaimedChunkDto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(name = "chunk_index", nullable = false) private Long chunkIndex; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "landArea_id") + @JoinColumn(name = "landArea_id", nullable = false) private LandAreaEntity landArea; public ClaimedChunkEntity() { @@ -43,4 +51,17 @@ public Long chunkIndex() { public LandAreaDto landArea() { return landArea; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClaimedChunkEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From bd6926300d07ed4df3979373f43c229622bd81ba Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:03 +0200 Subject: [PATCH 14/33] fix(db): constrain LandRoleFlagEntity columns and de-duplicate Add length/NOT NULL on name, state, role and flagContainer_id, plus a uk_role_flags_container_name unique constraint so the same role flag cannot be persisted twice for one container. Add id-based equality. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/flag/LandRoleFlagEntity.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandRoleFlagEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandRoleFlagEntity.java index f42af5cb..3dcef477 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandRoleFlagEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandRoleFlagEntity.java @@ -7,25 +7,34 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; + @Entity -@Table(name = "role_flags") +@Table( + name = "role_flags", + uniqueConstraints = @UniqueConstraint( + name = "uk_role_flags_container_name", + columnNames = {"flagContainer_id", "name"} + ) +) public final class LandRoleFlagEntity implements RoleFlagDto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(name = "name", length = 64, nullable = false) private String name; - @Column + @Column(name = "state", nullable = false) private boolean state; @Enumerated(EnumType.STRING) + @Column(name = "role", length = 32, nullable = false) private LandRole role; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "flagContainer_id") + @JoinColumn(name = "flagContainer_id", nullable = false) private FlagContainerEntity flagContainer; public LandRoleFlagEntity() { @@ -65,4 +74,16 @@ public FlagContainerDto flagContainer() { return this.flagContainer; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LandRoleFlagEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From 7a9f3007f2f24aa0c0a7d945ead1c3b614ed2de7 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:09 +0200 Subject: [PATCH 15/33] fix(db): constrain LandNaturalFlagEntity columns and de-duplicate Add length/NOT NULL on name, state and flagContainer_id, plus a uk_natural_flags_container_name unique constraint. Add id-based equality. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/flag/LandNaturalFlagEntity.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandNaturalFlagEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandNaturalFlagEntity.java index 1f0be32f..04a4b18d 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandNaturalFlagEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandNaturalFlagEntity.java @@ -6,22 +6,30 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; + @Entity -@Table(name = "natural_flags") +@Table( + name = "natural_flags", + uniqueConstraints = @UniqueConstraint( + name = "uk_natural_flags_container_name", + columnNames = {"flagContainer_id", "name"} + ) +) public final class LandNaturalFlagEntity implements NaturalFlagDto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(name = "name", length = 64, nullable = false) private String name; - @Column + @Column(name = "state", nullable = false) private boolean state; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "flagContainer_id") + @JoinColumn(name = "flagContainer_id", nullable = false) private FlagContainerEntity flagContainer; public LandNaturalFlagEntity() { @@ -54,4 +62,17 @@ public boolean state() { public FlagContainerDto flagContainer() { return flagContainer; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LandNaturalFlagEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From 2435a10abbdf12c4d3b497af5553a9182bc44f68 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:15 +0200 Subject: [PATCH 16/33] fix(db): constrain LandEntityCapFlagEntity columns and de-duplicate Add length/NOT NULL on name, spawn_limit and flagContainer_id, plus a uk_entitycap_flags_container_name unique constraint. Add id-based equality. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/flag/LandEntityCapFlagEntity.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandEntityCapFlagEntity.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandEntityCapFlagEntity.java index 81d201d9..a3342f20 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandEntityCapFlagEntity.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/models/flag/LandEntityCapFlagEntity.java @@ -6,22 +6,30 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; + @Entity -@Table(name = "entityCap_flags") +@Table( + name = "entityCap_flags", + uniqueConstraints = @UniqueConstraint( + name = "uk_entitycap_flags_container_name", + columnNames = {"flagContainer_id", "name"} + ) +) public final class LandEntityCapFlagEntity implements EntityCapFlagDto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(name = "name", length = 64, nullable = false) private String name; - @Column + @Column(name = "spawn_limit", nullable = false) private Integer spawnLimit; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "flagContainer_id") + @JoinColumn(name = "flagContainer_id", nullable = false) private FlagContainerEntity flagContainer; @@ -56,4 +64,17 @@ public Integer spawnLimit() { public FlagContainerDto flagContainer() { return this.flagContainer; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LandEntityCapFlagEntity that)) return false; + if (id == null || that.id == null) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id == null ? System.identityHashCode(this) : Objects.hashCode(id); + } } From 118ad11248f5d5da5ca35a43b5dba7f9cab11e1f Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:24 +0200 Subject: [PATCH 17/33] fix(db): use ClaimedChunkMappingStrategy for chunk mapping getChunks was feeding ClaimedChunkDto instances into a new LandAreaMappingStrategy, so the instanceof check failed and every chunk came back null. Use ClaimedChunkMappingStrategy.create() instead. Also document that the strategy must run inside an open Hibernate session. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/mapper/land/LandAreaMappingStrategy.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandAreaMappingStrategy.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandAreaMappingStrategy.java index 8a280ac1..8a4c82ee 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandAreaMappingStrategy.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandAreaMappingStrategy.java @@ -20,6 +20,10 @@ import java.util.List; import java.util.function.Function; +/** + * Must be invoked while the Hibernate {@link org.hibernate.Session} that loaded the entity + * is still open — this strategy traverses {@code members} and {@code chunks} lazy collections. + */ public final class LandAreaMappingStrategy implements MapperStrategy { public static LandAreaMappingStrategy create() { @@ -61,7 +65,7 @@ private Land getLand(LandDto land) { private List getChunks(List chunks) { MappingContext mappingContext = MappingContext.create(); - mappingContext.setMappingStrategy(create()); + mappingContext.setMappingStrategy(ClaimedChunkMappingStrategy.create()); mappingContext.setMappingType(MapperType.ENTITY_TO_MODEL); return chunks.stream().map(chunk -> (ClaimedChunk) mappingContext.doMapping(chunk)).toList(); } From ff604dde460d7387f169bdee3e27e42cb5990799 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:31 +0200 Subject: [PATCH 18/33] fix(db): use per-flag strategies when mapping FlagContainer children The three flag collections were previously mapped through a single LandMappingStrategy instance, so every NaturalFlag / RoleFlag / EntityCapFlag failed the instanceof check and came back null. Wire each collection to its own NaturalFlagMappingStrategy / RoleFlagMappingStrategy / EntityCapFlagMappingStrategy and keep the land link on its own context. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../flag/FlagContainerMappingStrategy.java | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/flag/FlagContainerMappingStrategy.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/flag/FlagContainerMappingStrategy.java index 58816570..17118c26 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/flag/FlagContainerMappingStrategy.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/flag/FlagContainerMappingStrategy.java @@ -19,6 +19,11 @@ import java.util.List; import java.util.function.Function; +/** + * Must be invoked while the Hibernate {@link org.hibernate.Session} that loaded the entity + * is still open — this strategy traverses {@code land}, {@code naturalFlags}, + * {@code roleFlags} and {@code entityCapFlags} lazy associations. + */ public final class FlagContainerMappingStrategy implements MapperStrategy { @Override @@ -27,25 +32,33 @@ public Function entityToModel() { if (entity == null) return null; if (!(entity instanceof FlagContainerDto flagContainer)) return null; - MappingContext mappingContext = MappingContext.create(); - mappingContext.setMappingStrategy(LandMappingStrategy.create()); - mappingContext.setMappingType(MapperType.ENTITY_TO_MODEL); - - Land land = (Land) mappingContext.doMapping(flagContainer.land()); + MappingContext landCtx = MappingContext.create(); + landCtx.setMappingStrategy(LandMappingStrategy.create()); + landCtx.setMappingType(MapperType.ENTITY_TO_MODEL); + Land land = (Land) landCtx.doMapping(flagContainer.land()); + MappingContext naturalCtx = MappingContext.create(); + naturalCtx.setMappingStrategy(NaturalFlagMappingStrategy.create()); + naturalCtx.setMappingType(MapperType.ENTITY_TO_MODEL); List naturalFlags = flagContainer.naturalFlags() .stream() - .map(naturalFlagDBO -> (LandNaturalFlag) mappingContext.doMapping(naturalFlagDBO)) + .map(flag -> (LandNaturalFlag) naturalCtx.doMapping(flag)) .toList(); + MappingContext roleCtx = MappingContext.create(); + roleCtx.setMappingStrategy(RoleFlagMappingStrategy.create()); + roleCtx.setMappingType(MapperType.ENTITY_TO_MODEL); List roleFlags = flagContainer.roleFlags() .stream() - .map(naturalFlagDBO -> (LandRoleFlag) mappingContext.doMapping(naturalFlagDBO)) + .map(flag -> (LandRoleFlag) roleCtx.doMapping(flag)) .toList(); + MappingContext capCtx = MappingContext.create(); + capCtx.setMappingStrategy(EntityCapFlagMappingStrategy.create()); + capCtx.setMappingType(MapperType.ENTITY_TO_MODEL); List entityCapFlags = flagContainer.entityCapFlags() .stream() - .map(naturalFlagDBO -> (LandEntityCapFlag) mappingContext.doMapping(naturalFlagDBO)) + .map(flag -> (LandEntityCapFlag) capCtx.doMapping(flag)) .toList(); return new FlagContainer(flagContainer.id(), land, naturalFlags, roleFlags, entityCapFlags); @@ -59,27 +72,35 @@ public Function modelToEntity() { if (model == null) return null; if(!(model instanceof FlagContainer flagContainer)) return null; - MappingContext mappingContext = MappingContext.create(); - mappingContext.setMappingStrategy(LandMappingStrategy.create()); - mappingContext.setMappingType(MapperType.MODEL_TO_ENTITY); + MappingContext landCtx = MappingContext.create(); + landCtx.setMappingStrategy(LandMappingStrategy.create()); + landCtx.setMappingType(MapperType.MODEL_TO_ENTITY); + LandEntity land = (LandEntity) landCtx.doMapping(flagContainer.getLand()); + MappingContext naturalCtx = MappingContext.create(); + naturalCtx.setMappingStrategy(NaturalFlagMappingStrategy.create()); + naturalCtx.setMappingType(MapperType.MODEL_TO_ENTITY); List naturalFlags = flagContainer.getNaturalFlags() .stream() - .map(naturalFlagDBO -> (LandNaturalFlagEntity) mappingContext.doMapping(naturalFlagDBO)) + .map(flag -> (LandNaturalFlagEntity) naturalCtx.doMapping(flag)) .toList(); + MappingContext roleCtx = MappingContext.create(); + roleCtx.setMappingStrategy(RoleFlagMappingStrategy.create()); + roleCtx.setMappingType(MapperType.MODEL_TO_ENTITY); List roleFlags = flagContainer.getRoleFlags() .stream() - .map(naturalFlagDBO -> (LandRoleFlagEntity) mappingContext.doMapping(naturalFlagDBO)) + .map(flag -> (LandRoleFlagEntity) roleCtx.doMapping(flag)) .toList(); + MappingContext capCtx = MappingContext.create(); + capCtx.setMappingStrategy(EntityCapFlagMappingStrategy.create()); + capCtx.setMappingType(MapperType.MODEL_TO_ENTITY); List entityCapFlags = flagContainer.getEntityCapFlags() .stream() - .map(naturalFlagDBO -> (LandEntityCapFlagEntity) mappingContext.doMapping(naturalFlagDBO)) + .map(flag -> (LandEntityCapFlagEntity) capCtx.doMapping(flag)) .toList(); - LandEntity land = (LandEntity) mappingContext.doMapping(flagContainer.getLand()); - return new FlagContainerEntity(flagContainer.getId(), land, naturalFlags, roleFlags, entityCapFlags); }; } From 439bb0d2957ba20e4b0a2ae03367778f39206a5d Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:38 +0200 Subject: [PATCH 19/33] docs(db): document session-open requirement for LandMappingStrategy Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/mapper/land/LandMappingStrategy.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandMappingStrategy.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandMappingStrategy.java index efb313a8..768d1189 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandMappingStrategy.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/mapper/land/LandMappingStrategy.java @@ -18,6 +18,11 @@ import java.util.List; import java.util.function.Function; +/** + * Must be invoked while the Hibernate {@link org.hibernate.Session} that loaded the entity + * is still open — this strategy traverses {@code owner}, {@code home}, {@code flagContainer}, + * and {@code areas}. Use {@code DatabaseLandService#getLands()} as the canonical JOIN FETCH pattern. + */ public final class LandMappingStrategy implements MapperStrategy { @Override From 72b58735bcb4de7cfacc50401d6608fabd4baeda Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:45 +0200 Subject: [PATCH 20/33] chore(config): consolidate entity mappings into common/hibernate.cfg.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The common module builds the SessionFactory, so the list lives here. The plugin-side hibernate.cfg.xml becomes redundant and is removed in a follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- common/src/main/resources/hibernate.cfg.xml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/common/src/main/resources/hibernate.cfg.xml b/common/src/main/resources/hibernate.cfg.xml index 57929a0b..f69747d9 100644 --- a/common/src/main/resources/hibernate.cfg.xml +++ b/common/src/main/resources/hibernate.cfg.xml @@ -23,6 +23,16 @@ org.hibernate.cache.internal.NoCacheProvider - + + + + + + + + + + + - \ No newline at end of file + From ff3336ef94e4bc33ef0d48ecb77185ef851cd703 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:51 +0200 Subject: [PATCH 21/33] chore(config): drop duplicate plugin-side hibernate.cfg.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entity mappings now live in common/hibernate.cfg.xml. Keeping two copies invited drift — the plugin copy had the mappings while the common copy had the dialect/connection block. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/src/main/resources/hibernate.cfg.xml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 plugin/src/main/resources/hibernate.cfg.xml diff --git a/plugin/src/main/resources/hibernate.cfg.xml b/plugin/src/main/resources/hibernate.cfg.xml deleted file mode 100644 index 568ad592..00000000 --- a/plugin/src/main/resources/hibernate.cfg.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file From bfbd9d60f4d7ee85e6880a0d3c2177e974165229 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:55:58 +0200 Subject: [PATCH 22/33] chore(test): run common tests against in-memory H2 The test config pointed at localhost PostgreSQL, so a developer needed a running Postgres container to execute :common:test. Switch to an in-memory H2 database with H2Dialect and create-drop so tests can run in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) --- common/src/test/resources/connection.cfg.xml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/common/src/test/resources/connection.cfg.xml b/common/src/test/resources/connection.cfg.xml index 57929a0b..1ffe126b 100644 --- a/common/src/test/resources/connection.cfg.xml +++ b/common/src/test/resources/connection.cfg.xml @@ -4,25 +4,19 @@ "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> - org.postgresql.Driver - jdbc:postgresql://localhost:5432/pandoras_cluster - PUBLIC - pandoras_cluster - pandoras_cluster + org.h2.Driver + jdbc:h2:mem:pandoras;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + sa + - 1 - org.hibernate.dialect.PostgreSQLDialect + org.hibernate.dialect.H2Dialect false false - - update + create-drop - org.hibernate.cache.internal.NoCacheProvider - - - \ No newline at end of file + From cb1ad90300cb81ee61e41bf205840af6a7828d23 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:04 +0200 Subject: [PATCH 23/33] chore(build): pull in H2 for the common test classpath New in-memory test configuration in connection.cfg.xml needs the H2 driver at runtime for :common:test. Co-Authored-By: Claude Opus 4.7 (1M context) --- common/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index cc160537..43af5893 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { testImplementation(project(":adapters:database")) testImplementation(project(":api")) + testImplementation(libs.h2) testImplementation(platform(libs.junitBom)) testImplementation(libs.junitApi) From 062a92a3f636ba4aa7d7364b71528348188be338 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:11 +0200 Subject: [PATCH 24/33] chore(test): remove obsolete constants.kt The common module does not apply the Kotlin plugin, so this test support file never compiled. It also referenced DBO classes that were removed long ago. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onelitefeather/pandorascluster/api/constants.kt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 common/src/test/kotlin/net/onelitefeather/pandorascluster/api/constants.kt diff --git a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/constants.kt b/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/constants.kt deleted file mode 100644 index 673d882a..00000000 --- a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/constants.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.onelitefeather.pandorascluster.api - -import net.onelitefeather.pandorascluster.api.chunk.ClaimedChunk -import net.onelitefeather.pandorascluster.api.position.HomePosition -import java.util.* - -val updatedHome = HomePosition(null, 185.0, 27.0, 75.0, 0.0F, 0.0F) -val landHome = HomePosition(null, 180.0, 25.0, 60.0, 0.0F, 0.0F) -val mainChunk = ClaimedChunk(null, -12455436) -val landOwnerUUID: UUID = UUID.fromString("df7c66a6-7876-44c2-9abd-9bd33f7d3d9e") - -const val randomBound = 199999999L -val random = Random() \ No newline at end of file From 91efbb0881644679a87481e7a725000f77c38737 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:17 +0200 Subject: [PATCH 25/33] chore(test): remove obsolete TestDatabaseServiceImpl.kt Dead test support code referencing removed DatabaseService contract. Common module has no Kotlin plugin applied so it never compiled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/TestDatabaseServiceImpl.kt | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestDatabaseServiceImpl.kt diff --git a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestDatabaseServiceImpl.kt b/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestDatabaseServiceImpl.kt deleted file mode 100644 index 12fffee7..00000000 --- a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestDatabaseServiceImpl.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.onelitefeather.pandorascluster.api - -import net.onelitefeather.pandorascluster.api.chunk.ClaimedChunk -import net.onelitefeather.pandorascluster.api.land.Land -import net.onelitefeather.pandorascluster.api.land.flag.FlagRoleAttachment -import net.onelitefeather.pandorascluster.api.mapper.DatabaseEntityMapper -import net.onelitefeather.pandorascluster.api.player.LandMember -import net.onelitefeather.pandorascluster.api.player.LandPlayer -import net.onelitefeather.pandorascluster.api.position.HomePosition -import net.onelitefeather.pandorascluster.api.service.DatabaseService -import net.onelitefeather.pandorascluster.api.utils.ThreadHelper -import net.onelitefeather.pandorascluster.database.mapper.impl.* -import net.onelitefeather.pandorascluster.dbo.chunk.ClaimedChunkDBO -import net.onelitefeather.pandorascluster.dbo.flag.FlagRoleAttachmentDBO -import net.onelitefeather.pandorascluster.dbo.land.LandDBO -import net.onelitefeather.pandorascluster.dbo.player.LandMemberDBO -import net.onelitefeather.pandorascluster.dbo.player.LandPlayerDBO -import net.onelitefeather.pandorascluster.dbo.position.HomePositionDBO -import org.hibernate.HibernateException -import org.hibernate.SessionFactory -import org.hibernate.cfg.Configuration - -class TestDatabaseServiceImpl : DatabaseService, ThreadHelper { - - lateinit var sessionFactory: SessionFactory - - override fun connect(configFileResource: String) { - syncThreadForServiceLoader { - try { - sessionFactory = Configuration().configure().configure(configFileResource).buildSessionFactory() - } catch (e: HibernateException) { - throw HibernateException("Cannot build session factorty.", e) - } - } - } - - override fun shutdown() { - sessionFactory.close() - } - - override fun isRunning(): Boolean = sessionFactory.isOpen - - override fun sessionFactory(): SessionFactory = sessionFactory - - override fun landMapper(): DatabaseEntityMapper = LandEntityMapper(this) - - override fun landMemberMapper(): DatabaseEntityMapper = LandMemberMapper(this) - - override fun landPlayerMapper(): DatabaseEntityMapper = LandPlayerMapper() - - override fun claimedChunkMapper(): DatabaseEntityMapper = ClaimedChunkMapper() - - override fun homePositionMapper(): DatabaseEntityMapper = HomePositionMapper() - - override fun flagMapper(): DatabaseEntityMapper = FlagRoleAttachmentMapper() -} \ No newline at end of file From cd2340d0a8526a924f79b0ede10f51c8c1043e38 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:23 +0200 Subject: [PATCH 26/33] chore(test): remove obsolete TestLandFlagService.kt Dead test support code referencing removed DBO types. Common module has no Kotlin plugin applied so it never compiled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/TestLandFlagService.kt | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandFlagService.kt diff --git a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandFlagService.kt b/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandFlagService.kt deleted file mode 100644 index 6845d87b..00000000 --- a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandFlagService.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.onelitefeather.pandorascluster.api - -import net.onelitefeather.pandorascluster.api.enums.LandRole -import net.onelitefeather.pandorascluster.api.land.flag.LandFlag -import net.onelitefeather.pandorascluster.api.service.LandFlagService -import net.onelitefeather.pandorascluster.database.service.DatabaseLandFlagService -import org.junit.Test -import kotlin.test.assertNotNull - -class TestLandFlagService { - - private val api: PandorasClusterApi = TestPandorasClusterApiImpl() - private val landFlagService: LandFlagService = DatabaseLandFlagService(api.getDatabaseService(), api.getLandService()) - private val testLandService: TestLandService = TestLandService() - - @Test - fun testAddLandFlag() { - testLandService.testLandCreation() - val land = api.getLandService().getLand(mainChunk) - assertNotNull(land) - landFlagService.addLandFlag(LandFlag.POTION_SPLASH, LandRole.MEMBER, land) - landFlagService.addLandFlag(LandFlag.ENTITY_LEASH, LandRole.VISITOR, land) - landFlagService.addLandFlag(LandFlag.VILLAGER_INTERACT, LandRole.ADMIN, land) - } - - @Test - fun testRemoveLandFlag() { - - testLandService.testLandCreation() - val land = api.getLandService().getLand(mainChunk) - assertNotNull(land) - - val landFlag = land.getFlag(LandFlag.ENTITY_LEASH) - assertNotNull(landFlag) - landFlagService.removeLandFlag(landFlag, land) - } - - @Test - fun testUpdateLandFlag() { - - testLandService.testLandCreation() - val land = api.getLandService().getLand(mainChunk) - assertNotNull(land) - - val flag = land.getFlag(LandFlag.POTION_SPLASH) - assertNotNull(flag) - landFlagService.updateLandFlag(flag.copy(role = LandRole.ADMIN), land) - } -} \ No newline at end of file From d6fb427dd0e0cc317d3c3c137d62eb8d7f5a8e7e Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:33 +0200 Subject: [PATCH 27/33] chore(test): remove obsolete TestLandPlayerService.kt Dead test support code referencing removed DBO types. Common module has no Kotlin plugin applied so it never compiled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/TestLandPlayerService.kt | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandPlayerService.kt diff --git a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandPlayerService.kt b/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandPlayerService.kt deleted file mode 100644 index 75650312..00000000 --- a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandPlayerService.kt +++ /dev/null @@ -1,73 +0,0 @@ -package net.onelitefeather.pandorascluster.api - -import net.onelitefeather.pandorascluster.api.enums.LandRole -import net.onelitefeather.pandorascluster.api.service.LandPlayerService -import net.onelitefeather.pandorascluster.api.service.LandService -import org.junit.Test -import java.util.* -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class TestLandPlayerService { - - private val api: PandorasClusterApi = TestPandorasClusterApiImpl() - private val landService: LandService = api.getLandService() - private val landPlayerService: LandPlayerService = api.getLandPlayerService() - - private val testLandService = TestLandService() - - private val uuid = UUID.fromString("df7c66a6-7876-44c2-9abd-9bd33f7d3d9e") - - @Test - fun testCreateLandPlayer() { - val playerName = "theShadowsDust" - if(landPlayerService.playerExists(uuid)) return - assertTrue { landPlayerService.createPlayer(uuid, playerName) } - } - - @Test - fun testUpdateLandPlayer() { - val landPlayer = landPlayerService.getLandPlayer(uuid) - assertNotNull(landPlayer) - landPlayerService.updateLandPlayer(landPlayer.copy(name = "theEvilReaper")) - } - - @Test - fun testAddLandMember() { - testLandService.testLandCreation() - val land = landService.getLand(mainChunk) - assertNotNull(land) - - val landPlayer = landPlayerService.getLandPlayer(uuid) - assertNotNull(landPlayer) - - landPlayerService.addLandMember(land, landPlayer, LandRole.MEMBER) - } - - @Test - fun testUpdateLandMember() { - testLandService.testLandCreation() - val land = landService.getLand(mainChunk) - assertNotNull(land) - - val member = landPlayerService.getLandMember(land, uuid) - assertNotNull(member) - - landPlayerService.updateLandMember(land, member.copy(role = LandRole.TRUSTED)) - } - - @Test - fun testRemoveLandMember() { - val land = landService.getLand(mainChunk) - assertNotNull(land) - - val member = landPlayerService.getLandMember(land, uuid) - assertNotNull(member) - landPlayerService.removeLandMember(member) - } - - @Test - fun testDeleteLandPlayer() { - landPlayerService.deletePlayer(uuid) - } -} \ No newline at end of file From 22f1d47450fda5bc43e4acf094a8f962f20f33a9 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:38 +0200 Subject: [PATCH 28/33] chore(test): remove obsolete TestLandService.kt Dead test support code referencing removed DBO types. Common module has no Kotlin plugin applied so it never compiled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pandorascluster/api/TestLandService.kt | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandService.kt diff --git a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandService.kt b/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandService.kt deleted file mode 100644 index 738ae882..00000000 --- a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestLandService.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.onelitefeather.pandorascluster.api - -import net.onelitefeather.pandorascluster.api.chunk.ClaimedChunk -import net.onelitefeather.pandorascluster.api.service.LandService -import org.junit.Test -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class TestLandService { - - private val api: PandorasClusterApi = TestPandorasClusterApiImpl() - private val landService: LandService = api.getLandService() - - @Test - fun testCreateLandOwner() { - if (api.getLandPlayerService().playerExists(landOwnerUUID)) return - assertTrue(api.getLandPlayerService().createPlayer(landOwnerUUID, "theShadowsDust")) - } - - @Test - fun testLandCreation() { - val landPlayer = api.getLandPlayerService().getLandPlayer(landOwnerUUID) - assertNotNull(landPlayer) - - val land = landService.createLand(landPlayer, landHome, mainChunk, "world") - assertNotNull(land) - } - - @Test - fun testUpdateLandHome() { - - val owner = api.getLandPlayerService().getLandPlayer(landOwnerUUID) - assertNotNull(owner) - - val land = landService.getLand(owner) - assertNotNull(land) - - landService.updateLandHome(updatedHome.copy(land.home?.id), landOwnerUUID) - assertNotEquals(updatedHome, land.home) - } - - @Test - fun testisChunkClaimed() { - assertTrue(landService.isChunkClaimed(mainChunk)) - } - - @Test - fun testhasPlayerLand() { - val owner = api.getLandPlayerService().getLandPlayer(landOwnerUUID) - assertNotNull(owner) - assertTrue(landService.hasPlayerLand(owner)) - } - - @Test - fun testAddAndRemoveChunk() { - - val owner = api.getLandPlayerService().getLandPlayer(landOwnerUUID) - assertNotNull(owner) - - val land = landService.getLand(owner) - assertNotNull(land) - - val claimedChunk = ClaimedChunk(null, -random.nextLong(randomBound)) - landService.addClaimedChunk(claimedChunk, land) - - val chunk = landService.getClaimedChunk(claimedChunk.chunkIndex) - assertNotNull(chunk) - assertTrue(landService.removeClaimedChunk(chunk.chunkIndex)) - } - - @Test - fun testgetLand() { - - - val land = landService.getLand(mainChunk) - println("land = $land") - assertNotNull(land) - - val ownerByName = api.getLandPlayerService().getLandPlayer("theShadowsDust") - assertNotNull(ownerByName) - assertNotNull(landService.getLand(ownerByName)) - } - - @Test - fun testUnclaimLand() { - val land = landService.getLand(mainChunk) - assertNotNull(land) - landService.unclaimLand(land) - } -} \ No newline at end of file From b8b7df1df3817d5cfeaed30605b04a36462d0818 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:46 +0200 Subject: [PATCH 29/33] chore(test): remove obsolete TestPandorasClusterApiImpl.kt Dead test support code referencing a removed PandorasClusterApi interface and the wrong service constructors. Common module has no Kotlin plugin applied so it never compiled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/TestPandorasClusterApiImpl.kt | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestPandorasClusterApiImpl.kt diff --git a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestPandorasClusterApiImpl.kt b/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestPandorasClusterApiImpl.kt deleted file mode 100644 index b5292966..00000000 --- a/common/src/test/kotlin/net/onelitefeather/pandorascluster/api/TestPandorasClusterApiImpl.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.onelitefeather.pandorascluster.api - -import net.onelitefeather.pandorascluster.api.service.* -import net.onelitefeather.pandorascluster.database.service.DatabaseLandFlagService -import net.onelitefeather.pandorascluster.database.service.DatabaseLandPlayerService -import net.onelitefeather.pandorascluster.database.service.DatabaseLandService - -class TestPandorasClusterApiImpl : PandorasClusterApi { - - private var databaseService: DatabaseService = TestDatabaseServiceImpl() - private lateinit var landPlayerService: LandPlayerService - private lateinit var landFlagService: LandFlagService - private lateinit var landService: LandService - private lateinit var staffNotification: StaffNotificationService - - init { - databaseService.connect("connection.cfg.xml") - if (databaseService.isRunning()) { - landService = DatabaseLandService(this, databaseService) - landFlagService = DatabaseLandFlagService(databaseService, landService) - landPlayerService = DatabaseLandPlayerService(databaseService, landService) - staffNotification = StaffNotificationService(this) - } - } - - override fun getDatabaseStorageService(): LandService = landService - - override fun getLandPlayerService(): LandPlayerService = landPlayerService - - override fun getDatabaseService(): DatabaseService = databaseService - - override fun getLandService(): LandService = landService - - override fun getLandFlagService(): LandFlagService = landFlagService - - override fun getStaffNotification(): StaffNotificationService = staffNotification - - -} \ No newline at end of file From 3e49c130185d150b57aacb6d4bfeaa8fcd03cee9 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:56:53 +0200 Subject: [PATCH 30/33] chore(build): drop unused Liquibase plugin alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The liquibase Gradle plugin was applied but never configured — no changelog file, no liquibase {} block, no activities. Schema is owned by Hibernate via hbm2ddl.auto=update, so remove the dead alias. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 29d023df..5598d0ed 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -3,7 +3,6 @@ plugins { alias(libs.plugins.run.paper) alias(libs.plugins.plugin.yml) alias(libs.plugins.shadow) - alias(libs.plugins.liquibase) `maven-publish` } From ae654f6f91fc6b66dccdab3f9c3663f6c29f3d0c Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 19:57:01 +0200 Subject: [PATCH 31/33] chore(build): drop Liquibase and migrate Shadow to com.gradleup.shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the unused liquibase version/plugin entries. Schema is owned by Hibernate via hbm2ddl.auto=update. - Bump Shadow 8.1.1 → 9.4.1 and switch plugin id from the archived com.github.johnrengelman.shadow to com.gradleup.shadow so shadowJar works under Gradle 9.x. Co-Authored-By: Claude Opus 4.7 (1M context) --- settings.gradle.kts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index c85b8526..d40f3d13 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,7 +15,6 @@ dependencyResolutionManagement { create("libs") { // Non Paper - version("liquibase", "2.2.2") version("guava", "33.3.1-jre") version("jaxb-runtime", "4.0.7") version("caffeine", "3.2.3") @@ -41,7 +40,7 @@ dependencyResolutionManagement { // Gradle Plugins version("plugin.yml", "0.6.0") version("run-paper", "3.0.2") - version("shadow", "8.1.1") + version("shadow", "9.4.1") // Paper library("paper", "io.papermc.paper", "paper-api").versionRef("paper") @@ -82,8 +81,7 @@ dependencyResolutionManagement { // Plugins plugin("plugin.yml", "net.minecrell.plugin-yml.paper").versionRef("plugin.yml") plugin("run.paper", "xyz.jpenilla.run-paper").versionRef("run-paper") - plugin("shadow", "com.github.johnrengelman.shadow").versionRef("shadow") - plugin("liquibase", "org.liquibase.gradle").versionRef("liquibase") + plugin("shadow", "com.gradleup.shadow").versionRef("shadow") } } } From ac3eee1727ae4aa7206008800f5f1c53610b75b5 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 20:18:37 +0200 Subject: [PATCH 32/33] fix(db): split bag collections in getLandArea to avoid MultipleBagFetchException Hibernate 7 rejects JOIN FETCH-ing two bag-style List collections in the same query. The LandAreaEntity has two such bags (members and chunks), so the original query failed at translation time with "cannot simultaneously fetch multiple bags". Fetch the area together with its parent land in the primary query, then initialize the two bag collections separately via Hibernate.initialize(). Still inside the session, so the mapper can walk them without a LazyInitializationException. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../service/DatabaseLandAreaService.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java index da63e04d..c5e0add7 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandAreaService.java @@ -12,6 +12,7 @@ import net.onelitefeather.pandorascluster.database.mapper.land.LandAreaMappingStrategy; import net.onelitefeather.pandorascluster.database.models.chunk.ClaimedChunkEntity; import net.onelitefeather.pandorascluster.database.models.land.LandAreaEntity; +import org.hibernate.Hibernate; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.Transaction; @@ -86,20 +87,25 @@ public boolean removeClaimedChunk(long chunkIndex) { public @Nullable LandArea getLandArea(long chunkIndex) { try (Session session = this.databaseService.sessionFactory().openSession()) { - var query = session.createQuery( - "SELECT DISTINCT cc FROM ClaimedChunkEntity cc " + + var chunkQuery = session.createQuery( + "SELECT cc FROM ClaimedChunkEntity cc " + "JOIN FETCH cc.landArea la " + - "LEFT JOIN FETCH la.members " + - "LEFT JOIN FETCH la.chunks " + "LEFT JOIN FETCH la.land " + "WHERE cc.chunkIndex = :chunkindex", ClaimedChunkEntity.class); - query.setParameter("chunkindex", chunkIndex); + chunkQuery.setParameter("chunkindex", chunkIndex); - ClaimedChunkEntity claimedChunk = query.uniqueResult(); + ClaimedChunkEntity claimedChunk = chunkQuery.uniqueResult(); if (claimedChunk == null) return null; LandAreaEntity landArea = (LandAreaEntity) claimedChunk.landArea(); + + // Hibernate forbids JOIN FETCH-ing two bag-style collections in one query + // (MultipleBagFetchException), so the members and chunks collections are + // initialized in separate round-trips. + Hibernate.initialize(landArea.members()); + Hibernate.initialize(landArea.chunks()); + return toModel(landArea); } catch (HibernateException e) { From 0ea331e49ea421483c4adc55400bfd1891e6ba3f Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sat, 18 Apr 2026 20:18:47 +0200 Subject: [PATCH 33/33] fix(db): persist createLand graph in FK-dependency order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After home_id and flag_container_id became NOT NULL, Hibernate started throwing TransientPropertyValueException because LandEntity was persisted before its HomePositionEntity. Reorder so: 1. home and flag container (no outgoing non-null FKs) are persisted first 2. owner is referenced via session.getReference (player already exists) 3. landEntity is persisted once all three FKs point at managed entities 4. area and its initial chunk follow This also drops the flagContainer.withLand(landEntity) call — after Phase 3, FlagContainerEntity.land is mappedBy and carries no FK column, so the back-reference is purely ornamental at persist time. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../database/service/DatabaseLandService.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java index ccbba417..16fb0359 100644 --- a/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java +++ b/adapters/database/src/main/java/net/onelitefeather/pandorascluster/database/service/DatabaseLandService.java @@ -126,20 +126,23 @@ public void addLandArea(Land land, String name, List chunks) { try (Session session = this.databaseService.sessionFactory().openSession()) { transaction = session.beginTransaction(); + // Persist in FK-dependency order: home and flag container first (no outgoing + // non-null FKs), then the land (needs owner, home, flag_container), then the + // area and its first chunk. + var homeEntity = toHomePositionEntity(home); + session.persist(homeEntity); + var flagContainerEntity = new FlagContainerEntity(null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - var landEntity = new LandEntity(null, - new LandPlayerEntity(owner.getId(), owner.getUniqueId().toString(), owner.getName()), - toHomePositionEntity(home), - Collections.emptyList(), - flagContainerEntity); + session.persist(flagContainerEntity); - session.persist(flagContainerEntity.withLand(landEntity)); + var ownerRef = session.getReference(LandPlayerEntity.class, owner.getId()); + var landEntity = new LandEntity(null, ownerRef, homeEntity, Collections.emptyList(), flagContainerEntity); session.persist(landEntity); - session.persist(landEntity.home()); var landAreaEntity = new LandAreaEntity(null, "default", Collections.emptyList(), Collections.emptyList(), landEntity); - var claimedChunkEntity = new ClaimedChunkEntity(null, chunk.getChunkIndex(), landAreaEntity); session.persist(landAreaEntity); + + var claimedChunkEntity = new ClaimedChunkEntity(null, chunk.getChunkIndex(), landAreaEntity); session.persist(claimedChunkEntity); transaction.commit();