feat: add contract cooldowns, mining target matches, and rank command rewards
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
out/
|
out/
|
||||||
|
logs/
|
||||||
|
|
||||||
# Loom / Mod Dev Gradle caches
|
# Loom / Mod Dev Gradle caches
|
||||||
.loom-cache/
|
.loom-cache/
|
||||||
|
|||||||
+21
-13
@@ -20,15 +20,19 @@ repositories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||||
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||||
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
||||||
|
|
||||||
implementation "org.yaml:snakeyaml:${project.snakeyaml_version}"
|
implementation "org.yaml:snakeyaml:${project.snakeyaml_version}"
|
||||||
include "org.yaml:snakeyaml:${project.snakeyaml_version}"
|
include "org.yaml:snakeyaml:${project.snakeyaml_version}"
|
||||||
}
|
|
||||||
|
testImplementation platform("org.junit:junit-bom:5.11.4")
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter"
|
||||||
|
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
||||||
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
inputs.property 'version', project.version
|
inputs.property 'version', project.version
|
||||||
@@ -38,9 +42,13 @@ processResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
it.options.release = 21
|
it.options.release = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
@@ -65,4 +73,4 @@ publishing {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,18 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
|||||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||||
|
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
import net.minecraft.entity.damage.DamageSource;
|
import net.minecraft.entity.damage.DamageSource;
|
||||||
|
import net.minecraft.item.BlockItem;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
import net.minecraft.registry.Registries;
|
import net.minecraft.registry.Registries;
|
||||||
|
import net.minecraft.util.ActionResult;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
import net.minecraft.text.Text;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -56,7 +61,6 @@ public final class SoulStealMod implements ModInitializer {
|
|||||||
private TrackerCompassService trackerCompassService;
|
private TrackerCompassService trackerCompassService;
|
||||||
private ShopService shopService;
|
private ShopService shopService;
|
||||||
private HudService hudService;
|
private HudService hudService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the mod, loads configuration and persistent state, and registers all runtime
|
* Initializes the mod, loads configuration and persistent state, and registers all runtime
|
||||||
* event handlers.
|
* event handlers.
|
||||||
@@ -74,7 +78,7 @@ public final class SoulStealMod implements ModInitializer {
|
|||||||
throw new UncheckedIOException("Failed to load Soul Steal configuration or persistent data.", exception);
|
throw new UncheckedIOException("Failed to load Soul Steal configuration or persistent data.", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
permissionService = new PermissionService(dataStore);
|
permissionService = new PermissionService(dataStore, () -> this.config().permissions());
|
||||||
soulService = new SoulService(this::config, dataStore);
|
soulService = new SoulService(this::config, dataStore);
|
||||||
bountyService = new BountyService(this::config, dataStore, soulService);
|
bountyService = new BountyService(this::config, dataStore, soulService);
|
||||||
contractService = new ContractService(() -> this.bundle().contractCatalog(), dataStore, soulService);
|
contractService = new ContractService(() -> this.bundle().contractCatalog(), dataStore, soulService);
|
||||||
@@ -102,8 +106,36 @@ public final class SoulStealMod implements ModInitializer {
|
|||||||
contractService.recordHunting(killer, Registries.ENTITY_TYPE.getId(entity.getType()).toString());
|
contractService.recordHunting(killer, Registries.ENTITY_TYPE.getId(entity.getType()).toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> {
|
||||||
|
if (!(player instanceof ServerPlayerEntity serverPlayer)) {
|
||||||
|
return ActionResult.PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack stack = serverPlayer.getStackInHand(hand);
|
||||||
|
if (!(stack.getItem() instanceof BlockItem blockItem)) {
|
||||||
|
return ActionResult.PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match block placement by the item the player is holding, not the broken state.
|
||||||
|
// That lets us mark player-placed ore blocks even when silk touch preserves the block.
|
||||||
|
if (!contractService.matchesMiningTarget(blockItem.getBlock())) {
|
||||||
|
return ActionResult.PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = blockKey(world, placedBlockPos(hitResult.getBlockPos(), hitResult.getSide()));
|
||||||
|
dataStore.data().playerPlacedMiningTargets().add(key);
|
||||||
|
saveDataQuietly();
|
||||||
|
return ActionResult.PASS;
|
||||||
|
});
|
||||||
|
|
||||||
PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, blockEntity) -> {
|
PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, blockEntity) -> {
|
||||||
if (player instanceof ServerPlayerEntity serverPlayer) {
|
if (player instanceof ServerPlayerEntity serverPlayer) {
|
||||||
|
String key = blockKey(world, pos);
|
||||||
|
// Ignore blocks we recorded as player-placed targets; those should not advance mining.
|
||||||
|
if (dataStore.data().playerPlacedMiningTargets().remove(key)) {
|
||||||
|
saveDataQuietly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
contractService.recordMining(serverPlayer, Registries.BLOCK.getId(state.getBlock()).toString());
|
contractService.recordMining(serverPlayer, Registries.BLOCK.getId(state.getBlock()).toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -185,6 +217,28 @@ public final class SoulStealMod implements ModInitializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveDataQuietly() {
|
||||||
|
try {
|
||||||
|
dataStore.save();
|
||||||
|
} catch (IOException exception) {
|
||||||
|
LOGGER.error("Failed to save Soul Steal data.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blockKey(net.minecraft.world.World world, net.minecraft.util.math.BlockPos pos) {
|
||||||
|
return world.getRegistryKey().getValue() + "|" + pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the block position where a placement will land for a normal face click.
|
||||||
|
*
|
||||||
|
* <p>We use the placed block position, not the clicked block position, so player-placed
|
||||||
|
* contract targets are marked correctly even when the item came from silk touch.</p>
|
||||||
|
*/
|
||||||
|
static BlockPos placedBlockPos(BlockPos clickedPos, Direction side) {
|
||||||
|
return clickedPos.offset(side);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the loaded configuration bundle.
|
* Returns the loaded configuration bundle.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public record ConfigBundle(SoulStealConfig config, ShopCatalog shopCatalog, Cont
|
|||||||
SoulStealConfig config = SoulStealConfig.fromMap(configMap);
|
SoulStealConfig config = SoulStealConfig.fromMap(configMap);
|
||||||
|
|
||||||
Map<String, Object> shopMap = YamlConfigHelper.loadOrCreate(configDirectory.resolve("shop.yml"), ShopCatalog.defaultYaml());
|
Map<String, Object> shopMap = YamlConfigHelper.loadOrCreate(configDirectory.resolve("shop.yml"), ShopCatalog.defaultYaml());
|
||||||
ShopCatalog shopCatalog = ShopCatalog.fromMap(shopMap, config.shop());
|
ShopCatalog shopCatalog = ShopCatalog.fromMap(shopMap, config.shop(), config.permissions());
|
||||||
|
|
||||||
Map<String, Object> contractMap = YamlConfigHelper.loadOrCreate(configDirectory.resolve("catalog.yml"), ContractCatalog.defaultYaml());
|
Map<String, Object> contractMap = YamlConfigHelper.loadOrCreate(configDirectory.resolve("catalog.yml"), ContractCatalog.defaultYaml());
|
||||||
ContractCatalog contractCatalog = ContractCatalog.fromMap(contractMap, config.contracts());
|
ContractCatalog contractCatalog = ContractCatalog.fromMap(contractMap, config.contracts());
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ public record SoulStealConfig(
|
|||||||
ContractConfig contracts,
|
ContractConfig contracts,
|
||||||
ShopUiConfig shop,
|
ShopUiConfig shop,
|
||||||
HudConfig hud,
|
HudConfig hud,
|
||||||
PermissionConfig permissions
|
PermissionConfig permissions
|
||||||
) {
|
) {
|
||||||
public static SoulStealConfig fromMap(Map<String, Object> root) {
|
public static SoulStealConfig fromMap(Map<String, Object> root) {
|
||||||
Map<String, Object> economySection = YamlConfigHelper.section(root, "economy");
|
Map<String, Object> economySection = YamlConfigHelper.section(root, "economy");
|
||||||
Map<String, Object> deathPenaltySection = YamlConfigHelper.section(economySection, "death_penalty");
|
Map<String, Object> deathPenaltySection = YamlConfigHelper.section(economySection, "death_penalty");
|
||||||
@@ -77,7 +77,8 @@ public record SoulStealConfig(
|
|||||||
new ContractHudConfig(
|
new ContractHudConfig(
|
||||||
YamlConfigHelper.bool(contractsSection, "hud_enabled", true),
|
YamlConfigHelper.bool(contractsSection, "hud_enabled", true),
|
||||||
YamlConfigHelper.string(contractsSection, "hud_title", "Active Contract")
|
YamlConfigHelper.string(contractsSection, "hud_title", "Active Contract")
|
||||||
)
|
),
|
||||||
|
Math.max(0L, YamlConfigHelper.longValue(contractsSection, "default_repeat_cooldown_seconds", 0L))
|
||||||
);
|
);
|
||||||
|
|
||||||
ShopUiConfig shopUiConfig = new ShopUiConfig(
|
ShopUiConfig shopUiConfig = new ShopUiConfig(
|
||||||
@@ -104,16 +105,17 @@ public record SoulStealConfig(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
PermissionConfig permissionConfig = new PermissionConfig(
|
PermissionConfig permissionConfig = new PermissionConfig(
|
||||||
YamlConfigHelper.string(permissionsSection, "admin_node", "soulsteal.admin"),
|
YamlConfigHelper.string(permissionsSection, "admin_node", "soulsteal.admin"),
|
||||||
YamlConfigHelper.string(permissionsSection, "reload_node", "soulsteal.admin.reload"),
|
YamlConfigHelper.string(permissionsSection, "reload_node", "soulsteal.admin.reload"),
|
||||||
YamlConfigHelper.string(permissionsSection, "shop_node", "soulsteal.shop"),
|
YamlConfigHelper.string(permissionsSection, "shop_node", "soulsteal.shop"),
|
||||||
YamlConfigHelper.string(permissionsSection, "bounty_node", "soulsteal.bounty"),
|
YamlConfigHelper.string(permissionsSection, "bounty_node", "soulsteal.bounty"),
|
||||||
YamlConfigHelper.string(permissionsSection, "balance_others_node", "soulsteal.admin.balance.others"),
|
YamlConfigHelper.bool(permissionsSection, "luckperms_enabled", true),
|
||||||
YamlConfigHelper.string(permissionsSection, "set_node", "soulsteal.admin.balance.set"),
|
YamlConfigHelper.string(permissionsSection, "balance_others_node", "soulsteal.admin.balance.others"),
|
||||||
YamlConfigHelper.string(permissionsSection, "add_node", "soulsteal.admin.balance.add"),
|
YamlConfigHelper.string(permissionsSection, "set_node", "soulsteal.admin.balance.set"),
|
||||||
YamlConfigHelper.string(permissionsSection, "take_node", "soulsteal.admin.balance.take"),
|
YamlConfigHelper.string(permissionsSection, "add_node", "soulsteal.admin.balance.add"),
|
||||||
YamlConfigHelper.string(permissionsSection, "scoreboard_node", "soulsteal.scoreboard"),
|
YamlConfigHelper.string(permissionsSection, "take_node", "soulsteal.admin.balance.take"),
|
||||||
|
YamlConfigHelper.string(permissionsSection, "scoreboard_node", "soulsteal.scoreboard"),
|
||||||
YamlConfigHelper.string(permissionsSection, "leaderboard_node", "soulsteal.leaderboard")
|
YamlConfigHelper.string(permissionsSection, "leaderboard_node", "soulsteal.leaderboard")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,17 +155,18 @@ public record SoulStealConfig(
|
|||||||
update_interval_ticks: 20
|
update_interval_ticks: 20
|
||||||
expire_if_target_offline: false
|
expire_if_target_offline: false
|
||||||
|
|
||||||
contracts:
|
contracts:
|
||||||
enabled: true
|
enabled: true
|
||||||
auto_claim: true
|
auto_claim: true
|
||||||
hud_enabled: true
|
hud_enabled: true
|
||||||
hud_title: "Active Contract"
|
hud_title: "Active Contract"
|
||||||
|
default_repeat_cooldown_seconds: 0
|
||||||
|
|
||||||
shop:
|
shop:
|
||||||
title: "Soul Shop"
|
title: "Soul Shop"
|
||||||
rows: 3
|
rows: 3
|
||||||
filler_item: "minecraft:light_gray_stained_glass_pane"
|
filler_item: "minecraft:light_gray_stained_glass_pane"
|
||||||
default_purchase_cooldown_seconds: 0
|
default_purchase_cooldown_seconds: 0
|
||||||
enable_custom_amount_selector: true
|
enable_custom_amount_selector: true
|
||||||
default_max_custom_amount: 64
|
default_max_custom_amount: 64
|
||||||
|
|
||||||
@@ -184,9 +187,10 @@ public record SoulStealConfig(
|
|||||||
# soulsteal.admin grants every admin-only action below.
|
# soulsteal.admin grants every admin-only action below.
|
||||||
admin_node: "soulsteal.admin"
|
admin_node: "soulsteal.admin"
|
||||||
reload_node: "soulsteal.admin.reload"
|
reload_node: "soulsteal.admin.reload"
|
||||||
shop_node: "soulsteal.shop"
|
shop_node: "soulsteal.shop"
|
||||||
bounty_node: "soulsteal.bounty"
|
bounty_node: "soulsteal.bounty"
|
||||||
balance_others_node: "soulsteal.admin.balance.others"
|
luckperms_enabled: true
|
||||||
|
balance_others_node: "soulsteal.admin.balance.others"
|
||||||
set_node: "soulsteal.admin.balance.set"
|
set_node: "soulsteal.admin.balance.set"
|
||||||
add_node: "soulsteal.admin.balance.add"
|
add_node: "soulsteal.admin.balance.add"
|
||||||
take_node: "soulsteal.admin.balance.take"
|
take_node: "soulsteal.admin.balance.take"
|
||||||
@@ -238,7 +242,7 @@ public record SoulStealConfig(
|
|||||||
public record TrackerConfig(boolean enabled, long durationSeconds, int updateIntervalTicks, boolean expireIfTargetOffline) {
|
public record TrackerConfig(boolean enabled, long durationSeconds, int updateIntervalTicks, boolean expireIfTargetOffline) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ContractConfig(boolean enabled, boolean autoClaim, ContractHudConfig hud) {
|
public record ContractConfig(boolean enabled, boolean autoClaim, ContractHudConfig hud, long defaultRepeatCooldownSeconds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ContractHudConfig(boolean enabled, String title) {
|
public record ContractHudConfig(boolean enabled, String title) {
|
||||||
@@ -266,15 +270,16 @@ public record SoulStealConfig(
|
|||||||
public record LeaderboardConfig(int pageSize) {
|
public record LeaderboardConfig(int pageSize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PermissionConfig(
|
public record PermissionConfig(
|
||||||
String adminNode,
|
String adminNode,
|
||||||
String reloadNode,
|
String reloadNode,
|
||||||
String shopNode,
|
String shopNode,
|
||||||
String bountyNode,
|
String bountyNode,
|
||||||
String balanceOthersNode,
|
boolean luckpermsEnabled,
|
||||||
String setNode,
|
String balanceOthersNode,
|
||||||
String addNode,
|
String setNode,
|
||||||
String takeNode,
|
String addNode,
|
||||||
|
String takeNode,
|
||||||
String scoreboardNode,
|
String scoreboardNode,
|
||||||
String leaderboardNode
|
String leaderboardNode
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
Object rawContracts = root.get("contracts");
|
Object rawContracts = root.get("contracts");
|
||||||
if (rawContracts instanceof List<?> rawList) {
|
if (rawContracts instanceof List<?> rawList) {
|
||||||
for (Object rawContract : rawList) {
|
for (Object rawContract : rawList) {
|
||||||
addContract(contracts, rawContract);
|
addContract(contracts, rawContract, config);
|
||||||
}
|
}
|
||||||
} else if (rawContracts instanceof Map<?, ?> rawSections) {
|
} else if (rawContracts instanceof Map<?, ?> rawSections) {
|
||||||
for (Map.Entry<?, ?> sectionEntry : rawSections.entrySet()) {
|
for (Map.Entry<?, ?> sectionEntry : rawSections.entrySet()) {
|
||||||
if (sectionEntry.getValue() instanceof List<?> rawList) {
|
if (sectionEntry.getValue() instanceof List<?> rawList) {
|
||||||
for (Object rawContract : rawList) {
|
for (Object rawContract : rawList) {
|
||||||
addContract(contracts, rawContract);
|
addContract(contracts, rawContract, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,12 +49,17 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
name: "Mining Contracts"
|
name: "Mining Contracts"
|
||||||
icon: "minecraft:iron_pickaxe"
|
icon: "minecraft:iron_pickaxe"
|
||||||
type: "mining"
|
type: "mining"
|
||||||
target: "minecraft:iron_ore"
|
targets:
|
||||||
|
- "minecraft:iron_ore"
|
||||||
|
- "minecraft:deepslate_iron_ore"
|
||||||
target_name: "Iron Ore"
|
target_name: "Iron Ore"
|
||||||
description: "Mine iron ore to earn souls."
|
amount: 20
|
||||||
amount: 64
|
reward: 200
|
||||||
reward: 250
|
|
||||||
repeatable: true
|
repeatable: true
|
||||||
|
cooldown: 10
|
||||||
|
matches:
|
||||||
|
- "Iron Ore"
|
||||||
|
- "Deepslate Iron Ore"
|
||||||
hunting:
|
hunting:
|
||||||
- id: "zombie_hunter"
|
- id: "zombie_hunter"
|
||||||
name: "Zombie Hunter"
|
name: "Zombie Hunter"
|
||||||
@@ -79,7 +84,7 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addContract(List<ContractDefinition> contracts, Object rawContract) {
|
private static void addContract(List<ContractDefinition> contracts, Object rawContract, ContractConfig config) {
|
||||||
if (!(rawContract instanceof Map<?, ?> rawMap)) {
|
if (!(rawContract instanceof Map<?, ?> rawMap)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,17 +102,38 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
java.util.List<String> targets = YamlConfigHelper.stringList(map, "targets");
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
// Backward compatibility: allow the old single-target field for existing catalogs.
|
||||||
|
String single = YamlConfigHelper.string(map, "target", "").trim();
|
||||||
|
if (!single.isBlank()) {
|
||||||
|
targets = java.util.List.of(single);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targets = targets.stream().map(String::trim).filter(target -> !target.isBlank()).distinct().toList();
|
||||||
|
|
||||||
|
java.util.List<String> matches = YamlConfigHelper.stringList(map, "matches")
|
||||||
|
.stream()
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(match -> !match.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
long cooldown = Math.max(0L, YamlConfigHelper.longValue(map, "cooldown", YamlConfigHelper.longValue(map, "cooldown_seconds", config.defaultRepeatCooldownSeconds())));
|
||||||
|
|
||||||
contracts.add(new ContractDefinition(
|
contracts.add(new ContractDefinition(
|
||||||
id,
|
id,
|
||||||
YamlConfigHelper.string(map, "name", id),
|
YamlConfigHelper.string(map, "name", id),
|
||||||
YamlConfigHelper.string(map, "icon", type == ContractType.MINING ? "minecraft:iron_pickaxe" : "minecraft:zombie_head"),
|
YamlConfigHelper.string(map, "icon", type == ContractType.MINING ? "minecraft:iron_pickaxe" : "minecraft:zombie_head"),
|
||||||
type,
|
type,
|
||||||
YamlConfigHelper.string(map, "target", ""),
|
targets,
|
||||||
|
matches,
|
||||||
YamlConfigHelper.string(map, "target_name", YamlConfigHelper.string(map, "target", id)),
|
YamlConfigHelper.string(map, "target_name", YamlConfigHelper.string(map, "target", id)),
|
||||||
YamlConfigHelper.string(map, "description", ""),
|
YamlConfigHelper.string(map, "description", ""),
|
||||||
Math.max(1L, YamlConfigHelper.longValue(map, "amount", 1L)),
|
Math.max(1L, YamlConfigHelper.longValue(map, "amount", 1L)),
|
||||||
Math.max(0L, YamlConfigHelper.longValue(map, "reward", 0L)),
|
Math.max(0L, YamlConfigHelper.longValue(map, "reward", 0L)),
|
||||||
YamlConfigHelper.bool(map, "repeatable", true)
|
YamlConfigHelper.bool(map, "repeatable", true),
|
||||||
|
cooldown
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.g2806.soulsteal.contract;
|
package com.g2806.soulsteal.contract;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable definition for one contract entry loaded from `catalog.yml`.
|
* Immutable definition for one contract entry loaded from `catalog.yml`.
|
||||||
*
|
*
|
||||||
@@ -11,11 +14,33 @@ public record ContractDefinition(
|
|||||||
String name,
|
String name,
|
||||||
String iconItemId,
|
String iconItemId,
|
||||||
ContractType type,
|
ContractType type,
|
||||||
String targetId,
|
List<String> targetIds,
|
||||||
|
List<String> displayMatches,
|
||||||
String targetName,
|
String targetName,
|
||||||
String description,
|
String description,
|
||||||
long amountRequired,
|
long amountRequired,
|
||||||
long reward,
|
long reward,
|
||||||
boolean repeatable
|
boolean repeatable,
|
||||||
|
long cooldownSeconds
|
||||||
) {
|
) {
|
||||||
|
public String primaryTarget() {
|
||||||
|
return targetIds == null || targetIds.isEmpty() ? "" : targetIds.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String targetSummary() {
|
||||||
|
if (targetIds == null || targetIds.isEmpty()) {
|
||||||
|
return targetName;
|
||||||
|
}
|
||||||
|
if (targetIds.size() == 1) {
|
||||||
|
return targetName;
|
||||||
|
}
|
||||||
|
return targetName + " (" + targetIds.size() + " targets)";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String displayMatchesSummary() {
|
||||||
|
if (displayMatches == null || displayMatches.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return displayMatches.stream().collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,13 @@ public final class ContractGuiService {
|
|||||||
if (!contract.description().isBlank()) {
|
if (!contract.description().isBlank()) {
|
||||||
lore.add(Text.literal(contract.description()).formatted(Formatting.GRAY));
|
lore.add(Text.literal(contract.description()).formatted(Formatting.GRAY));
|
||||||
}
|
}
|
||||||
lore.add(Text.literal("Target: " + contract.targetName()).formatted(Formatting.AQUA));
|
lore.add(Text.literal("Target: " + contract.targetSummary()).formatted(Formatting.AQUA));
|
||||||
|
if (contract.displayMatches() != null && !contract.displayMatches().isEmpty()) {
|
||||||
|
lore.add(Text.literal("Matches: " + contract.displayMatchesSummary()).formatted(Formatting.DARK_AQUA));
|
||||||
|
}
|
||||||
|
if (contract.cooldownSeconds() > 0L) {
|
||||||
|
lore.add(Text.literal("Cooldown: " + DurationFormatter.formatSeconds(contract.cooldownSeconds())).formatted(Formatting.DARK_GRAY));
|
||||||
|
}
|
||||||
lore.add(Text.literal("Progress: " + progress + "/" + contract.amountRequired()).formatted(Formatting.GOLD));
|
lore.add(Text.literal("Progress: " + progress + "/" + contract.amountRequired()).formatted(Formatting.GOLD));
|
||||||
lore.add(Text.literal("Reward: " + contract.reward() + " souls").formatted(Formatting.GREEN));
|
lore.add(Text.literal("Reward: " + contract.reward() + " souls").formatted(Formatting.GREEN));
|
||||||
lore.add(Text.literal(contract.repeatable() ? "Repeatable" : "One-time").formatted(Formatting.DARK_GRAY));
|
lore.add(Text.literal(contract.repeatable() ? "Repeatable" : "One-time").formatted(Formatting.DARK_GRAY));
|
||||||
@@ -237,7 +243,7 @@ public final class ContractGuiService {
|
|||||||
default -> List.of();
|
default -> List.of();
|
||||||
};
|
};
|
||||||
return contracts.stream()
|
return contracts.stream()
|
||||||
.filter(contract -> contract.repeatable() || !contractService.hasCompletedContract(player.getUuid(), contract.id()))
|
.filter(contract -> contractService.isContractAvailable(player.getUuid(), contract))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ public final class SoulStealData {
|
|||||||
private List<StoredBounty> activeBounties = new ArrayList<>();
|
private List<StoredBounty> activeBounties = new ArrayList<>();
|
||||||
private Map<String, Set<String>> unlockedEntries = new HashMap<>();
|
private Map<String, Set<String>> unlockedEntries = new HashMap<>();
|
||||||
private Map<String, Map<String, Long>> purchaseCooldowns = new HashMap<>();
|
private Map<String, Map<String, Long>> purchaseCooldowns = new HashMap<>();
|
||||||
private Map<String, Map<String, Boolean>> grantedPermissions = new HashMap<>();
|
private Map<String, Map<String, Boolean>> grantedPermissions = new HashMap<>();
|
||||||
private Map<String, Long> bountyPlacementCooldowns = new HashMap<>();
|
private Map<String, Integer> grantedRankPriorities = new HashMap<>();
|
||||||
private Map<String, String> playerNames = new HashMap<>();
|
private Map<String, Long> bountyPlacementCooldowns = new HashMap<>();
|
||||||
private Map<String, Boolean> scoreboardVisibility = new HashMap<>();
|
private Map<String, String> playerNames = new HashMap<>();
|
||||||
|
private Map<String, Boolean> scoreboardVisibility = new HashMap<>();
|
||||||
private Map<String, String> selectedContracts = new HashMap<>();
|
private Map<String, String> selectedContracts = new HashMap<>();
|
||||||
private Map<String, Map<String, Long>> contractProgress = new HashMap<>();
|
private Map<String, Map<String, Long>> contractProgress = new HashMap<>();
|
||||||
private Map<String, Set<String>> completedContracts = new HashMap<>();
|
private Map<String, Set<String>> completedContracts = new HashMap<>();
|
||||||
|
private Map<String, Map<String, Long>> contractCooldowns = new HashMap<>();
|
||||||
|
private Set<String> playerPlacedMiningTargets = new HashSet<>();
|
||||||
|
|
||||||
public SoulStealData normalize() {
|
public SoulStealData normalize() {
|
||||||
if (souls == null) {
|
if (souls == null) {
|
||||||
@@ -39,9 +42,12 @@ public final class SoulStealData {
|
|||||||
if (purchaseCooldowns == null) {
|
if (purchaseCooldowns == null) {
|
||||||
purchaseCooldowns = new HashMap<>();
|
purchaseCooldowns = new HashMap<>();
|
||||||
}
|
}
|
||||||
if (grantedPermissions == null) {
|
if (grantedPermissions == null) {
|
||||||
grantedPermissions = new HashMap<>();
|
grantedPermissions = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
if (grantedRankPriorities == null) {
|
||||||
|
grantedRankPriorities = new HashMap<>();
|
||||||
|
}
|
||||||
if (bountyPlacementCooldowns == null) {
|
if (bountyPlacementCooldowns == null) {
|
||||||
bountyPlacementCooldowns = new HashMap<>();
|
bountyPlacementCooldowns = new HashMap<>();
|
||||||
}
|
}
|
||||||
@@ -57,8 +63,14 @@ public final class SoulStealData {
|
|||||||
if (contractProgress == null) {
|
if (contractProgress == null) {
|
||||||
contractProgress = new HashMap<>();
|
contractProgress = new HashMap<>();
|
||||||
}
|
}
|
||||||
if (completedContracts == null) {
|
if (completedContracts == null) {
|
||||||
completedContracts = new HashMap<>();
|
completedContracts = new HashMap<>();
|
||||||
|
}
|
||||||
|
if (contractCooldowns == null) {
|
||||||
|
contractCooldowns = new HashMap<>();
|
||||||
|
}
|
||||||
|
if (playerPlacedMiningTargets == null) {
|
||||||
|
playerPlacedMiningTargets = new HashSet<>();
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -105,7 +117,11 @@ public final class SoulStealData {
|
|||||||
* @return mutable permission map
|
* @return mutable permission map
|
||||||
*/
|
*/
|
||||||
public Map<String, Map<String, Boolean>> grantedPermissions() {
|
public Map<String, Map<String, Boolean>> grantedPermissions() {
|
||||||
return grantedPermissions;
|
return grantedPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Integer> grantedRankPriorities() {
|
||||||
|
return grantedRankPriorities;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +159,15 @@ public final class SoulStealData {
|
|||||||
return contractProgress;
|
return contractProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Set<String>> completedContracts() {
|
public Map<String, Set<String>> completedContracts() {
|
||||||
return completedContracts;
|
return completedContracts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, Long>> contractCooldowns() {
|
||||||
|
return contractCooldowns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> playerPlacedMiningTargets() {
|
||||||
|
return playerPlacedMiningTargets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import com.g2806.soulsteal.contract.ContractType;
|
|||||||
import com.g2806.soulsteal.data.SoulStealDataStore;
|
import com.g2806.soulsteal.data.SoulStealDataStore;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
|
import net.minecraft.registry.Registries;
|
||||||
|
|
||||||
public final class ContractService {
|
public final class ContractService {
|
||||||
private final Supplier<ContractCatalog> catalogSupplier;
|
private final Supplier<ContractCatalog> catalogSupplier;
|
||||||
@@ -36,7 +38,7 @@ public final class ContractService {
|
|||||||
if (contract.isEmpty()) {
|
if (contract.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!contract.get().repeatable() && hasCompletedContract(player.getUuid(), contract.get().id())) {
|
if (!isContractAvailable(player.getUuid(), contract.get())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
dataStore.data().selectedContracts().put(key(player.getUuid()), contract.get().id());
|
dataStore.data().selectedContracts().put(key(player.getUuid()), contract.get().id());
|
||||||
@@ -69,6 +71,21 @@ public final class ContractService {
|
|||||||
.contains(contractId);
|
.contains(contractId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isContractAvailable(UUID playerUuid, ContractDefinition contract) {
|
||||||
|
String playerKey = key(playerUuid);
|
||||||
|
if (!contract.repeatable() && hasCompletedContract(playerUuid, contract.id())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Repeatable contracts re-enter the browser only after their per-player cooldown expires.
|
||||||
|
if (contract.repeatable() && contract.cooldownSeconds() > 0L) {
|
||||||
|
long until = dataStore.data().contractCooldowns()
|
||||||
|
.getOrDefault(playerKey, Map.of())
|
||||||
|
.getOrDefault(contract.id(), 0L);
|
||||||
|
return System.currentTimeMillis() >= until;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void recordMining(ServerPlayerEntity player, String blockId) {
|
public void recordMining(ServerPlayerEntity player, String blockId) {
|
||||||
record(player, ContractType.MINING, blockId);
|
record(player, ContractType.MINING, blockId);
|
||||||
}
|
}
|
||||||
@@ -77,13 +94,32 @@ public final class ContractService {
|
|||||||
record(player, ContractType.HUNTING, entityId);
|
record(player, ContractType.HUNTING, entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean matchesMiningTarget(net.minecraft.block.Block block) {
|
||||||
|
if (!catalogSupplier.get().enabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String blockId = Registries.BLOCK.getId(block).toString();
|
||||||
|
// Used when a player places a block that is also a mining target.
|
||||||
|
// We record that position so breaking the placed block does not count as real mining.
|
||||||
|
return catalogSupplier.get().contractsOfType(ContractType.MINING).stream()
|
||||||
|
.map(ContractDefinition::targetIds)
|
||||||
|
.filter(targetIds -> targetIds != null)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.anyMatch(target -> target.equalsIgnoreCase(blockId));
|
||||||
|
}
|
||||||
|
|
||||||
private void record(ServerPlayerEntity player, ContractType type, String targetId) {
|
private void record(ServerPlayerEntity player, ContractType type, String targetId) {
|
||||||
Optional<ContractDefinition> selected = selectedContract(player.getUuid());
|
Optional<ContractDefinition> selected = selectedContract(player.getUuid());
|
||||||
if (selected.isEmpty() || !catalogSupplier.get().enabled()) {
|
if (selected.isEmpty() || !catalogSupplier.get().enabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ContractDefinition contract = selected.get();
|
ContractDefinition contract = selected.get();
|
||||||
if (contract.type() != type || !contract.targetId().equalsIgnoreCase(targetId)) {
|
if (contract.type() != type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matches = contract.targetIds() != null && contract.targetIds().stream().anyMatch(t -> t.equalsIgnoreCase(targetId));
|
||||||
|
if (!matches) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +132,22 @@ public final class ContractService {
|
|||||||
player.sendMessage(net.minecraft.text.Text.literal("Contract complete: " + contract.name() + " (+"
|
player.sendMessage(net.minecraft.text.Text.literal("Contract complete: " + contract.name() + " (+"
|
||||||
+ contract.reward() + " souls)").formatted(net.minecraft.util.Formatting.GREEN), false);
|
+ contract.reward() + " souls)").formatted(net.minecraft.util.Formatting.GREEN), false);
|
||||||
if (!contract.repeatable()) {
|
if (!contract.repeatable()) {
|
||||||
|
// One-time contracts are permanently removed after completion.
|
||||||
|
dataStore.data().selectedContracts().remove(playerKey);
|
||||||
dataStore.data().completedContracts()
|
dataStore.data().completedContracts()
|
||||||
.computeIfAbsent(playerKey, ignored -> new java.util.HashSet<>())
|
.computeIfAbsent(playerKey, ignored -> new java.util.HashSet<>())
|
||||||
.add(contract.id());
|
.add(contract.id());
|
||||||
}
|
} else {
|
||||||
if (!contract.repeatable()) {
|
if (contract.cooldownSeconds() > 0L) {
|
||||||
dataStore.data().selectedContracts().remove(playerKey);
|
// Repeatable contracts with cooldown reappear after the timer expires.
|
||||||
|
dataStore.data().selectedContracts().remove(playerKey);
|
||||||
|
dataStore.data().contractCooldowns()
|
||||||
|
.computeIfAbsent(playerKey, ignored -> new java.util.HashMap<>())
|
||||||
|
.put(contract.id(), System.currentTimeMillis() + (contract.cooldownSeconds() * 1000L));
|
||||||
|
} else {
|
||||||
|
// Repeatable contracts with no cooldown immediately restart.
|
||||||
|
progressMap.put(contract.id(), 0L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
progressMap.put(contract.id(), updated);
|
progressMap.put(contract.id(), updated);
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package com.g2806.soulsteal.service;
|
package com.g2806.soulsteal.service;
|
||||||
|
|
||||||
import com.g2806.soulsteal.SoulStealMod;
|
import com.g2806.soulsteal.SoulStealMod;
|
||||||
import com.g2806.soulsteal.data.SoulStealDataStore;
|
import com.g2806.soulsteal.config.SoulStealConfig.PermissionConfig;
|
||||||
|
import com.g2806.soulsteal.data.SoulStealDataStore;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import net.minecraft.server.command.ServerCommandSource;
|
import java.util.function.Supplier;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.command.ServerCommandSource;
|
||||||
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bridges Soul Steal's permission checks to the Fabric Permissions API and optional LuckPerms.
|
* Bridges Soul Steal's permission checks to the Fabric Permissions API and optional LuckPerms.
|
||||||
@@ -19,11 +21,21 @@ import net.minecraft.server.network.ServerPlayerEntity;
|
|||||||
* <p>Permission rewards first try LuckPerms for external integrations, then optionally fall back to
|
* <p>Permission rewards first try LuckPerms for external integrations, then optionally fall back to
|
||||||
* a persisted internal store so Soul Steal's own nodes continue to work without extra mods.</p>
|
* a persisted internal store so Soul Steal's own nodes continue to work without extra mods.</p>
|
||||||
*/
|
*/
|
||||||
public final class PermissionService {
|
public final class PermissionService {
|
||||||
private final SoulStealDataStore dataStore;
|
private final SoulStealDataStore dataStore;
|
||||||
|
private final Supplier<PermissionConfig> permissionConfigSupplier;
|
||||||
public PermissionService(SoulStealDataStore dataStore) {
|
|
||||||
|
public PermissionService(SoulStealDataStore dataStore, Supplier<PermissionConfig> permissionConfigSupplier) {
|
||||||
this.dataStore = dataStore;
|
this.dataStore = dataStore;
|
||||||
|
this.permissionConfigSupplier = permissionConfigSupplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoulStealDataStore dataStore() {
|
||||||
|
return dataStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLuckPermsEnabled() {
|
||||||
|
return permissionConfigSupplier.get().luckpermsEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +124,7 @@ public final class PermissionService {
|
|||||||
* @return grant outcome and backend details
|
* @return grant outcome and backend details
|
||||||
*/
|
*/
|
||||||
public GrantResult grantPersistentPermission(UUID playerUuid, String permission, boolean value, boolean storeFallback) {
|
public GrantResult grantPersistentPermission(UUID playerUuid, String permission, boolean value, boolean storeFallback) {
|
||||||
boolean grantedViaLuckPerms = tryGrantWithLuckPerms(playerUuid, permission, value);
|
boolean grantedViaLuckPerms = tryGrantWithLuckPerms(playerUuid, permission, value);
|
||||||
boolean storedInternally = false;
|
boolean storedInternally = false;
|
||||||
|
|
||||||
if (storeFallback) {
|
if (storeFallback) {
|
||||||
@@ -132,9 +144,23 @@ public final class PermissionService {
|
|||||||
grantedViaLuckPerms ? "Permission granted successfully." : "Permission stored in Soul Steal fallback permissions.");
|
grantedViaLuckPerms ? "Permission granted successfully." : "Permission stored in Soul Steal fallback permissions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GrantResult(false, false, false,
|
return new GrantResult(false, false, false,
|
||||||
"No supported permissions backend was available for that reward.");
|
"No supported permissions backend was available for that reward.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RankGrantResult grantLuckPermsGroup(UUID playerUuid, String group, boolean storeFallback) {
|
||||||
|
if (!isLuckPermsEnabled()) {
|
||||||
|
return new RankGrantResult(false, false, "LuckPerms is disabled in config.");
|
||||||
|
}
|
||||||
|
boolean granted = tryGrantLuckPermsGroup(playerUuid, group);
|
||||||
|
if (!granted) {
|
||||||
|
return new RankGrantResult(false, false, "No supported permissions backend was available for that rank.");
|
||||||
|
}
|
||||||
|
if (storeFallback) {
|
||||||
|
saveQuietly();
|
||||||
|
}
|
||||||
|
return new RankGrantResult(true, storeFallback, "Rank granted successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasStoredPermission(UUID playerUuid, String permission) {
|
private boolean hasStoredPermission(UUID playerUuid, String permission) {
|
||||||
return dataStore.data().grantedPermissions()
|
return dataStore.data().grantedPermissions()
|
||||||
@@ -142,8 +168,8 @@ public final class PermissionService {
|
|||||||
.getOrDefault(permission, false);
|
.getOrDefault(permission, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryGrantWithLuckPerms(UUID playerUuid, String permission, boolean value) {
|
private boolean tryGrantWithLuckPerms(UUID playerUuid, String permission, boolean value) {
|
||||||
try {
|
try {
|
||||||
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||||
Object api = providerClass.getMethod("get").invoke(null);
|
Object api = providerClass.getMethod("get").invoke(null);
|
||||||
Object userManager = api.getClass().getMethod("getUserManager").invoke(api);
|
Object userManager = api.getClass().getMethod("getUserManager").invoke(api);
|
||||||
@@ -168,9 +194,46 @@ public final class PermissionService {
|
|||||||
return false;
|
return false;
|
||||||
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
|
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
|
||||||
SoulStealMod.LOGGER.warn("Failed to grant LuckPerms permission {} to {}", permission, playerUuid, exception);
|
SoulStealMod.LOGGER.warn("Failed to grant LuckPerms permission {} to {}", permission, playerUuid, exception);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean tryGrantLuckPermsGroup(UUID playerUuid, String group) {
|
||||||
|
try {
|
||||||
|
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||||
|
Object api = providerClass.getMethod("get").invoke(null);
|
||||||
|
Object userManager = api.getClass().getMethod("getUserManager").invoke(api);
|
||||||
|
Object groupManager = api.getClass().getMethod("getGroupManager").invoke(api);
|
||||||
|
Object lpGroup = groupManager.getClass().getMethod("getGroup", String.class).invoke(groupManager, group);
|
||||||
|
if (lpGroup == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String groupName = String.valueOf(lpGroup.getClass().getMethod("getName").invoke(lpGroup));
|
||||||
|
Class<?> userClass = Class.forName("net.luckperms.api.model.user.User");
|
||||||
|
|
||||||
|
Consumer<Object> consumer = user -> {
|
||||||
|
try {
|
||||||
|
Object data = user.getClass().getMethod("data").invoke(user);
|
||||||
|
Class<?> nodeClass = Class.forName("net.luckperms.api.node.Node");
|
||||||
|
Class<?> inheritanceNodeClass = Class.forName("net.luckperms.api.node.types.InheritanceNode");
|
||||||
|
Object builder = inheritanceNodeClass.getMethod("builder", String.class).invoke(null, groupName);
|
||||||
|
Object builtNode = builder.getClass().getMethod("build").invoke(builder);
|
||||||
|
data.getClass().getMethod("add", nodeClass).invoke(data, builtNode);
|
||||||
|
userManager.getClass().getMethod("saveUser", userClass).invoke(userManager, user);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
userManager.getClass().getMethod("modifyUser", UUID.class, Consumer.class).invoke(userManager, playerUuid, consumer);
|
||||||
|
return true;
|
||||||
|
} catch (ClassNotFoundException exception) {
|
||||||
|
return false;
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
|
||||||
|
SoulStealMod.LOGGER.warn("Failed to grant LuckPerms group {} to {}", group, playerUuid, exception);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Boolean invokePermissionsCheck(Object subject, String permission, Object defaultValue) {
|
private Boolean invokePermissionsCheck(Object subject, String permission, Object defaultValue) {
|
||||||
try {
|
try {
|
||||||
@@ -233,6 +296,9 @@ public final class PermissionService {
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record GrantResult(boolean success, boolean grantedViaLuckPerms, boolean storedInternally, String message) {
|
public record GrantResult(boolean success, boolean grantedViaLuckPerms, boolean storedInternally, String message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record RankGrantResult(boolean success, boolean storedInternally, String message) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.g2806.soulsteal.service;
|
package com.g2806.soulsteal.service;
|
||||||
|
|
||||||
import com.g2806.soulsteal.shop.CommandRewardDefinition;
|
import com.g2806.soulsteal.shop.CommandRewardDefinition;
|
||||||
import com.g2806.soulsteal.shop.EffectRewardDefinition;
|
import com.g2806.soulsteal.shop.EffectRewardDefinition;
|
||||||
import com.g2806.soulsteal.shop.ItemRewardDefinition;
|
import com.g2806.soulsteal.shop.ItemRewardDefinition;
|
||||||
import com.g2806.soulsteal.shop.PermissionRewardDefinition;
|
import com.g2806.soulsteal.shop.PermissionRewardDefinition;
|
||||||
import com.g2806.soulsteal.shop.RewardDefinition;
|
import com.g2806.soulsteal.shop.RewardDefinition;
|
||||||
import com.g2806.soulsteal.shop.StackMode;
|
import com.g2806.soulsteal.shop.StackMode;
|
||||||
import java.util.ArrayList;
|
import com.g2806.soulsteal.shop.RankRewardDefinition;
|
||||||
import java.util.List;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import net.minecraft.component.DataComponentTypes;
|
import net.minecraft.component.DataComponentTypes;
|
||||||
import net.minecraft.component.type.LoreComponent;
|
import net.minecraft.component.type.LoreComponent;
|
||||||
import net.minecraft.entity.effect.StatusEffect;
|
import net.minecraft.entity.effect.StatusEffect;
|
||||||
@@ -29,8 +33,8 @@ import net.minecraft.world.World;
|
|||||||
|
|
||||||
/** Executes validated shop rewards for the player who bought an entry. */
|
/** Executes validated shop rewards for the player who bought an entry. */
|
||||||
public final class RewardService {
|
public final class RewardService {
|
||||||
private final PermissionService permissionService;
|
private final PermissionService permissionService;
|
||||||
private final SoulService soulService;
|
private final SoulService soulService;
|
||||||
|
|
||||||
public RewardService(PermissionService permissionService, SoulService soulService) {
|
public RewardService(PermissionService permissionService, SoulService soulService) {
|
||||||
this.permissionService = permissionService;
|
this.permissionService = permissionService;
|
||||||
@@ -50,15 +54,20 @@ public final class RewardService {
|
|||||||
return new ValidationResult(false, "Unknown status effect id: " + effectReward.effectId());
|
return new ValidationResult(false, "Unknown status effect id: " + effectReward.effectId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case PermissionRewardDefinition permissionReward -> {
|
case PermissionRewardDefinition permissionReward -> {
|
||||||
if (permissionReward.node().isBlank()) {
|
if (permissionReward.node().isBlank()) {
|
||||||
return new ValidationResult(false, "Permission rewards require a non-empty node.");
|
return new ValidationResult(false, "Permission rewards require a non-empty node.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case CommandRewardDefinition commandReward -> {
|
case RankRewardDefinition rankReward -> {
|
||||||
if (commandReward.command().isBlank()) {
|
if (rankReward.command().isBlank()) {
|
||||||
return new ValidationResult(false, "Command rewards require a non-empty command string.");
|
return new ValidationResult(false, "Rank rewards require a non-empty command.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
case CommandRewardDefinition commandReward -> {
|
||||||
|
if (commandReward.command().isBlank()) {
|
||||||
|
return new ValidationResult(false, "Command rewards require a non-empty command string.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,18 +96,25 @@ public final class RewardService {
|
|||||||
applyEffectReward(player, effectEntry, effectReward);
|
applyEffectReward(player, effectEntry, effectReward);
|
||||||
granted.add(rewardDisplayName(effectReward));
|
granted.add(rewardDisplayName(effectReward));
|
||||||
}
|
}
|
||||||
case PermissionRewardDefinition permissionReward -> {
|
case PermissionRewardDefinition permissionReward -> {
|
||||||
PermissionService.GrantResult result = permissionService.grantPersistentPermission(
|
PermissionService.GrantResult result = permissionService.grantPersistentPermission(
|
||||||
player.getUuid(), permissionReward.node(), permissionReward.value(), permissionReward.storeFallback());
|
player.getUuid(), permissionReward.node(), permissionReward.value(), permissionReward.storeFallback());
|
||||||
if (!result.success()) {
|
if (!result.success()) {
|
||||||
return new GrantResult(false, result.message(), granted);
|
return new GrantResult(false, result.message(), granted);
|
||||||
}
|
}
|
||||||
granted.add(rewardDisplayName(permissionReward));
|
granted.add(rewardDisplayName(permissionReward));
|
||||||
}
|
}
|
||||||
case CommandRewardDefinition commandReward -> {
|
case RankRewardDefinition rankReward -> {
|
||||||
executeCommandReward(player, commandReward);
|
GrantResult rankResult = grantRankReward(player, rankReward);
|
||||||
granted.add(rewardDisplayName(commandReward));
|
if (!rankResult.success()) {
|
||||||
}
|
return new GrantResult(false, rankResult.message(), granted);
|
||||||
|
}
|
||||||
|
granted.add(rewardDisplayName(rankReward));
|
||||||
|
}
|
||||||
|
case CommandRewardDefinition commandReward -> {
|
||||||
|
executeCommandReward(player, commandReward);
|
||||||
|
granted.add(rewardDisplayName(commandReward));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +127,12 @@ public final class RewardService {
|
|||||||
switch (reward) {
|
switch (reward) {
|
||||||
case ItemRewardDefinition itemReward -> lines.add(Text.literal("Reward: " + itemReward.amount() + "x " + rewardDisplayName(itemReward)).formatted(Formatting.GRAY));
|
case ItemRewardDefinition itemReward -> lines.add(Text.literal("Reward: " + itemReward.amount() + "x " + rewardDisplayName(itemReward)).formatted(Formatting.GRAY));
|
||||||
case EffectRewardDefinition effectReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(effectReward) + " for " + effectReward.durationSeconds() + "s").formatted(Formatting.GRAY));
|
case EffectRewardDefinition effectReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(effectReward) + " for " + effectReward.durationSeconds() + "s").formatted(Formatting.GRAY));
|
||||||
case PermissionRewardDefinition permissionReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(permissionReward)).formatted(Formatting.GRAY));
|
case PermissionRewardDefinition permissionReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(permissionReward)).formatted(Formatting.GRAY));
|
||||||
case CommandRewardDefinition commandReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(commandReward)).formatted(Formatting.GRAY));
|
case RankRewardDefinition rankReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(rankReward)).formatted(Formatting.GRAY));
|
||||||
}
|
case CommandRewardDefinition commandReward -> lines.add(Text.literal("Reward: " + rewardDisplayName(commandReward)).formatted(Formatting.GRAY));
|
||||||
}
|
}
|
||||||
return lines;
|
}
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean supportsCustomAmount(List<RewardDefinition> rewards) {
|
public boolean supportsCustomAmount(List<RewardDefinition> rewards) {
|
||||||
@@ -215,12 +232,19 @@ public final class RewardService {
|
|||||||
return reward.effectId();
|
return reward.effectId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String rewardDisplayName(PermissionRewardDefinition reward) {
|
private String rewardDisplayName(PermissionRewardDefinition reward) {
|
||||||
if (reward.displayName() != null && !reward.displayName().isBlank()) {
|
if (reward.displayName() != null && !reward.displayName().isBlank()) {
|
||||||
return reward.displayName();
|
return reward.displayName();
|
||||||
}
|
}
|
||||||
return "permission " + reward.node();
|
return "permission " + reward.node();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String rewardDisplayName(RankRewardDefinition reward) {
|
||||||
|
if (reward.displayName() != null && !reward.displayName().isBlank()) {
|
||||||
|
return reward.displayName();
|
||||||
|
}
|
||||||
|
return "rank command";
|
||||||
|
}
|
||||||
|
|
||||||
private String rewardDisplayName(CommandRewardDefinition reward) {
|
private String rewardDisplayName(CommandRewardDefinition reward) {
|
||||||
if (reward.displayName() != null && !reward.displayName().isBlank()) {
|
if (reward.displayName() != null && !reward.displayName().isBlank()) {
|
||||||
@@ -245,17 +269,37 @@ public final class RewardService {
|
|||||||
return Registries.ITEM.get(identifier);
|
return Registries.ITEM.get(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RegistryEntry<StatusEffect> resolveStatusEffect(String effectId) {
|
private RegistryEntry<StatusEffect> resolveStatusEffect(String effectId) {
|
||||||
Identifier identifier = Identifier.tryParse(effectId);
|
Identifier identifier = Identifier.tryParse(effectId);
|
||||||
if (identifier == null) {
|
if (identifier == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return Registries.STATUS_EFFECT.getEntry(identifier).orElse(null);
|
return Registries.STATUS_EFFECT.getEntry(identifier).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GrantResult grantRankReward(ServerPlayerEntity player, RankRewardDefinition reward) {
|
||||||
|
String playerKey = player.getUuidAsString();
|
||||||
|
int currentPriority = permissionService.dataStore().data().grantedRankPriorities().getOrDefault(playerKey, Integer.MIN_VALUE);
|
||||||
|
if (currentPriority >= reward.priority()) {
|
||||||
|
return new GrantResult(false, "You already own an equal or higher rank.", List.of());
|
||||||
|
}
|
||||||
|
permissionService.dataStore().data().grantedRankPriorities().put(playerKey, reward.priority());
|
||||||
|
try {
|
||||||
|
permissionService.dataStore().save();
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new UncheckedIOException("Failed to persist rank reward data.", exception);
|
||||||
|
}
|
||||||
|
executeCommandReward(player, new CommandRewardDefinition(
|
||||||
|
reward.command(),
|
||||||
|
reward.runAsConsole(),
|
||||||
|
reward.displayName()
|
||||||
|
));
|
||||||
|
return new GrantResult(true, "Rank command executed successfully.", List.of(reward.command()));
|
||||||
|
}
|
||||||
|
|
||||||
public record ValidationResult(boolean success, String message) {
|
public record ValidationResult(boolean success, String message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record GrantResult(boolean success, String message, List<String> grantedRewards) {
|
public record GrantResult(boolean success, String message, List<String> grantedRewards) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.g2806.soulsteal.shop;
|
||||||
|
|
||||||
|
/** Rank reward backed by a command and a track priority. */
|
||||||
|
public record RankRewardDefinition(String command, int priority, boolean runAsConsole, String displayName) implements RewardDefinition {
|
||||||
|
@Override
|
||||||
|
public RewardType type() {
|
||||||
|
return RewardType.RANK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.g2806.soulsteal.shop;
|
package com.g2806.soulsteal.shop;
|
||||||
|
|
||||||
/** Marker interface for all shop reward definitions. */
|
/** Marker interface for all shop reward definitions. */
|
||||||
public sealed interface RewardDefinition permits CommandRewardDefinition, EffectRewardDefinition, ItemRewardDefinition, PermissionRewardDefinition {
|
public sealed interface RewardDefinition permits CommandRewardDefinition, EffectRewardDefinition, ItemRewardDefinition, PermissionRewardDefinition, RankRewardDefinition {
|
||||||
RewardType type();
|
RewardType type();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.g2806.soulsteal.shop;
|
package com.g2806.soulsteal.shop;
|
||||||
|
|
||||||
/** Supported reward types that can be granted by the soul shop. */
|
/** Supported reward types that can be granted by the soul shop. */
|
||||||
public enum RewardType {
|
public enum RewardType {
|
||||||
ITEM,
|
ITEM,
|
||||||
PERMISSION,
|
PERMISSION,
|
||||||
EFFECT,
|
EFFECT,
|
||||||
COMMAND
|
COMMAND,
|
||||||
}
|
RANK
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package com.g2806.soulsteal.shop;
|
package com.g2806.soulsteal.shop;
|
||||||
|
|
||||||
import com.g2806.soulsteal.config.SoulStealConfig.ShopUiConfig;
|
import com.g2806.soulsteal.config.SoulStealConfig.PermissionConfig;
|
||||||
|
import com.g2806.soulsteal.config.SoulStealConfig.ShopUiConfig;
|
||||||
import com.g2806.soulsteal.config.YamlConfigHelper;
|
import com.g2806.soulsteal.config.YamlConfigHelper;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/** Parsed representation of the editable shop catalog. */
|
/** Parsed representation of the editable shop catalog. */
|
||||||
public record ShopCatalog(String title, int rows, String fillerItemId, List<ShopCategoryDefinition> categories) {
|
public record ShopCatalog(String title, int rows, String fillerItemId, List<ShopCategoryDefinition> categories) {
|
||||||
public static ShopCatalog fromMap(Map<String, Object> root, ShopUiConfig shopUi) {
|
public static ShopCatalog fromMap(Map<String, Object> root, ShopUiConfig shopUi, PermissionConfig permissionConfig) {
|
||||||
Map<String, Object> categoriesSection = YamlConfigHelper.section(root, "categories");
|
Map<String, Object> categoriesSection = YamlConfigHelper.section(root, "categories");
|
||||||
List<ShopCategoryDefinition> categories = new ArrayList<>();
|
List<ShopCategoryDefinition> categories = new ArrayList<>();
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> itemMap = toStringMap(rawItemMap);
|
Map<String, Object> itemMap = toStringMap(rawItemMap);
|
||||||
List<RewardDefinition> rewards = parseRewards(YamlConfigHelper.list(itemMap, "rewards"));
|
List<RewardDefinition> rewards = parseRewards(YamlConfigHelper.list(itemMap, "rewards"), permissionConfig.luckpermsEnabled());
|
||||||
if (rewards.isEmpty()) {
|
if (rewards.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ShopCatalog(shopUi.title(), shopUi.rows(), shopUi.fillerItemId(), categories);
|
return new ShopCatalog(shopUi.title(), shopUi.rows(), shopUi.fillerItemId(), categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<ShopCategoryDefinition> category(String key) {
|
public Optional<ShopCategoryDefinition> category(String key) {
|
||||||
@@ -111,11 +112,11 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
stack_mode: ADD_DURATION
|
stack_mode: ADD_DURATION
|
||||||
name: "Speed Boost"
|
name: "Speed Boost"
|
||||||
|
|
||||||
unlocks:
|
unlocks:
|
||||||
name: "Unlocks"
|
name: "Unlocks"
|
||||||
icon: "minecraft:nether_star"
|
icon: "minecraft:nether_star"
|
||||||
items:
|
items:
|
||||||
nickname_access:
|
nickname_access:
|
||||||
slot: 13
|
slot: 13
|
||||||
icon: "minecraft:name_tag"
|
icon: "minecraft:name_tag"
|
||||||
name: "Nickname Access"
|
name: "Nickname Access"
|
||||||
@@ -124,17 +125,33 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
- "Requires LuckPerms for external permissions."
|
- "Requires LuckPerms for external permissions."
|
||||||
cost: 1000
|
cost: 1000
|
||||||
repeatable: false
|
repeatable: false
|
||||||
rewards:
|
rewards:
|
||||||
- type: permission
|
- type: permission
|
||||||
node: "example.nick"
|
node: "example.nick"
|
||||||
value: true
|
value: true
|
||||||
store_fallback: true
|
store_fallback: true
|
||||||
name: "Nickname Permission"
|
name: "Nickname Permission"
|
||||||
|
|
||||||
utility_commands:
|
vip_rank:
|
||||||
name: "Command Hooks"
|
slot: 14
|
||||||
icon: "minecraft:command_block"
|
icon: "minecraft:diamond_chestplate"
|
||||||
items:
|
name: "VIP Rank"
|
||||||
|
description:
|
||||||
|
- "Runs a command when bought."
|
||||||
|
- "Ranks are tracked by priority so lower ranks cannot overwrite higher ones."
|
||||||
|
cost: 5000
|
||||||
|
repeatable: false
|
||||||
|
rewards:
|
||||||
|
- type: rank
|
||||||
|
priority: 20
|
||||||
|
run_as_console: true
|
||||||
|
command: "lp user %player% parent add vip"
|
||||||
|
name: "VIP Rank"
|
||||||
|
|
||||||
|
utility_commands:
|
||||||
|
name: "Command Hooks"
|
||||||
|
icon: "minecraft:command_block"
|
||||||
|
items:
|
||||||
starter_crate:
|
starter_crate:
|
||||||
slot: 15
|
slot: 15
|
||||||
icon: "minecraft:chest"
|
icon: "minecraft:chest"
|
||||||
@@ -152,7 +169,7 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<RewardDefinition> parseRewards(List<Object> rawRewards) {
|
private static List<RewardDefinition> parseRewards(List<Object> rawRewards, boolean luckPermsEnabled) {
|
||||||
List<RewardDefinition> rewards = new ArrayList<>();
|
List<RewardDefinition> rewards = new ArrayList<>();
|
||||||
|
|
||||||
for (Object rewardValue : rawRewards) {
|
for (Object rewardValue : rawRewards) {
|
||||||
@@ -176,16 +193,27 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
Math.max(1, YamlConfigHelper.intValue(rewardMap, "amount", 1)),
|
Math.max(1, YamlConfigHelper.intValue(rewardMap, "amount", 1)),
|
||||||
rewardName
|
rewardName
|
||||||
));
|
));
|
||||||
case PERMISSION -> rewards.add(new PermissionRewardDefinition(
|
case PERMISSION -> rewards.add(new PermissionRewardDefinition(
|
||||||
YamlConfigHelper.string(rewardMap, "node", "soulsteal.example"),
|
YamlConfigHelper.string(rewardMap, "node", "soulsteal.example"),
|
||||||
YamlConfigHelper.bool(rewardMap, "value", true),
|
YamlConfigHelper.bool(rewardMap, "value", true),
|
||||||
YamlConfigHelper.bool(rewardMap, "store_fallback", true),
|
YamlConfigHelper.bool(rewardMap, "store_fallback", true),
|
||||||
rewardName
|
rewardName
|
||||||
));
|
));
|
||||||
case EFFECT -> {
|
case RANK -> {
|
||||||
StackMode stackMode;
|
if (!luckPermsEnabled) {
|
||||||
try {
|
continue;
|
||||||
stackMode = StackMode.valueOf(YamlConfigHelper.string(rewardMap, "stack_mode", StackMode.REPLACE.name()).toUpperCase(Locale.ROOT));
|
}
|
||||||
|
rewards.add(new RankRewardDefinition(
|
||||||
|
YamlConfigHelper.string(rewardMap, "command", "say %player% purchased a rank."),
|
||||||
|
Math.max(0, YamlConfigHelper.intValue(rewardMap, "priority", 0)),
|
||||||
|
YamlConfigHelper.bool(rewardMap, "run_as_console", true),
|
||||||
|
rewardName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
case EFFECT -> {
|
||||||
|
StackMode stackMode;
|
||||||
|
try {
|
||||||
|
stackMode = StackMode.valueOf(YamlConfigHelper.string(rewardMap, "stack_mode", StackMode.REPLACE.name()).toUpperCase(Locale.ROOT));
|
||||||
} catch (IllegalArgumentException ignored) {
|
} catch (IllegalArgumentException ignored) {
|
||||||
stackMode = StackMode.REPLACE;
|
stackMode = StackMode.REPLACE;
|
||||||
}
|
}
|
||||||
@@ -226,4 +254,4 @@ public record ShopCatalog(String title, int rows, String fillerItemId, List<Shop
|
|||||||
}
|
}
|
||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.g2806.soulsteal;
|
||||||
|
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.util.math.Direction;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class SoulStealModTest {
|
||||||
|
@Test
|
||||||
|
void placedBlockPositionUsesClickedFaceOffset() {
|
||||||
|
BlockPos clicked = new BlockPos(10, 64, -3);
|
||||||
|
|
||||||
|
assertEquals(new BlockPos(10, 65, -3), SoulStealMod.placedBlockPos(clicked, Direction.UP));
|
||||||
|
assertEquals(new BlockPos(10, 63, -3), SoulStealMod.placedBlockPos(clicked, Direction.DOWN));
|
||||||
|
assertEquals(new BlockPos(11, 64, -3), SoulStealMod.placedBlockPos(clicked, Direction.EAST));
|
||||||
|
assertEquals(new BlockPos(9, 64, -3), SoulStealMod.placedBlockPos(clicked, Direction.WEST));
|
||||||
|
assertEquals(new BlockPos(10, 64, -4), SoulStealMod.placedBlockPos(clicked, Direction.NORTH));
|
||||||
|
assertEquals(new BlockPos(10, 64, -2), SoulStealMod.placedBlockPos(clicked, Direction.SOUTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.g2806.soulsteal.contract;
|
||||||
|
|
||||||
|
import com.g2806.soulsteal.config.SoulStealConfig;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ContractCatalogTest {
|
||||||
|
@Test
|
||||||
|
void parsesCooldownAndDisplayMatches() {
|
||||||
|
Map<String, Object> root = Map.of(
|
||||||
|
"contracts", Map.of(
|
||||||
|
"mining", List.of(Map.of(
|
||||||
|
"id", "iron_miner",
|
||||||
|
"type", "mining",
|
||||||
|
"name", "Iron Miner",
|
||||||
|
"targets", List.of("minecraft:iron_ore", "minecraft:deepslate_iron_ore"),
|
||||||
|
"matches", List.of("Iron Ore", "Deepslate Iron Ore"),
|
||||||
|
"target_name", "Iron Ore",
|
||||||
|
"amount", 20,
|
||||||
|
"reward", 200,
|
||||||
|
"repeatable", true,
|
||||||
|
"cooldown", 10
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SoulStealConfig.ContractConfig config = new SoulStealConfig.ContractConfig(true, true, new SoulStealConfig.ContractHudConfig(true, "Active Contract"), 0L);
|
||||||
|
ContractCatalog catalog = ContractCatalog.fromMap(root, config);
|
||||||
|
|
||||||
|
ContractDefinition contract = catalog.contract("iron_miner").orElseThrow();
|
||||||
|
assertEquals(10L, contract.cooldownSeconds());
|
||||||
|
assertEquals(List.of("Iron Ore", "Deepslate Iron Ore"), contract.displayMatches());
|
||||||
|
assertEquals(2, contract.targetIds().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.g2806.soulsteal.service;
|
||||||
|
|
||||||
|
import com.g2806.soulsteal.contract.ContractCatalog;
|
||||||
|
import com.g2806.soulsteal.contract.ContractDefinition;
|
||||||
|
import com.g2806.soulsteal.contract.ContractType;
|
||||||
|
import com.g2806.soulsteal.data.SoulStealDataStore;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ContractServiceTest {
|
||||||
|
@Test
|
||||||
|
void repeatableContractIsBlockedUntilCooldownExpires() throws Exception {
|
||||||
|
SoulStealDataStore dataStore = newDataStore();
|
||||||
|
SoulService soulService = newSoulService(dataStore);
|
||||||
|
ContractDefinition contract = contract("repeatable", true, 30L, List.of("minecraft:iron_ore"));
|
||||||
|
ContractService service = new ContractService(() -> catalog(contract), dataStore, soulService);
|
||||||
|
|
||||||
|
UUID player = UUID.randomUUID();
|
||||||
|
Map<String, Long> cooldowns = new LinkedHashMap<>();
|
||||||
|
cooldowns.put("repeatable", System.currentTimeMillis() + 60_000L);
|
||||||
|
dataStore.data().contractCooldowns().put(player.toString(), cooldowns);
|
||||||
|
|
||||||
|
assertFalse(service.isContractAvailable(player, contract));
|
||||||
|
cooldowns = new LinkedHashMap<>();
|
||||||
|
cooldowns.put("repeatable", System.currentTimeMillis() - 1L);
|
||||||
|
dataStore.data().contractCooldowns().put(player.toString(), cooldowns);
|
||||||
|
assertTrue(service.isContractAvailable(player, contract));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completedOneTimeContractIsUnavailable() throws Exception {
|
||||||
|
SoulStealDataStore dataStore = newDataStore();
|
||||||
|
SoulService soulService = newSoulService(dataStore);
|
||||||
|
ContractDefinition contract = contract("one_time", false, 0L, List.of("minecraft:zombie"));
|
||||||
|
ContractService service = new ContractService(() -> catalog(contract), dataStore, soulService);
|
||||||
|
|
||||||
|
UUID player = UUID.randomUUID();
|
||||||
|
java.util.Set<String> completed = new java.util.HashSet<>();
|
||||||
|
completed.add("one_time");
|
||||||
|
dataStore.data().completedContracts().put(player.toString(), completed);
|
||||||
|
|
||||||
|
assertFalse(service.isContractAvailable(player, contract));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectedContractReadsFromPersistentData() throws Exception {
|
||||||
|
SoulStealDataStore dataStore = newDataStore();
|
||||||
|
SoulService soulService = newSoulService(dataStore);
|
||||||
|
ContractDefinition contract = contract("iron_miner", true, 0L, List.of("minecraft:iron_ore"));
|
||||||
|
ContractService service = new ContractService(() -> catalog(contract), dataStore, soulService);
|
||||||
|
|
||||||
|
UUID player = UUID.randomUUID();
|
||||||
|
dataStore.data().selectedContracts().put(player.toString(), "iron_miner");
|
||||||
|
|
||||||
|
assertTrue(service.selectedContract(player).isPresent());
|
||||||
|
assertEquals("iron_miner", service.selectedContract(player).orElseThrow().id());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContractDefinition contract(String id, boolean repeatable, long cooldownSeconds, List<String> targets) {
|
||||||
|
return new ContractDefinition(id, id, "minecraft:iron_pickaxe", ContractType.MINING, targets, List.of(), "Iron Ore", "", 20L, 250L, repeatable, cooldownSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContractCatalog catalog(ContractDefinition contract) {
|
||||||
|
return new ContractCatalog(true, true, true, "Active Contract", List.of(contract));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SoulStealDataStore newDataStore() throws Exception {
|
||||||
|
Path dir = Files.createTempDirectory("soulsteal-contract-test");
|
||||||
|
SoulStealDataStore store = new SoulStealDataStore(dir);
|
||||||
|
store.load();
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SoulService newSoulService(SoulStealDataStore dataStore) {
|
||||||
|
return new SoulService(() -> new com.g2806.soulsteal.config.SoulStealConfig(
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.EconomyConfig(0L, 1_000_000L, 25L,
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.DeathPenaltyConfig(15L, 0.10D, 5L, 100L),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.TransferConfig(true, 1L)),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.BountyConfig(true, 25L, 10_000L, 7_200L, 600L, 86_400L, 0.50D, 60L, 5, 3),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.TrackerConfig(true, 900L, 20, false),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.ContractConfig(true, true, new com.g2806.soulsteal.config.SoulStealConfig.ContractHudConfig(true, "Active Contract"), 0L),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.ShopUiConfig("Soul Shop", 3, "minecraft:black_stained_glass_pane", 0L, true, 64),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.HudConfig(
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.ScoreboardConfig(true, false, "Soul HUD"),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.BountyBossbarConfig(true, "Bounty on You"),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.LeaderboardConfig(10)
|
||||||
|
),
|
||||||
|
new com.g2806.soulsteal.config.SoulStealConfig.PermissionConfig("admin", "reload", "shop", "bounty", true, "balance", "set", "add", "take", "scoreboard", "leaderboard")
|
||||||
|
), dataStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.g2806.soulsteal.shop;
|
||||||
|
|
||||||
|
import com.g2806.soulsteal.config.SoulStealConfig;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ShopCatalogTest {
|
||||||
|
@Test
|
||||||
|
void filtersRankRewardsWhenLuckPermsIsDisabled() {
|
||||||
|
Map<String, Object> root = Map.of(
|
||||||
|
"categories", Map.of(
|
||||||
|
"ranks", Map.of(
|
||||||
|
"name", "Ranks",
|
||||||
|
"items", Map.of(
|
||||||
|
"vip", Map.of(
|
||||||
|
"name", "VIP",
|
||||||
|
"cost", 1000,
|
||||||
|
"repeatable", false,
|
||||||
|
"rewards", List.of(Map.of(
|
||||||
|
"type", "rank",
|
||||||
|
"priority", 20,
|
||||||
|
"run_as_console", true,
|
||||||
|
"command", "lp user %player% parent add vip",
|
||||||
|
"name", "VIP Rank"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SoulStealConfig.ShopUiConfig shopUi = new SoulStealConfig.ShopUiConfig("Shop", 3, "minecraft:glass", 0L, true, 64);
|
||||||
|
SoulStealConfig.PermissionConfig permissions = new SoulStealConfig.PermissionConfig("admin", "reload", "shop", "bounty", false, "balance", "set", "add", "take", "scoreboard", "leaderboard");
|
||||||
|
|
||||||
|
ShopCatalog catalog = ShopCatalog.fromMap(root, shopUi, permissions);
|
||||||
|
assertTrue(catalog.category("ranks").orElseThrow().entries().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesRankRewardsWhenLuckPermsIsEnabled() {
|
||||||
|
Map<String, Object> root = Map.of(
|
||||||
|
"categories", Map.of(
|
||||||
|
"ranks", Map.of(
|
||||||
|
"name", "Ranks",
|
||||||
|
"items", Map.of(
|
||||||
|
"vip", Map.of(
|
||||||
|
"name", "VIP",
|
||||||
|
"cost", 1000,
|
||||||
|
"repeatable", false,
|
||||||
|
"rewards", List.of(Map.of(
|
||||||
|
"type", "rank",
|
||||||
|
"priority", 20,
|
||||||
|
"run_as_console", true,
|
||||||
|
"command", "lp user %player% parent add vip",
|
||||||
|
"name", "VIP Rank"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SoulStealConfig.ShopUiConfig shopUi = new SoulStealConfig.ShopUiConfig("Shop", 3, "minecraft:glass", 0L, true, 64);
|
||||||
|
SoulStealConfig.PermissionConfig permissions = new SoulStealConfig.PermissionConfig("admin", "reload", "shop", "bounty", true, "balance", "set", "add", "take", "scoreboard", "leaderboard");
|
||||||
|
|
||||||
|
ShopCatalog catalog = ShopCatalog.fromMap(root, shopUi, permissions);
|
||||||
|
ShopEntryDefinition entry = catalog.category("ranks").orElseThrow().entries().get(0);
|
||||||
|
assertEquals(1, entry.rewards().size());
|
||||||
|
assertInstanceOf(RankRewardDefinition.class, entry.rewards().get(0));
|
||||||
|
RankRewardDefinition reward = (RankRewardDefinition) entry.rewards().get(0);
|
||||||
|
assertEquals(20, reward.priority());
|
||||||
|
assertEquals("lp user %player% parent add vip", reward.command());
|
||||||
|
assertTrue(reward.runAsConsole());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.g2806.soulsteal.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class DurationFormatterTest {
|
||||||
|
@Test
|
||||||
|
void formatsSecondsIntoReadableUnits() {
|
||||||
|
assertEquals("10s", DurationFormatter.formatSeconds(10));
|
||||||
|
assertEquals("10m", DurationFormatter.formatSeconds(600));
|
||||||
|
assertEquals("2h", DurationFormatter.formatSeconds(7_200));
|
||||||
|
assertEquals("1d 3h", DurationFormatter.formatSeconds(97_200));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user