Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions src/main/java/net/tfminecraft/armourshop/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public class Cache {
/** GunsAndGadgets skins.yml (Step 15). */
public static String gunsSkinsYmlPath = "";

/**
* RPCharacters custom mask registry. Blank config falls back to this path
* so an existing live config.yml still registers masks.
*/
public static String masksYmlPath = "plugins/RPCharacters/custom-masks.yml";

/** Daily force pull+reload at HH:mm server local; blank disables. */
public static String forceReloadTime = "06:00";
/** Seconds between iareload and iazip. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public void load(File configFile) {
String gunsSkins = config.getString("pack-apply.guns-skins-yml", "");
Cache.gunsSkinsYmlPath = gunsSkins == null ? "" : gunsSkins.trim();

String masksYml = config.getString("pack-apply.masks-yml", "");
if (masksYml == null || masksYml.isBlank()) {
masksYml = "plugins/RPCharacters/custom-masks.yml";
}
Cache.masksYmlPath = masksYml.trim();

String forceTime = config.getString("pack-apply.force-reload-time", "06:00");
Cache.forceReloadTime = forceTime == null ? "" : forceTime.trim();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import net.tfminecraft.armourshop.pack.writer.flat.FlatItemWriter;
import net.tfminecraft.armourshop.pack.writer.gun.GunWriter;
import net.tfminecraft.armourshop.pack.writer.large.LargeHandheldWriter;
import net.tfminecraft.armourshop.pack.writer.mask.MasksYml;
import net.tfminecraft.armourshop.pack.writer.model3d.Item3dWriter;
import net.tfminecraft.armourshop.pack.writer.model3d.ShieldWriter;
import java.nio.file.Path;
Expand Down Expand Up @@ -479,6 +480,15 @@ private static void writeKind(
ns
);
return;
case "mask":
requireModel3d(packFiles);
Item3dWriter.writeMask(
contentsRoot,
new PackSubmission(slug, display, PackKind.MASK, packFiles),
ns
);
MasksYml.upsert(requireMasksYml(), slug, ns);
return;
case "gun":
requireGun(packFiles);
GunWriter.write(
Expand Down Expand Up @@ -562,6 +572,16 @@ private static void requireGun(Map<String, byte[]> files) {
}
}

static Path requireMasksYml() {
String masks = Cache.masksYmlPath;
if (masks == null || masks.isBlank()) {
throw new IllegalStateException(
"pack-apply.masks-yml is not set in config.yml"
);
}
return Path.of(masks.trim());
}

static Path requireGunsSkinsYml() {
String skins = Cache.gunsSkinsYmlPath;
if (skins == null || skins.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.tfminecraft.armourshop.pack.model.PackPaths;
import net.tfminecraft.armourshop.pack.writer.flat.BookWriter;
import net.tfminecraft.armourshop.pack.writer.gun.GunWriter;
import net.tfminecraft.armourshop.pack.writer.mask.MasksYml;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -80,6 +81,15 @@ public static List<Path> remove(
removeArmorPack(contentsRoot, ns, s, removed);
} else if ("gun".equals(k)) {
removed.addAll(GunWriter.remove(contentsRoot, ns, s, gunsSkinsYmlOrNull()));
} else if ("mask".equals(k)) {
removeNonArmorPack(contentsRoot, ns, s, removed);
try {
MasksYml.remove(masksYmlOrNull(), s);
} catch (RuntimeException e) {
if (log != null) {
log.warning("[pack-delete] mask registry remove failed: " + e.getMessage());
}
}
} else if ("book".equals(k)) {
removed.addAll(BookWriter.remove(contentsRoot, ns, s));
} else {
Expand Down Expand Up @@ -156,6 +166,14 @@ private static void deleteQuiet(Path path, List<Path> removed) throws IOExceptio
}
}

private static Path masksYmlOrNull() {
String masks = Cache.masksYmlPath;
if (masks == null || masks.isBlank()) {
return null;
}
return Path.of(masks.trim());
}

private static Path gunsSkinsYmlOrNull() {
String skins = Cache.gunsSkinsYmlPath;
if (skins == null || skins.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum PackKind {
ITEM_3D,
SHIELD,
HELMET_3D,
MASK,
GUN,
BOOK
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ private static boolean isItemKind(String kind) {
|| "item_3d".equals(kind)
|| "shield".equals(kind)
|| "helmet_3d".equals(kind)
|| "mask".equals(kind)
|| "gun".equals(kind)
|| "book".equals(kind);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package net.tfminecraft.armourshop.pack.writer.mask;

import net.tfminecraft.armourshop.pack.util.YamlUtil;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Upsert / remove RPCharacters {@code custom-masks.yml} entries (Bukkit-free).
* Each entry is the ItemsAdder path of an approved mask skin.
*/
public final class MasksYml {

private MasksYml() {}

public static void upsert(Path masksYml, String slug, String namespace)
throws IOException
{
if (masksYml == null) {
throw new IllegalArgumentException("masksYml is null");
}
String key = requireSlug(slug);
String item = "ia." + requireNamespace(namespace) + ":" + key;
Path parent = masksYml.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
Map<String, String> entries = read(masksYml);
entries.put(key, item);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
write(masksYml, entries);
}

public static boolean remove(Path masksYml, String slug) throws IOException {
if (masksYml == null || !Files.isRegularFile(masksYml)) {
return false;
}
String key = requireSlug(slug);
Map<String, String> entries = read(masksYml);
if (entries.remove(key) == null) {
return false;
}
write(masksYml, entries);
return true;
}

static Map<String, String> read(Path masksYml) throws IOException {
Map<String, String> entries = new LinkedHashMap<>();
if (masksYml == null || !Files.isRegularFile(masksYml)) {
return entries;
}
String current = null;
for (String raw : Files.readString(masksYml, StandardCharsets.UTF_8).split("\\R")) {
String line = raw.stripTrailing();
if (line.startsWith(" ") && !line.startsWith(" ") && line.endsWith(":")) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
String key = line.substring(2, line.length() - 1).trim();
current = key.isEmpty() ? null : key;
continue;
}
if (current != null && line.startsWith(" item:")) {
String item = line.substring(" item:".length()).trim();
if (!item.isEmpty()) {
entries.put(current, item);
}
current = null;
}
}
return entries;
}

private static void write(Path masksYml, Map<String, String> entries) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("# Custom RP masks written by ArmourShop. Loaded by RPCharacters with masks.yml.\n");
sb.append("masks:\n");
for (Map.Entry<String, String> entry : entries.entrySet()) {
sb.append(" ").append(entry.getKey()).append(":\n");
sb.append(" item: ").append(entry.getValue()).append('\n');
}
Files.writeString(masksYml, sb.toString(), StandardCharsets.UTF_8);
Comment thread
Drefvelin marked this conversation as resolved.
Outdated
}

private static String requireSlug(String slug) {
if (slug == null || slug.isBlank()) {
throw new IllegalArgumentException("missing slug");
}
String key = slug.trim();
YamlUtil.validateSlug(key);
return key;
}

private static String requireNamespace(String namespace) {
if (namespace == null || namespace.isBlank()) {
throw new IllegalArgumentException("missing namespace");
}
String ns = namespace.trim();
for (int i = 0; i < ns.length(); i++) {
char c = ns.charAt(i);
boolean ok = (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| c == '_'
|| c == '-';
if (!ok) {
throw new IllegalArgumentException("invalid namespace: " + namespace);
}
}
return ns;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,30 @@ public static List<Path> writeHelmet3d(
);
}

/**
* Writes mask skins as an unplaceable carved pumpkin hat, same pack shape as helmet_3d.
* RPCharacters identity comes from the mask registry, not from this YAML.
*/
public static List<Path> writeMask(
Path contentsRoot,
PackSubmission submission,
String namespace
) throws IOException {
if (submission.kind() != PackKind.MASK) {
throw new IllegalArgumentException(
"writeMask requires MASK, got " + submission.kind()
);
}
return writeModelItem(
contentsRoot,
submission,
"CARVED_PUMPKIN",
null,
true,
namespace
);
}

/**
* Shared pack write for standalone 3D items (item_3d / helmet_3d).
*
Expand Down
13 changes: 13 additions & 0 deletions src/main/resources/base-sets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,19 @@ helmets:
- helmets.infantry_mythril_helmet
- helmets.infantry_custom_helmet

masks:
item:
- masks.ghost_mask
- masks.monster_mask
- masks.oni_mask
- masks.urukhai_mask
- masks.samurai_mask
- masks.steampunk_mask
- masks.steampunk_robot_mask
- masks.skeleton_mask
- masks.skeleton_mask2
- masks.zerratoris_mask

rifles:
item:
- guns.rifle_template
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/categories.yml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ i_helmets:
item: m.helmets.shako
is-item: true

i_masks:
name: "Masks"
colour: "#7f7d80"
item: m.masks.ghost_mask
is-item: true

i_guns:
name: "Guns"
colour: "#7f7d80"
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pack-apply:
categories-path: "plugins/ArmourShop/Categories"
# GunsAndGadgets skins.yml (Step 15 — IA ids; no CMD dual-write)
guns-skins-yml: "plugins/GunsAndGadgets/skins.yml"
# RPCharacters custom mask registry (mask skin kind)
masks-yml: "plugins/RPCharacters/custom-masks.yml"
# Daily force pull + IA refresh at this server-local time (HH:mm). Blank disables.
force-reload-time: "06:00"
# Seconds to wait after /iareload before /iazip
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/permission-groups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ groups:
display-name: "&#8939EE&lA&#9033DD&ls&#972DCC&lc&#9E27BB&le&#A620AB&ln&#AD1A9A&ld&#B41489&le&#BB0E78&ld&#C20867"
name-colour-stops: 20
skin-token-cooldown-days: 14
skin-kinds: [item_3d, shield, helmet_3d, gun]
skin-kinds: [item_3d, shield, helmet_3d, mask, gun]
allow-armor-3d-helmet: true
# Uncomment to raise the combined texture+model budget for this rank:
# max-3d-pair-bytes: 40960
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package net.tfminecraft.armourshop.pack.writer.mask;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class MasksYmlTest {

@Test
void upsertAndRemoveKeepOtherMasks(@TempDir Path dir) throws Exception {
Path file = dir.resolve("custom-masks.yml");
MasksYml.upsert(file, "oni_mask", "tfmc_submissions");
MasksYml.upsert(file, "ghost_mask", "tfmc_armorshop");

assertEquals(
"ia.tfmc_submissions:oni_mask",
MasksYml.read(file).get("oni_mask")
);
assertEquals(
"ia.tfmc_armorshop:ghost_mask",
MasksYml.read(file).get("ghost_mask")
);

assertTrue(MasksYml.remove(file, "oni_mask"));
assertFalse(MasksYml.read(file).containsKey("oni_mask"));
assertEquals(
"ia.tfmc_armorshop:ghost_mask",
MasksYml.read(file).get("ghost_mask")
);
assertFalse(MasksYml.remove(file, "oni_mask"));
}

@Test
void rewriteReplacesTheSameSlug(@TempDir Path dir) throws Exception {
Path file = dir.resolve("custom-masks.yml");
MasksYml.upsert(file, "oni_mask", "tfmc_submissions");
MasksYml.upsert(file, "oni_mask", "tfmc_armorshop");
assertEquals(1, MasksYml.read(file).size());
assertEquals(
"ia.tfmc_armorshop:oni_mask",
MasksYml.read(file).get("oni_mask")
);
assertTrue(Files.readString(file).contains("masks:"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.tfminecraft.armourshop.pack.writer.model3d;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;

import org.junit.jupiter.api.Test;

import net.tfminecraft.armourshop.pack.model.PackKind;
import net.tfminecraft.armourshop.pack.model.PackSubmission;

class Item3dWriterMaskTest {

@Test
void maskYamlIsAnUnplaceableHat() {
String yaml = Item3dWriter.buildYaml(
new PackSubmission("oni_mask", "Oni", PackKind.MASK, Map.of()),
"CARVED_PUMPKIN",
null,
true,
"tfmc_submissions"
);
assertTrue(yaml.contains("material: CARVED_PUMPKIN"));
assertTrue(yaml.contains("generate: false"));
assertTrue(yaml.contains("hat: true"));
assertTrue(yaml.contains("model_path: item/oni_mask"));
}
}