Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,412 @@
package com.tiedup.remake.v2.furniture;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.tiedup.remake.v2.BodyRegionV2;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import net.minecraft.resources.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
/**
* Parses JSON files into {@link FurnitureDefinition} instances.
*
* <p>Uses manual field extraction (not Gson deserialization) for strict
* validation control. Invalid required fields cause the entire definition
* to be rejected; optional fields use safe defaults.</p>
*
* <p>Expected JSON files in {@code data/<namespace>/tiedup_furniture/}.</p>
*/
public final class FurnitureParser {
private static final Logger LOGGER = LogManager.getLogger("TiedUpFurniture");
private static final String TAG = "[FurnitureParser]";
/** Strict hex color pattern: # followed by exactly 6 hex digits. */
private static final Pattern HEX_COLOR = Pattern.compile("^#[0-9A-Fa-f]{6}$");
/** Maximum number of seats per furniture (bitmask limit: 8 bits). */
private static final int MAX_SEATS = 8;
private FurnitureParser() {}
/**
* Parse a JSON input stream into a FurnitureDefinition.
*
* @param input the JSON input stream
* @param fileId the resource location of the source file (for error messages)
* @return the parsed definition, or null if the file is invalid
*/
@Nullable
public static FurnitureDefinition parse(InputStream input, ResourceLocation fileId) {
try {
JsonObject root = JsonParser.parseReader(
new InputStreamReader(input, StandardCharsets.UTF_8)
).getAsJsonObject();
return parseObject(root, fileId);
} catch (Exception e) {
LOGGER.error("{} Failed to parse JSON {}: {}", TAG, fileId, e.getMessage());
return null;
}
}
/**
* Parse a JsonObject into a FurnitureDefinition.
*
* @param root the parsed JSON object
* @param fileId the resource location of the source file (for error messages)
* @return the parsed definition, or null if validation fails
*/
@Nullable
public static FurnitureDefinition parseObject(JsonObject root, ResourceLocation fileId) {
// --- Required: id ---
String idStr = getStringOrNull(root, "id");
if (idStr == null || idStr.isEmpty()) {
LOGGER.error("{} Skipping {}: missing 'id'", TAG, fileId);
return null;
}
ResourceLocation id = ResourceLocation.tryParse(idStr);
if (id == null) {
LOGGER.error("{} Skipping {}: invalid id ResourceLocation '{}'", TAG, fileId, idStr);
return null;
}
// --- Required: display_name ---
String displayName = getStringOrNull(root, "display_name");
if (displayName == null || displayName.isEmpty()) {
LOGGER.error("{} Skipping {}: missing 'display_name'", TAG, fileId);
return null;
}
// --- Optional: translation_key ---
String translationKey = getStringOrNull(root, "translation_key");
// --- Required: model ---
String modelStr = getStringOrNull(root, "model");
if (modelStr == null || modelStr.isEmpty()) {
LOGGER.error("{} Skipping {}: missing 'model'", TAG, fileId);
return null;
}
ResourceLocation modelLocation = ResourceLocation.tryParse(modelStr);
if (modelLocation == null) {
LOGGER.error("{} Skipping {}: invalid model ResourceLocation '{}'", TAG, fileId, modelStr);
return null;
}
// --- Optional: tint_channels (strict hex validation) ---
Map<String, Integer> tintChannels = parseTintChannels(root, fileId);
if (tintChannels == null) {
// parseTintChannels returns null on invalid hex -> reject entire furniture
return null;
}
// --- Optional: supports_color (default false) ---
boolean supportsColor = getBooleanOrDefault(root, "supports_color", false);
// --- Optional: hitbox (defaults: 1.0 x 1.0, clamped [0.1, 5.0]) ---
float hitboxWidth = 1.0f;
float hitboxHeight = 1.0f;
if (root.has("hitbox") && root.get("hitbox").isJsonObject()) {
JsonObject hitbox = root.getAsJsonObject("hitbox");
hitboxWidth = clamp(getFloatOrDefault(hitbox, "width", 1.0f), 0.1f, 5.0f);
hitboxHeight = clamp(getFloatOrDefault(hitbox, "height", 1.0f), 0.1f, 5.0f);
}
// --- Optional: placement ---
boolean snapToWall = false;
boolean floorOnly = true;
if (root.has("placement") && root.get("placement").isJsonObject()) {
JsonObject placement = root.getAsJsonObject("placement");
snapToWall = getBooleanOrDefault(placement, "snap_to_wall", false);
floorOnly = getBooleanOrDefault(placement, "floor_only", true);
}
// --- Optional: lockable (default false) ---
boolean lockable = getBooleanOrDefault(root, "lockable", false);
// --- Optional: break_resistance (default 100, clamped [1, 10000]) ---
float breakResistance = clamp(getFloatOrDefault(root, "break_resistance", 100.0f), 1.0f, 10000.0f);
// --- Optional: drop_on_break (default true) ---
boolean dropOnBreak = getBooleanOrDefault(root, "drop_on_break", true);
// --- Required: seats (non-empty array, size [1, 8]) ---
if (!root.has("seats") || !root.get("seats").isJsonArray()) {
LOGGER.error("{} Skipping {}: missing or invalid 'seats' array", TAG, fileId);
return null;
}
JsonArray seatsArray = root.getAsJsonArray("seats");
if (seatsArray.isEmpty()) {
LOGGER.error("{} Skipping {}: 'seats' array is empty", TAG, fileId);
return null;
}
if (seatsArray.size() > MAX_SEATS) {
LOGGER.error("{} Skipping {}: 'seats' array has {} entries (max {})",
TAG, fileId, seatsArray.size(), MAX_SEATS);
return null;
}
List<SeatDefinition> seats = new ArrayList<>(seatsArray.size());
for (int i = 0; i < seatsArray.size(); i++) {
if (!seatsArray.get(i).isJsonObject()) {
LOGGER.error("{} Skipping {}: seats[{}] is not a JSON object", TAG, fileId, i);
return null;
}
SeatDefinition seat = parseSeat(seatsArray.get(i).getAsJsonObject(), i, lockable, fileId);
if (seat == null) {
// parseSeat already logged the error
return null;
}
seats.add(seat);
}
// --- Optional: feedback ---
FurnitureFeedback feedback = FurnitureFeedback.EMPTY;
if (root.has("feedback") && root.get("feedback").isJsonObject()) {
feedback = parseFeedback(root.getAsJsonObject("feedback"), fileId);
}
// --- Optional: category (default "furniture") ---
String category = getStringOrDefault(root, "category", "furniture");
// --- Optional: icon (item model ResourceLocation for inventory sprite) ---
ResourceLocation icon = parseOptionalResourceLocation(root, "icon", fileId);
return new FurnitureDefinition(
id, displayName, translationKey, modelLocation,
tintChannels, supportsColor,
hitboxWidth, hitboxHeight,
snapToWall, floorOnly,
lockable, breakResistance, dropOnBreak,
seats, feedback, category, icon
);
}
// ===== Seat Parsing =====
/**
* Parse a single seat JSON object.
*
* @param obj the seat JSON object
* @param index the seat index (for error messages)
* @param parentLockable the top-level lockable value (used as default)
* @param fileId the source file (for error messages)
* @return the parsed seat, or null on validation failure
*/
@Nullable
private static SeatDefinition parseSeat(JsonObject obj, int index,
boolean parentLockable,
ResourceLocation fileId) {
// Required: id (must not contain ':')
String seatId = getStringOrNull(obj, "id");
if (seatId == null || seatId.isEmpty()) {
LOGGER.error("{} Skipping {}: seats[{}] missing 'id'", TAG, fileId, index);
return null;
}
if (seatId.contains(":")) {
LOGGER.error("{} Skipping {}: seats[{}] id '{}' must not contain ':'",
TAG, fileId, index, seatId);
return null;
}
// Required: armature
String armature = getStringOrNull(obj, "armature");
if (armature == null || armature.isEmpty()) {
LOGGER.error("{} Skipping {}: seats[{}] missing 'armature'", TAG, fileId, index);
return null;
}
// Optional: blocked_regions (unknown region = fatal for entire furniture)
Set<BodyRegionV2> blockedRegions = parseBlockedRegions(obj, index, fileId);
if (blockedRegions == null) {
// parseBlockedRegions returns null ONLY on unknown region name (fatal)
return null;
}
// Optional: lockable (inherits from top-level)
boolean seatLockable = getBooleanOrDefault(obj, "lockable", parentLockable);
// Optional: locked_difficulty (clamped [1, 10000], default 1)
int lockedDifficulty = clampInt(getIntOrDefault(obj, "locked_difficulty", 1), 1, 10000);
// Optional: item_difficulty_bonus (default false)
boolean itemDifficultyBonus = getBooleanOrDefault(obj, "item_difficulty_bonus", false);
return new SeatDefinition(
seatId, armature, blockedRegions,
seatLockable, lockedDifficulty, itemDifficultyBonus
);
}
/**
* Parse blocked_regions for a seat. Returns empty set if field is absent.
* Returns null (fatal) if any region name is unknown.
*/
@Nullable
private static Set<BodyRegionV2> parseBlockedRegions(JsonObject obj, int seatIndex,
ResourceLocation fileId) {
if (!obj.has("blocked_regions") || !obj.get("blocked_regions").isJsonArray()) {
return Collections.unmodifiableSet(EnumSet.noneOf(BodyRegionV2.class));
}
JsonArray arr = obj.getAsJsonArray("blocked_regions");
if (arr.isEmpty()) {
return Collections.unmodifiableSet(EnumSet.noneOf(BodyRegionV2.class));
}
EnumSet<BodyRegionV2> regions = EnumSet.noneOf(BodyRegionV2.class);
for (JsonElement elem : arr) {
String name;
try {
name = elem.getAsString().toUpperCase();
} catch (Exception e) {
LOGGER.error("{} Skipping {}: seats[{}] invalid element in 'blocked_regions': {}",
TAG, fileId, seatIndex, e.getMessage());
return null;
}
BodyRegionV2 region = BodyRegionV2.fromName(name);
if (region == null) {
LOGGER.error("{} Skipping {}: seats[{}] unknown body region '{}'",
TAG, fileId, seatIndex, name);
return null;
}
regions.add(region);
}
return Collections.unmodifiableSet(regions);
}
// ===== Feedback Parsing =====
private static FurnitureFeedback parseFeedback(JsonObject obj, ResourceLocation fileId) {
return new FurnitureFeedback(
parseOptionalResourceLocation(obj, "mount_sound", fileId),
parseOptionalResourceLocation(obj, "lock_sound", fileId),
parseOptionalResourceLocation(obj, "unlock_sound", fileId),
parseOptionalResourceLocation(obj, "struggle_loop_sound", fileId),
parseOptionalResourceLocation(obj, "escape_sound", fileId),
parseOptionalResourceLocation(obj, "denied_sound", fileId)
);
}
// ===== Tint Channel Parsing =====
/**
* Parse tint_channels with strict hex validation.
* Returns empty map if field is absent. Returns null if any value is invalid hex.
*/
@Nullable
private static Map<String, Integer> parseTintChannels(JsonObject root, ResourceLocation fileId) {
if (!root.has("tint_channels") || !root.get("tint_channels").isJsonObject()) {
return Map.of();
}
JsonObject channels = root.getAsJsonObject("tint_channels");
Map<String, Integer> result = new LinkedHashMap<>();
for (Map.Entry<String, JsonElement> entry : channels.entrySet()) {
String hex;
try {
hex = entry.getValue().getAsString();
} catch (Exception e) {
LOGGER.error("{} Skipping {}: tint_channels '{}' value is not a string",
TAG, fileId, entry.getKey());
return null;
}
if (!HEX_COLOR.matcher(hex).matches()) {
LOGGER.error("{} Skipping {}: tint_channels '{}' has invalid hex color '{}' "
+ "(expected '#' followed by 6 hex digits)",
TAG, fileId, entry.getKey(), hex);
return null;
}
int color = Integer.parseInt(hex.substring(1), 16);
result.put(entry.getKey(), color);
}
return Collections.unmodifiableMap(result);
}
// ===== Primitive Helpers =====
@Nullable
private static String getStringOrNull(JsonObject obj, String key) {
if (!obj.has(key) || obj.get(key).isJsonNull()) return null;
try {
return obj.get(key).getAsString();
} catch (Exception e) {
return null;
}
}
private static String getStringOrDefault(JsonObject obj, String key, String defaultValue) {
String value = getStringOrNull(obj, key);
return (value != null && !value.isEmpty()) ? value : defaultValue;
}
private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) {
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
return obj.get(key).getAsInt();
} catch (Exception e) {
return defaultValue;
}
}
private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) {
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
return obj.get(key).getAsFloat();
} catch (Exception e) {
return defaultValue;
}
}
private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) {
if (!obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
return obj.get(key).getAsBoolean();
} catch (Exception e) {
return defaultValue;
}
}
@Nullable
private static ResourceLocation parseOptionalResourceLocation(
JsonObject obj, String key, ResourceLocation fileId
) {
String value = getStringOrNull(obj, key);
if (value == null || value.isEmpty()) return null;
ResourceLocation loc = ResourceLocation.tryParse(value);
if (loc == null) {
LOGGER.warn("{} In {}: invalid ResourceLocation for '{}': '{}'", TAG, fileId, key, value);
}
return loc;
}
// ===== Clamping Helpers =====
private static float clamp(float value, float min, float max) {
return Math.max(min, Math.min(max, value));
}
private static int clampInt(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}