Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions apps/blockwarriors-beacon/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

<repositories>
Expand All @@ -26,6 +26,11 @@
<id>OnARandomBox</id>
<url>https://repo.onarandombox.com/content/groups/public/</url>
</repository>

<repository>
<id>enginehub</id>
<url>https://maven.enginehub.org/repo/</url>
</repository>
</repositories>

<dependencies>
Expand Down Expand Up @@ -56,7 +61,21 @@
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>


<!-- WorldEdit API for schematic loading (provided by server plugin) -->
<dependency>
<groupId>com.sk89q.worldedit</groupId>
<artifactId>worldedit-core</artifactId>
<version>7.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sk89q.worldedit</groupId>
<artifactId>worldedit-bukkit</artifactId>
<version>7.4.0</version>
<scope>provided</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import ai.blockwarriors.beacon.arena.ArenaManager;
import ai.blockwarriors.beacon.constants.GameConfig;
import ai.blockwarriors.beacon.game.GameRegistry;
import ai.blockwarriors.beacon.game.impl.BridgeGame;
import ai.blockwarriors.beacon.game.impl.CTFGame;
import ai.blockwarriors.beacon.game.impl.PvPGame;
import ai.blockwarriors.beacon.service.MatchPollingService;
import ai.blockwarriors.beacon.service.MatchTelemetryService;
Expand Down Expand Up @@ -116,7 +118,33 @@ public void onEnable() {
.build()
);

LOGGER.info("GameRegistry initialized. Registered game types: " +
// Register Bridge game type
registry.registerGame(
GameConfig.GAME_TYPE_BRIDGE,
BridgeGame::new,
GameRegistry.GameMetadata.builder()
.displayName("Bridge Challenge")
.description("1v1 bridge building — reach the enemy goal zone to score")
.playersPerTeam(1)
.teamCount(2)
.defaultArena("bridge")
.build()
);

// Register CTF game type
registry.registerGame(
GameConfig.GAME_TYPE_CTF,
CTFGame::new,
GameRegistry.GameMetadata.builder()
.displayName("Capture the Flag")
.description("4v4 team-based flag capture")
.playersPerTeam(4)
.teamCount(2)
.defaultArena("ctf")
.build()
);

LOGGER.info("GameRegistry initialized. Registered game types: " +
registry.getRegisteredTypes());

// Initialize login command with Convex URL and secret
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void loadArenas() {
*/
private void saveDefaultArenas() {
// List of default arena configs to save
String[] defaultArenas = {"pvp.yml"};
String[] defaultArenas = {"pvp.yml", "bridge.yml", "ctf.yml"};

for (String arenaName : defaultArenas) {
String resourcePath = ARENAS_FOLDER + "/" + arenaName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package ai.blockwarriors.beacon.arena;

import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
import com.sk89q.worldedit.function.operation.Operation;
import com.sk89q.worldedit.function.operation.Operations;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.session.ClipboardHolder;

import org.bukkit.Bukkit;
import org.bukkit.World;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.Logger;

/**
* Loads and pastes WorldEdit schematics (.schem files) into Minecraft worlds.
*
* Requires WorldEdit or FAWE to be installed on the server. If unavailable,
* {@link #isAvailable()} returns false and games should fall back to
* programmatic arena generation.
*/
public class SchematicLoader {

private static final Logger LOGGER = Logger.getLogger("beacon");

private static Boolean worldEditAvailable;

/**
* Check if WorldEdit is available on this server.
*/
public static boolean isAvailable() {
if (worldEditAvailable == null) {
worldEditAvailable = Bukkit.getPluginManager().getPlugin("WorldEdit") != null
|| Bukkit.getPluginManager().getPlugin("FastAsyncWorldEdit") != null;
if (worldEditAvailable) {
LOGGER.info("WorldEdit/FAWE detected — schematic loading enabled");
} else {
LOGGER.info("WorldEdit/FAWE not found — using programmatic arena generation");
}
}
return worldEditAvailable;
}

/**
* Load a schematic file into a WorldEdit Clipboard.
*
* @param schematicFile The .schem file to load
* @return The loaded Clipboard, or null on failure
*/
public static Clipboard loadSchematic(File schematicFile) {
ClipboardFormat format = ClipboardFormats.findByFile(schematicFile);
if (format == null) {
LOGGER.warning("Unknown schematic format: " + schematicFile.getName());
return null;
}

try (ClipboardReader reader = format.getReader(new FileInputStream(schematicFile))) {
Clipboard clipboard = reader.read();
LOGGER.info("Loaded schematic: " + schematicFile.getName()
+ " (" + clipboard.getRegion().getVolume() + " blocks)");
return clipboard;
} catch (IOException e) {
LOGGER.severe("Failed to load schematic " + schematicFile.getName() + ": " + e.getMessage());
return null;
}
}

/**
* Paste a schematic into a world at the given coordinates.
*
* @param schematicFile The .schem file to paste
* @param world Target Bukkit world
* @param x Paste origin X
* @param y Paste origin Y
* @param z Paste origin Z
* @return true if paste succeeded
*/
public static boolean pasteSchematic(File schematicFile, World world, int x, int y, int z) {
return pasteSchematic(schematicFile, world, x, y, z, true);
}

/**
* Paste a schematic into a world at the given coordinates.
*
* @param schematicFile The .schem file to paste
* @param world Target Bukkit world
* @param x Paste origin X
* @param y Paste origin Y
* @param z Paste origin Z
* @param ignoreAir If true, air blocks in the schematic won't overwrite existing blocks
* @return true if paste succeeded
*/
public static boolean pasteSchematic(File schematicFile, World world, int x, int y, int z, boolean ignoreAir) {
if (!isAvailable()) {
LOGGER.warning("Cannot paste schematic — WorldEdit not available");
return false;
}

Clipboard clipboard = loadSchematic(schematicFile);
if (clipboard == null) {
return false;
}

return pasteClipboard(clipboard, world, x, y, z, ignoreAir);
}

/**
* Paste a pre-loaded Clipboard into a world.
*
* @param clipboard The loaded clipboard
* @param world Target Bukkit world
* @param x Paste origin X
* @param y Paste origin Y
* @param z Paste origin Z
* @param ignoreAir Whether to skip air blocks
* @return true if paste succeeded
*/
public static boolean pasteClipboard(Clipboard clipboard, World world, int x, int y, int z, boolean ignoreAir) {
try (EditSession editSession = WorldEdit.getInstance()
.newEditSession(BukkitAdapter.adapt(world))) {

Operation operation = new ClipboardHolder(clipboard)
.createPaste(editSession)
.to(BlockVector3.at(x, y, z))
.ignoreAirBlocks(ignoreAir)
.build();

Operations.complete(operation);

LOGGER.info("Pasted schematic at (" + x + ", " + y + ", " + z + ") in world " + world.getName());
return true;
} catch (Exception e) {
LOGGER.severe("Failed to paste schematic: " + e.getMessage());
e.printStackTrace();
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private GameConfig() {
public static final int WOOL_WARS_TOKENS_PER_TEAM = 4;
public static final int BRIDGE_TOKENS_PER_TEAM = 1;
public static final int BEDWARS_TOKENS_PER_TEAM = 4;
public static final int CTF_TOKENS_PER_TEAM = 5;
public static final int CTF_TOKENS_PER_TEAM = 4;

/**
* Get tokens per team for a game type.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package ai.blockwarriors.beacon.game;

import ai.blockwarriors.beacon.arena.ArenaConfig;
import ai.blockwarriors.beacon.arena.ArenaManager;
import ai.blockwarriors.beacon.arena.SchematicLoader;

import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;
import java.util.*;
import java.util.logging.Logger;

Expand Down Expand Up @@ -41,6 +44,9 @@ public abstract class BaseGame {

/** Arena configuration (spawns, boundaries, objectives) */
protected ArenaConfig arenaConfig;

/** Arena manager for schematic resolution */
protected ArenaManager arenaManager;

/** Current game state */
protected GameState state = GameState.INITIALIZING;
Expand Down Expand Up @@ -233,6 +239,14 @@ public World getWorld() {
public ArenaConfig getArenaConfig() {
return arenaConfig;
}

/**
* Set the arena manager for schematic resolution.
* Called by MatchInitializer before initialize().
*/
public void setArenaManager(ArenaManager arenaManager) {
this.arenaManager = arenaManager;
}

/**
* Check if a player is in this game.
Expand Down Expand Up @@ -297,8 +311,59 @@ public boolean hasEnded() {
return state == GameState.FINISHED || state == GameState.TERMINATED;
}

// ==================== Event Methods ====================

/**
* Handle damage dealt by one player to another.
* Default implementation is a no-op. Override in game implementations
* that need to track damage (e.g., PvP).
*
* @param damager The player dealing damage
* @param target The player receiving damage
* @param damage The final damage amount
*/
public void handleDamage(Player damager, Player target, double damage) {
// Default no-op — override in subclasses that track damage
}

// ==================== Arena Loading ====================

/**
* Load the arena for this game. Tries to load a WorldEdit schematic first;
* if unavailable or no schematic exists, falls back to programmatic generation
* via {@link #generateArena()}.
*/
protected void loadArena() {
try {
if (arenaConfig != null && arenaManager != null && SchematicLoader.isAvailable()) {
File schematicFile = arenaManager.getSchematicFile(arenaConfig.getName());
if (schematicFile != null && schematicFile.exists()) {
boolean success = SchematicLoader.pasteSchematic(schematicFile, world, 0, 65, 0);
if (success) {
LOGGER.info("Arena loaded from schematic for match " + matchId);
return;
}
LOGGER.warning("Schematic paste failed for match " + matchId + ", falling back to programmatic generation");
}
}
} catch (NoClassDefFoundError e) {
// WorldEdit classes not on classpath — expected when WorldEdit is not installed
LOGGER.info("WorldEdit not available (classes not found), using programmatic arena generation");
}
generateArena();
}

/**
* Generate the arena programmatically. Called as a fallback when no
* schematic is available. Subclasses should override this to place
* blocks for their specific arena layout.
*/
protected void generateArena() {
// Default no-op — subclasses override with block placement logic
}

// ==================== Utility Methods ====================

/**
* Get players on the opposing team.
*
Expand Down
Loading
Loading