infinite dimension fallback dims

seperated computer to different mod
This commit is contained in:
craisinlord committed 2026-09-18 00:57:10 -05:00
1 parent c77d1da53b
commit a72bd7d590
263 files changed
+1781 -9288

No files matched your search

@@ -17,6 +17,7 @@ public final class AntarchySettings {
private static boolean rainbowAntRequiresReagent = true;
private static boolean rainbowAntRightClickDimension = true;
private static boolean termiteRequiresReagent = false;
private static boolean termitesFearBlockLight = true;
private static boolean termiteRightClickDimension = true;
private static boolean antsStealFromChests = true;
private static boolean duplicatorTreeEnabled = true;
@@ -261,14 +262,14 @@ public final class AntarchySettings {
private static int royalGuardianBoundaryProjectileCap = 64;
private static double royalBoltDamage = 6.0D;
private static int royalEggHatchChance = 3;
private static double princeHealth = 120.0D;
private static double princeHealth = 360.0D;
private static double princeAttackDamage = 14.0D;
private static double princeMovementSpeed = 0.28D;
private static double princeFlyingSpeed = 0.9D;
private static double princeArmor = 10.0D;
private static double princeKnockbackResistance = 0.6D;
private static double princeFollowRange = 48.0D;
private static double princessHealth = 110.0D;
private static double princessHealth = 330.0D;
private static double princessAttackDamage = 10.0D;
private static double princessMovementSpeed = 0.28D;
private static double princessFlyingSpeed = 0.95D;
@@ -557,10 +558,10 @@ public final class AntarchySettings {
private static int bloodCrystalLeggingsDefense = 6;
private static int bloodCrystalBootsDefense = 3;
private static double bloodCrystalArmorToughness = 2.0D;
private static int bloodCrystalHelmetDurability = 77;
private static int bloodCrystalChestplateDurability = 112;
private static int bloodCrystalLeggingsDurability = 105;
private static int bloodCrystalBootsDurability = 91;
private static int bloodCrystalHelmetDurability = 92;
private static int bloodCrystalChestplateDurability = 134;
private static int bloodCrystalLeggingsDurability = 126;
private static int bloodCrystalBootsDurability = 109;
private static double toreterrorHealth = 450.0D;
private static double toreterrorJumpAttackDamage = 14.0D;
private static double toreterrorJumpAttackKnockback = 2.5D;
@@ -705,6 +706,10 @@ public final class AntarchySettings {
return termiteRequiresReagent;
}
public static boolean termitesFearBlockLight() {
return termitesFearBlockLight;
}
public static boolean antsStealFromChests() {
return antsStealFromChests;
}
@@ -1328,6 +1333,10 @@ public final class AntarchySettings {
termiteRequiresReagent = value;
}
public static void setTermitesFearBlockLight(boolean value) {
termitesFearBlockLight = value;
}
public static void setAntsStealFromChests(boolean value) {
antsStealFromChests = value;
}
@@ -206,16 +206,6 @@ public final class AntarchyObjects {
public static Supplier<net.minecraft.core.component.DataComponentType<com.craisinlord.antarchy.content.entity.glimmer.GlimmerVariant>> GLIMMER_VARIANT_COMPONENT =
(Supplier<net.minecraft.core.component.DataComponentType<com.craisinlord.antarchy.content.entity.glimmer.GlimmerVariant>>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<net.minecraft.core.component.DataComponentType<net.minecraft.resources.ResourceLocation>> FLOPPY_DISK_COMPONENT =
(Supplier<net.minecraft.core.component.DataComponentType<net.minecraft.resources.ResourceLocation>>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<Item> FLOPPY_DISK = (Supplier<Item>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<Block> COMPUTER = (Supplier<Block>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<BlockEntityType<com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity>> COMPUTER_BLOCK_ENTITY =
(Supplier<BlockEntityType<com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity>>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<MenuType<com.craisinlord.antarchy.content.menu.DorrieInventoryMenu>> DORRIE_INVENTORY_MENU = (Supplier<MenuType<com.craisinlord.antarchy.content.menu.DorrieInventoryMenu>>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<MenuType<com.craisinlord.antarchy.content.menu.RoyalJudgmentMenu>> ROYAL_JUDGMENT_MENU = (Supplier<MenuType<com.craisinlord.antarchy.content.menu.RoyalJudgmentMenu>>) UNBOUND;
@@ -403,6 +393,8 @@ public final class AntarchyObjects {
@SuppressWarnings("unchecked")
public static Supplier<SimpleParticleType> STINKY_GAS = (Supplier<SimpleParticleType>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<SimpleParticleType> BUG_SPRAY_PARTICLE = (Supplier<SimpleParticleType>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<SimpleParticleType> NIGHTMARE_FIRE_FLAME = (Supplier<SimpleParticleType>) UNBOUND;
@SuppressWarnings("unchecked")
public static Supplier<SimpleParticleType> STINKY_FLY = (Supplier<SimpleParticleType>) UNBOUND;
@@ -650,6 +642,10 @@ public final class AntarchyObjects {
OCTOPUS_BOMB = supplier;
}
public static void setBugSprayParticle(Supplier<SimpleParticleType> supplier) {
BUG_SPRAY_PARTICLE = supplier;
}
public static void setTentacle(Supplier<EntityType<TentacleEntity>> supplier) {
TENTACLE = supplier;
}
@@ -912,22 +908,6 @@ public final class AntarchyObjects {
GLIMMER_VARIANT_COMPONENT = supplier;
}
public static void setFloppyDiskComponent(Supplier<net.minecraft.core.component.DataComponentType<net.minecraft.resources.ResourceLocation>> supplier) {
FLOPPY_DISK_COMPONENT = supplier;
}
public static void setFloppyDisk(Supplier<Item> supplier) {
FLOPPY_DISK = supplier;
}
public static void setComputer(Supplier<Block> supplier) {
COMPUTER = supplier;
}
public static void setComputerBlockEntity(Supplier<BlockEntityType<com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity>> supplier) {
COMPUTER_BLOCK_ENTITY = supplier;
}
public static void setDorrieInventoryMenu(Supplier<MenuType<com.craisinlord.antarchy.content.menu.DorrieInventoryMenu>> supplier) {
DORRIE_INVENTORY_MENU = supplier;
}
@@ -221,6 +221,9 @@ public final class AntarchySoundEvents {
public static Supplier<SoundEvent> KING_DECREE = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> KING_SUCCESS = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> KING_JUDGEMENT = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> HOVERBOARD_MOUNT = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> HOVERBOARD_DISMOUNT = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> HOVERBOARD_IDLE = (Supplier<SoundEvent>) UNBOUND;
private AntarchySoundEvents() {
}
@@ -656,4 +659,10 @@ public final class AntarchySoundEvents {
DIMENSIONAL_TEAR_CREATURE_LEAVES = dimensionalTearCreatureLeaves;
DIMENSIONAL_TEAR_CLOSE = dimensionalTearClose;
}
public static void bindHoverboardSounds(Supplier<SoundEvent> mount, Supplier<SoundEvent> dismount, Supplier<SoundEvent> idle) {
HOVERBOARD_MOUNT = mount;
HOVERBOARD_DISMOUNT = dismount;
HOVERBOARD_IDLE = idle;
}
}
@@ -1,76 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
public record AntmailAddress(String username) {
public static final String DOMAIN = "antmail.com";
private static final Pattern USERNAME = Pattern.compile("[a-z0-9_]{3,16}");
public AntmailAddress {
username = normalizeUsername(username);
}
public static AntmailAddress parse(String value) {
if (value == null) {
throw new IllegalArgumentException("Address is null");
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
String suffix = "@" + DOMAIN;
if (!normalized.endsWith(suffix)) {
throw new IllegalArgumentException("Invalid Antmail domain");
}
return new AntmailAddress(normalized.substring(0, normalized.length() - suffix.length()));
}
public static AntmailAddress ofUsername(String username) {
return new AntmailAddress(username);
}
public String fullAddress() {
return username + "@" + DOMAIN;
}
public static String normalizeUsername(String value) {
if (value == null) {
throw new IllegalArgumentException("Username is null");
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
if (!USERNAME.matcher(normalized).matches()) {
throw new IllegalArgumentException("Username must match [a-z0-9_]{3,16}");
}
return normalized;
}
public static boolean isValidUsername(String value) {
if (value == null) {
return false;
}
return USERNAME.matcher(value.trim().toLowerCase(Locale.ROOT)).matches();
}
public static boolean isValidAddress(String value) {
try {
parse(value);
return true;
} catch (IllegalArgumentException exception) {
return false;
}
}
@Override
public String toString() {
return fullAddress();
}
@Override
public boolean equals(Object object) {
return object instanceof AntmailAddress other && username.equals(other.username);
}
@Override
public int hashCode() {
return Objects.hash(username);
}
}
@@ -1,102 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.nbt.CompoundTag;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
public sealed interface AntmailAttachment permits AntmailAttachment.TextFile, AntmailAttachment.PaintImage {
String TYPE_TAG = "Type";
String NAME_TAG = "Name";
String fileName();
int byteSize();
CompoundTag toTag();
static AntmailAttachment fromTag(CompoundTag tag) {
return switch (tag.getString(TYPE_TAG)) {
case "text_file" -> TextFile.fromTag(tag);
case "paint_image" -> PaintImage.fromTag(tag);
default -> throw new IllegalArgumentException("Unknown Antmail attachment type");
};
}
record TextFile(String fileName, String contents) implements AntmailAttachment {
public TextFile {
fileName = validateFileName(fileName);
contents = Objects.requireNonNull(contents, "contents");
if (contents.length() > AntmailValidation.MAX_TEXT_ATTACHMENT_CHARACTERS) {
throw new IllegalArgumentException("Text attachment is too large");
}
}
@Override
public int byteSize() {
return contents.getBytes(StandardCharsets.UTF_8).length;
}
@Override
public CompoundTag toTag() {
CompoundTag tag = new CompoundTag();
tag.putString(TYPE_TAG, "text_file");
tag.putString(NAME_TAG, fileName);
tag.putString("Contents", contents);
return tag;
}
static TextFile fromTag(CompoundTag tag) {
return new TextFile(tag.getString(NAME_TAG), tag.getString("Contents"));
}
}
record PaintImage(String fileName, int width, int height, byte[] pixels) implements AntmailAttachment {
public PaintImage {
fileName = validateFileName(fileName);
if (width != AntmailValidation.MAX_PAINT_WIDTH || height != AntmailValidation.MAX_PAINT_HEIGHT) {
throw new IllegalArgumentException("Invalid Paint image dimensions");
}
if (pixels == null || pixels.length != width * height) {
throw new IllegalArgumentException("Paint pixel data does not match dimensions");
}
for (byte pixel : pixels) {
if (pixel != 0 && pixel != 1) throw new IllegalArgumentException("Paint pixel data must be monochrome");
}
pixels = Arrays.copyOf(pixels, pixels.length);
}
@Override
public byte[] pixels() {
return Arrays.copyOf(pixels, pixels.length);
}
@Override
public int byteSize() {
return pixels.length;
}
@Override
public CompoundTag toTag() {
CompoundTag tag = new CompoundTag();
tag.putString(TYPE_TAG, "paint_image");
tag.putString(NAME_TAG, fileName);
tag.putInt("Width", width);
tag.putInt("Height", height);
tag.putByteArray("Pixels", pixels);
return tag;
}
static PaintImage fromTag(CompoundTag tag) {
return new PaintImage(tag.getString(NAME_TAG), tag.getInt("Width"), tag.getInt("Height"), tag.getByteArray("Pixels"));
}
}
private static String validateFileName(String value) {
if (value == null || value.isBlank() || value.length() > AntmailValidation.MAX_FILE_NAME_LENGTH || value.contains("/") || value.contains("\\")) {
throw new IllegalArgumentException("Invalid attachment filename");
}
return value;
}
}
@@ -1,93 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
import com.craisinlord.antarchy.content.computer.paint.AntPaintCanvas;
import com.craisinlord.antarchy.content.computer.paint.AntPaintFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public final class AntmailAttachmentFiles {
private AntmailAttachmentFiles() {
}
public static Optional<AntmailAttachment> fromComputerFile(ComputerFileSystem.ComputerFile file) {
if (file == null || file.type() != ComputerFileSystem.ComputerFile.Type.TEXT) return Optional.empty();
try {
return Optional.of(new AntmailAttachment.TextFile(filename(file.path()), file.contents()));
} catch (IllegalArgumentException exception) {
return Optional.empty();
}
}
public static AntmailAttachment.TextFile fromTextFile(String path, String contents) {
return new AntmailAttachment.TextFile(filename(path), contents);
}
public static AntmailAttachment.PaintImage fromPaintFile(AntPaintFile file) {
if (file == null) throw new IllegalArgumentException("Paint file cannot be null");
AntPaintCanvas canvas = file.canvas();
if (canvas == null || canvas.copyPixels().length != AntPaintCanvas.PIXEL_COUNT) {
throw new IllegalArgumentException("Invalid AntPaint canvas");
}
boolean[] source = canvas.copyPixels();
byte[] pixels = new byte[source.length];
for (int index = 0; index < source.length; index++) pixels[index] = source[index] ? (byte) 1 : (byte) 0;
return new AntmailAttachment.PaintImage(file.filename(), AntPaintCanvas.WIDTH, AntPaintCanvas.HEIGHT, pixels);
}
public static AntPaintFile toPaintFile(AntmailAttachment.PaintImage image, String fileId, long createdTime, long modifiedTime) {
if (image == null) throw new IllegalArgumentException("Paint attachment cannot be null");
if (image.width() != AntPaintCanvas.WIDTH || image.height() != AntPaintCanvas.HEIGHT) {
throw new IllegalArgumentException("Unsupported AntPaint canvas size");
}
byte[] source = image.pixels();
boolean[] pixels = new boolean[source.length];
for (int index = 0; index < source.length; index++) {
if (source[index] != 0 && source[index] != 1) throw new IllegalArgumentException("Invalid AntPaint pixel");
pixels[index] = source[index] == 1;
}
AntPaintCanvas canvas = new AntPaintCanvas();
for (int index = 0; index < pixels.length; index++) {
if (pixels[index]) canvas.set(index % AntPaintCanvas.WIDTH, index / AntPaintCanvas.WIDTH, true);
}
return new AntPaintFile(fileId, image.fileName(), canvas, createdTime, modifiedTime);
}
public static ComputerFileSystem.ComputerFile toComputerTextFile(String path, AntmailAttachment.TextFile file) {
if (file == null) throw new IllegalArgumentException("Text attachment cannot be null");
String normalized = ComputerFileSystem.normalize(path);
if (normalized.isEmpty() || normalized.equals(ComputerFileSystem.ROOT)) throw new IllegalArgumentException("Invalid text file path");
return ComputerFileSystem.ComputerFile.text(normalized, file.contents());
}
public static List<ComputerFileSystem.ComputerFile> selectableTextFiles(Collection<ComputerFileSystem.ComputerFile> files) {
if (files == null) return List.of();
List<ComputerFileSystem.ComputerFile> result = new ArrayList<>();
for (ComputerFileSystem.ComputerFile file : files) {
if (fromComputerFile(file).isPresent()) result.add(file);
}
return List.copyOf(result);
}
public static List<AntmailAttachment> selectablePaintFiles(Collection<AntPaintFile> files) {
if (files == null) return List.of();
List<AntmailAttachment> result = new ArrayList<>();
for (AntPaintFile file : files) {
try {
result.add(fromPaintFile(file));
} catch (IllegalArgumentException ignored) {
}
}
return List.copyOf(result);
}
private static String filename(String path) {
String normalized = ComputerFileSystem.normalize(path);
if (normalized.isEmpty() || normalized.equals(ComputerFileSystem.ROOT)) throw new IllegalArgumentException("Invalid file path");
int separator = normalized.lastIndexOf('/');
return normalized.substring(separator + 1);
}
}
@@ -1,19 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import com.craisinlord.antarchy.Antarchy;
/** Opt-in diagnostics for AntMail networking and mailbox state transitions. */
public final class AntmailDebug {
private static final String PROPERTY = "antarchy.antmail.debug";
private AntmailDebug() {
}
public static void log(String message) {
if (Boolean.getBoolean(PROPERTY)) Antarchy.LOGGER.info("[AntMail debug] {}", message);
}
public static void error(String message, Throwable throwable) {
Antarchy.LOGGER.error("[AntMail] {}", message, throwable);
}
}
@@ -1,28 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import java.util.UUID;
public record AntmailDeliveryResult(Status status, UUID messageId, AntmailAddress sender, AntmailAddress recipient, String detail) {
public enum Status {
DELIVERED,
QUEUED,
INVALID_ADDRESS,
ADDRESS_NOT_FOUND,
MAILBOX_FULL,
MESSAGE_INVALID,
DUPLICATE,
FAILED
}
public static AntmailDeliveryResult delivered(AntmailMessage message) {
return new AntmailDeliveryResult(Status.DELIVERED, message.id(), message.sender(), message.recipient(), "");
}
public static AntmailDeliveryResult queued(AntmailMessage message) {
return new AntmailDeliveryResult(Status.QUEUED, message.id(), message.sender(), message.recipient(), "");
}
public static AntmailDeliveryResult failed(Status status, AntmailMessage message, String detail) {
return new AntmailDeliveryResult(status, message == null ? null : message.id(), message == null ? null : message.sender(), message == null ? null : message.recipient(), detail);
}
}
@@ -1,43 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public record AntmailDraft(UUID id, AntmailAddress sender, AntmailAddress recipient, String subject, String body, List<AntmailAttachment> attachments) {
public AntmailDraft {
id = id == null ? UUID.randomUUID() : id;
subject = subject == null ? "" : subject;
body = body == null ? "" : body;
attachments = attachments == null ? List.of() : List.copyOf(attachments);
if (subject.length() > AntmailValidation.MAX_SUBJECT_LENGTH || body.length() > AntmailValidation.MAX_BODY_LENGTH || attachments.size() > AntmailValidation.MAX_ATTACHMENTS) {
throw new IllegalArgumentException("Draft exceeds Antmail limits");
}
}
public CompoundTag toTag() {
CompoundTag tag = new CompoundTag();
tag.putString("Id", id.toString());
if (sender != null) tag.putString("From", sender.fullAddress());
if (recipient != null) tag.putString("To", recipient.fullAddress());
tag.putString("Subject", subject);
tag.putString("Body", body);
ListTag attachmentTags = new ListTag();
for (AntmailAttachment attachment : attachments) attachmentTags.add(attachment.toTag());
tag.put("Attachments", attachmentTags);
return tag;
}
public static AntmailDraft fromTag(CompoundTag tag) {
List<AntmailAttachment> attachments = new ArrayList<>();
ListTag attachmentTags = tag.getList("Attachments", 10);
for (int index = 0; index < attachmentTags.size(); index++) attachments.add(AntmailAttachment.fromTag(attachmentTags.getCompound(index)));
UUID id = UUID.fromString(tag.getString("Id"));
AntmailAddress sender = tag.contains("From") ? AntmailAddress.parse(tag.getString("From")) : null;
AntmailAddress recipient = tag.contains("To") ? AntmailAddress.parse(tag.getString("To")) : null;
return new AntmailDraft(id, sender, recipient, tag.getString("Subject"), tag.getString("Body"), attachments);
}
}
@@ -1,102 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.advancements.AdvancementHolder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class AntmailEventData extends SimplePreparableReloadListener<Map<ResourceLocation, AntmailEventData.Definition>> {
private static final AntmailEventData INSTANCE = new AntmailEventData();
private static volatile Map<ResourceLocation, Definition> definitions = Map.of();
private AntmailEventData() {
}
public static AntmailEventData instance() {
return INSTANCE;
}
public static void onTick(MinecraftServer server) {
long dayTime = server.overworld().getDayTime();
long day = dayTime / 24000L;
if (dayTime % 24000L >= 20L) return;
AntmailServerData data = AntmailServerData.access(server);
if (!data.beginRandomMailDay(day)) return;
for (AntmailAddress address : data.registeredAddresses()) {
for (Map.Entry<ResourceLocation, Definition> entry : definitions.entrySet()) {
Definition definition = entry.getValue();
if (definition.trigger().type().equals("random") && server.overworld().random.nextDouble() < definition.trigger().chance()) deliver(server, address, entry.getKey(), definition, false);
}
}
}
public static void onAdvancement(ServerPlayer player, AdvancementHolder advancement) {
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress address = data.lastUsedAddress(player);
if (address == null) return;
for (Map.Entry<ResourceLocation, Definition> entry : definitions.entrySet()) {
Trigger trigger = entry.getValue().trigger();
if (trigger.type().equals("advancement") && trigger.value().equals(advancement.id().toString())) deliver(player.server, address, entry.getKey(), entry.getValue(), true);
}
}
private static void deliver(MinecraftServer server, AntmailAddress recipient, ResourceLocation id, Definition definition, boolean permanentClaim) {
AntmailServerData data = AntmailServerData.access(server);
try {
AntmailAddress sender = AntmailAddress.parse(definition.sender());
AntmailMessage message = AntmailMessage.create(sender, recipient, definition.subject(), definition.body(), server.overworld().getGameTime(), List.of());
AntmailDeliveryResult result = data.deliver(server, message);
if (permanentClaim && result.status() != AntmailDeliveryResult.Status.MAILBOX_FULL
&& result.status() != AntmailDeliveryResult.Status.FAILED
&& result.status() != AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND) {
data.claimTrigger(recipient, id);
}
} catch (IllegalArgumentException ignored) {
}
}
@Override
protected Map<ResourceLocation, Definition> prepare(ResourceManager manager, ProfilerFiller profiler) {
Map<ResourceLocation, Definition> loaded = new HashMap<>();
for (Map.Entry<ResourceLocation, net.minecraft.server.packs.resources.Resource> resource : manager.listResources("antmail/message", path -> path.getPath().endsWith(".json")).entrySet()) {
try {
JsonElement element = JsonParser.parseReader(resource.getValue().openAsReader());
if (!element.isJsonObject()) continue;
var object = element.getAsJsonObject();
var trigger = object.getAsJsonObject("trigger");
String type = trigger.get("type").getAsString();
String value = trigger.has("value") ? trigger.get("value").getAsString() : String.valueOf(trigger.get("day").getAsInt());
ResourceLocation id = ResourceLocation.fromNamespaceAndPath(resource.getKey().getNamespace(), resource.getKey().getPath().substring("antmail/message/".length(), resource.getKey().getPath().length() - 5));
loaded.put(id, new Definition(object.get("sender").getAsString(), object.get("subject").getAsString(), object.get("body").getAsString(), new Trigger(type, value)));
} catch (Exception ignored) {
}
}
return Map.copyOf(loaded);
}
@Override
protected void apply(Map<ResourceLocation, Definition> loaded, ResourceManager manager, ProfilerFiller profiler) {
definitions = loaded;
}
public record Definition(String sender, String subject, String body, Trigger trigger) {
}
public record Trigger(String type, String value) {
double chance() {
try {
return Math.max(0.0D, Math.min(1.0D, Double.parseDouble(value)));
} catch (NumberFormatException ignored) {
return 0.0D;
}
}
}
}
@@ -1,126 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public final class AntmailMailbox {
public static final int PAGE_SIZE = 20;
private final AntmailAddress address;
private final List<AntmailMessage> inbox = new ArrayList<>();
private final List<AntmailMessage> sent = new ArrayList<>();
private final List<AntmailDraft> drafts = new ArrayList<>();
private int reportedUnreadCount = -1;
public AntmailMailbox(AntmailAddress address) {
this.address = address;
}
public AntmailAddress address() { return address; }
public List<AntmailMessage> inbox() { return List.copyOf(inbox); }
public List<AntmailMessage> sent() { return List.copyOf(sent); }
public List<AntmailDraft> drafts() { return List.copyOf(drafts); }
public int unreadCount() { return reportedUnreadCount >= 0 ? reportedUnreadCount : (int) inbox.stream().filter(message -> !message.read()).count(); }
public boolean addIncoming(AntmailMessage message) {
if (!address.equals(message.recipient()) || inbox.size() >= AntmailValidation.MAX_MAILBOX_MESSAGES || contains(inbox, message.id())) return false;
inbox.add(message);
return true;
}
public boolean addSent(AntmailMessage message) {
if (!address.equals(message.sender()) || sent.size() >= AntmailValidation.MAX_MAILBOX_MESSAGES || contains(sent, message.id())) return false;
sent.add(message);
return true;
}
public boolean saveDraft(AntmailDraft draft) {
if (!address.equals(draft.sender()) || drafts.size() >= AntmailValidation.MAX_DRAFTS && findDraft(draft.id()) == null) return false;
drafts.removeIf(existing -> existing.id().equals(draft.id()));
drafts.add(draft);
return true;
}
public boolean removeDraft(UUID id) { return drafts.removeIf(draft -> draft.id().equals(id)); }
public boolean removeInbox(UUID id) { return inbox.removeIf(message -> message.id().equals(id)); }
public boolean removeSent(UUID id) { return sent.removeIf(message -> message.id().equals(id)); }
public AntmailMessage findSent(UUID id) { return sent.stream().filter(message -> message.id().equals(id)).findFirst().orElse(null); }
public AntmailMessage findInbox(UUID id) { return inbox.stream().filter(message -> message.id().equals(id)).findFirst().orElse(null); }
public boolean markRead(UUID id) {
for (AntmailMessage message : inbox) if (message.id().equals(id)) { message.markRead(); return true; }
return false;
}
public AntmailDraft findDraft(UUID id) { return drafts.stream().filter(draft -> draft.id().equals(id)).findFirst().orElse(null); }
public CompoundTag toTag() {
return toTag(true);
}
public CompoundTag toTag(boolean includeMessageAttachments) {
CompoundTag tag = new CompoundTag();
tag.putString("Address", address.fullAddress());
tag.put("Inbox", messagesToTag(inbox, includeMessageAttachments));
tag.put("Sent", messagesToTag(sent, includeMessageAttachments));
ListTag draftTags = new ListTag();
for (AntmailDraft draft : drafts) draftTags.add(draft.toTag());
tag.put("Drafts", draftTags);
return tag;
}
public CompoundTag toPageTag(int folder, int page) {
int safePage = Math.max(0, page);
int start = safePage * PAGE_SIZE;
CompoundTag tag = new CompoundTag();
tag.putString("Address", address.fullAddress());
tag.putInt("Folder", folder);
tag.putInt("Page", safePage);
tag.putInt("PageSize", PAGE_SIZE);
tag.putInt("TotalInbox", inbox.size());
tag.putInt("TotalSent", sent.size());
tag.putInt("UnreadCount", (int) inbox.stream().filter(message -> !message.read()).count());
tag.put("Inbox", folder == 0 ? messagesToTag(page(inbox, start), false) : new ListTag());
tag.put("Sent", folder == 1 ? messagesToTag(page(sent, start), false) : new ListTag());
ListTag draftTags = new ListTag();
for (AntmailDraft draft : drafts) draftTags.add(draft.toTag());
tag.put("Drafts", draftTags);
return tag;
}
private static <T> List<T> page(List<T> values, int start) {
if (start >= values.size()) return List.of();
return values.subList(start, Math.min(start + PAGE_SIZE, values.size()));
}
public static AntmailMailbox fromTag(CompoundTag tag) {
AntmailMailbox mailbox = new AntmailMailbox(AntmailAddress.parse(tag.getString("Address")));
if (tag.contains("UnreadCount")) mailbox.reportedUnreadCount = Math.max(0, tag.getInt("UnreadCount"));
readMessages(tag.getList("Inbox", 10), mailbox.inbox);
readMessages(tag.getList("Sent", 10), mailbox.sent);
ListTag draftTags = tag.getList("Drafts", 10);
for (int index = 0; index < draftTags.size() && mailbox.drafts.size() < AntmailValidation.MAX_DRAFTS; index++) {
try { mailbox.drafts.add(AntmailDraft.fromTag(draftTags.getCompound(index))); } catch (RuntimeException ignored) { }
}
return mailbox;
}
private static ListTag messagesToTag(List<AntmailMessage> messages, boolean includeMessageAttachments) {
ListTag tags = new ListTag();
for (AntmailMessage message : messages) tags.add(message.toTag(includeMessageAttachments));
return tags;
}
private static void readMessages(ListTag tags, List<AntmailMessage> target) {
for (int index = 0; index < tags.size() && target.size() < AntmailValidation.MAX_MAILBOX_MESSAGES; index++) {
try { target.add(AntmailMessage.fromTag(tags.getCompound(index))); } catch (RuntimeException ignored) { }
}
}
private static boolean contains(List<AntmailMessage> messages, UUID id) {
return messages.stream().anyMatch(message -> message.id().equals(id));
}
}
@@ -1,54 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class AntmailMailboxStore {
private final Map<AntmailAddress, AntmailMailbox> mailboxes = new LinkedHashMap<>();
public AntmailMailbox getOrCreate(AntmailAddress address) {
return mailboxes.computeIfAbsent(address, AntmailMailbox::new);
}
public AntmailMailbox get(AntmailAddress address) {
return mailboxes.get(address);
}
public boolean contains(AntmailAddress address) {
return mailboxes.containsKey(address);
}
public void put(AntmailMailbox mailbox) {
mailboxes.put(mailbox.address(), mailbox);
}
public Collection<AntmailMailbox> mailboxes() {
return List.copyOf(mailboxes.values());
}
public CompoundTag toTag() {
CompoundTag tag = new CompoundTag();
ListTag mailboxTags = new ListTag();
for (AntmailMailbox mailbox : mailboxes.values()) mailboxTags.add(mailbox.toTag());
tag.put("Mailboxes", mailboxTags);
return tag;
}
public static AntmailMailboxStore fromTag(CompoundTag tag) {
AntmailMailboxStore store = new AntmailMailboxStore();
ListTag mailboxTags = tag.getList("Mailboxes", 10);
for (int index = 0; index < mailboxTags.size(); index++) {
try {
AntmailMailbox mailbox = AntmailMailbox.fromTag(mailboxTags.getCompound(index));
store.mailboxes.put(mailbox.address(), mailbox);
} catch (RuntimeException ignored) {
}
}
return store;
}
}
@@ -1,93 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public final class AntmailMessage {
private static final String ID_TAG = "Id";
private static final String FROM_TAG = "From";
private static final String TO_TAG = "To";
private static final String SUBJECT_TAG = "Subject";
private static final String BODY_TAG = "Body";
private static final String CREATED_TAG = "Created";
private static final String READ_TAG = "Read";
private static final String ATTACHMENTS_TAG = "Attachments";
private static final String DELIVERY_STATUS_TAG = "DeliveryStatus";
private final UUID id;
private final AntmailAddress sender;
private final AntmailAddress recipient;
private final String subject;
private final String body;
private final long createdAt;
private final List<AntmailAttachment> attachments;
private boolean read;
private String deliveryStatus = "DELIVERED";
public AntmailMessage(UUID id, AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List<AntmailAttachment> attachments, boolean read) {
AntmailValidation.Result result = AntmailValidation.validateMessage(sender, recipient, subject, body, attachments);
if (!result.valid()) {
throw new IllegalArgumentException(result.reason());
}
this.id = id == null ? UUID.randomUUID() : id;
this.sender = sender;
this.recipient = recipient;
this.subject = subject;
this.body = body;
this.createdAt = createdAt;
this.attachments = List.copyOf(attachments);
this.read = read;
}
public static AntmailMessage create(AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List<AntmailAttachment> attachments) {
return new AntmailMessage(UUID.randomUUID(), sender, recipient, subject, body, createdAt, attachments, false);
}
public UUID id() { return id; }
public AntmailAddress sender() { return sender; }
public AntmailAddress recipient() { return recipient; }
public String subject() { return subject; }
public String body() { return body; }
public long createdAt() { return createdAt; }
public List<AntmailAttachment> attachments() { return attachments; }
public boolean read() { return read; }
public String deliveryStatus() { return deliveryStatus; }
public void setDeliveryStatus(String status) { deliveryStatus = status == null || status.isBlank() ? "DELIVERED" : status; }
public void markRead() { read = true; }
public void markUnread() { read = false; }
public CompoundTag toTag() {
return toTag(true);
}
public CompoundTag toTag(boolean includeAttachments) {
CompoundTag tag = new CompoundTag();
tag.putString(ID_TAG, id.toString());
tag.putString(FROM_TAG, sender.fullAddress());
tag.putString(TO_TAG, recipient.fullAddress());
tag.putString(SUBJECT_TAG, subject);
tag.putString(BODY_TAG, body);
tag.putLong(CREATED_TAG, createdAt);
tag.putBoolean(READ_TAG, read);
tag.putString(DELIVERY_STATUS_TAG, deliveryStatus);
if (includeAttachments) {
ListTag attachmentTags = new ListTag();
for (AntmailAttachment attachment : attachments) attachmentTags.add(attachment.toTag());
tag.put(ATTACHMENTS_TAG, attachmentTags);
}
return tag;
}
public static AntmailMessage fromTag(CompoundTag tag) {
List<AntmailAttachment> attachments = new ArrayList<>();
ListTag attachmentTags = tag.getList(ATTACHMENTS_TAG, 10);
for (int index = 0; index < attachmentTags.size(); index++) attachments.add(AntmailAttachment.fromTag(attachmentTags.getCompound(index)));
AntmailMessage message = new AntmailMessage(UUID.fromString(tag.getString(ID_TAG)), AntmailAddress.parse(tag.getString(FROM_TAG)), AntmailAddress.parse(tag.getString(TO_TAG)), tag.getString(SUBJECT_TAG), tag.getString(BODY_TAG), tag.getLong(CREATED_TAG), attachments, tag.getBoolean(READ_TAG));
message.setDeliveryStatus(tag.getString(DELIVERY_STATUS_TAG));
return message;
}
}
@@ -1,339 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import net.minecraft.world.level.saveddata.SavedData;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public final class AntmailServerData extends SavedData {
private static final String ID = "antarchy_antmail";
private static final int MAX_PENDING_MESSAGES = AntmailValidation.MAX_MAILBOX_MESSAGES;
private final Map<AntmailAddress, Endpoint> endpoints = new LinkedHashMap<>();
private final AntmailMailboxStore mailboxes = new AntmailMailboxStore();
private final List<PendingMessage> pending = new ArrayList<>();
private final Map<AntmailAddress, java.util.Set<ResourceLocation>> triggered = new LinkedHashMap<>();
private final Map<AntmailAddress, Long> mailboxVersions = new LinkedHashMap<>();
private final Map<UUID, AntmailAddress> lastUsedAddresses = new LinkedHashMap<>();
private long randomMailDay = Long.MIN_VALUE;
private int retryTicks;
public static AntmailServerData create() {
return new AntmailServerData();
}
public static AntmailServerData load(CompoundTag tag, HolderLookup.Provider registries) {
AntmailServerData data = new AntmailServerData();
ListTag endpointTags = tag.getList("Endpoints", 10);
for (int i = 0; i < endpointTags.size(); i++) {
try {
CompoundTag endpoint = endpointTags.getCompound(i);
AntmailAddress address = AntmailAddress.parse(endpoint.getString("Address"));
data.endpoints.put(address, new Endpoint(endpoint.getString("Dimension"), endpoint.getLong("Position")));
} catch (RuntimeException ignored) {
}
}
AntmailServerData loaded = data;
AntmailMailboxStore restored = AntmailMailboxStore.fromTag(tag.getCompound("Mailboxes"));
for (AntmailMailbox mailbox : restored.mailboxes()) {
loaded.mailboxes.put(mailbox);
}
ListTag pendingTags = tag.getList("Pending", 10);
for (int i = 0; i < pendingTags.size(); i++) {
try {
CompoundTag pendingTag = pendingTags.getCompound(i);
loaded.pending.add(new PendingMessage(AntmailAddress.parse(pendingTag.getString("Recipient")), AntmailMessage.fromTag(pendingTag.getCompound("Message"))));
} catch (RuntimeException ignored) {
}
}
ListTag triggeredTags = tag.getList("Triggered", 10);
for (int i = 0; i < triggeredTags.size(); i++) {
CompoundTag triggeredTag = triggeredTags.getCompound(i);
try {
AntmailAddress address = AntmailAddress.parse(triggeredTag.getString("Address"));
java.util.Set<ResourceLocation> ids = new java.util.LinkedHashSet<>();
ListTag idsTag = triggeredTag.getList("Ids", 8);
for (int j = 0; j < idsTag.size(); j++) ids.add(ResourceLocation.parse(idsTag.getString(j)));
data.triggered.put(address, ids);
} catch (RuntimeException ignored) {
}
}
ListTag lastUsedTags = tag.getList("LastUsedAddresses", 10);
for (int i = 0; i < lastUsedTags.size(); i++) {
try {
CompoundTag lastUsedTag = lastUsedTags.getCompound(i);
data.lastUsedAddresses.put(UUID.fromString(lastUsedTag.getString("Player")), AntmailAddress.parse(lastUsedTag.getString("Address")));
} catch (RuntimeException ignored) {
}
}
data.randomMailDay = tag.contains("RandomMailDay") ? tag.getLong("RandomMailDay") : Long.MIN_VALUE;
ListTag versionTags = tag.getList("MailboxVersions", 10);
for (int i = 0; i < versionTags.size(); i++) {
try {
CompoundTag versionTag = versionTags.getCompound(i);
data.mailboxVersions.put(AntmailAddress.parse(versionTag.getString("Address")), Math.max(1L, versionTag.getLong("Version")));
} catch (RuntimeException ignored) {
}
}
return loaded;
}
@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
ListTag endpointTags = new ListTag();
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
CompoundTag endpoint = new CompoundTag();
endpoint.putString("Address", entry.getKey().fullAddress());
endpoint.putString("Dimension", entry.getValue().dimension());
endpoint.putLong("Position", entry.getValue().position());
endpointTags.add(endpoint);
}
tag.put("Endpoints", endpointTags);
tag.put("Mailboxes", mailboxes.toTag());
ListTag pendingTags = new ListTag();
for (PendingMessage message : pending) {
CompoundTag pendingTag = new CompoundTag();
pendingTag.putString("Recipient", message.recipient().fullAddress());
pendingTag.put("Message", message.message().toTag());
pendingTags.add(pendingTag);
}
tag.put("Pending", pendingTags);
ListTag triggeredTags = new ListTag();
for (Map.Entry<AntmailAddress, java.util.Set<ResourceLocation>> entry : triggered.entrySet()) {
CompoundTag triggeredTag = new CompoundTag();
triggeredTag.putString("Address", entry.getKey().fullAddress());
ListTag idsTag = new ListTag();
for (ResourceLocation id : entry.getValue()) idsTag.add(net.minecraft.nbt.StringTag.valueOf(id.toString()));
triggeredTag.put("Ids", idsTag);
triggeredTags.add(triggeredTag);
}
tag.put("Triggered", triggeredTags);
ListTag lastUsedTags = new ListTag();
for (Map.Entry<UUID, AntmailAddress> entry : lastUsedAddresses.entrySet()) {
CompoundTag lastUsedTag = new CompoundTag();
lastUsedTag.putString("Player", entry.getKey().toString());
lastUsedTag.putString("Address", entry.getValue().fullAddress());
lastUsedTags.add(lastUsedTag);
}
tag.put("LastUsedAddresses", lastUsedTags);
tag.putLong("RandomMailDay", randomMailDay);
ListTag versionTags = new ListTag();
for (Map.Entry<AntmailAddress, Long> entry : mailboxVersions.entrySet()) {
CompoundTag versionTag = new CompoundTag();
versionTag.putString("Address", entry.getKey().fullAddress());
versionTag.putLong("Version", entry.getValue());
versionTags.add(versionTag);
}
tag.put("MailboxVersions", versionTags);
return tag;
}
private static AntmailServerData get(MinecraftServer server) {
return server.overworld().getDataStorage().computeIfAbsent(
new SavedData.Factory<>(AntmailServerData::create, AntmailServerData::load, null), ID);
}
public static AntmailServerData access(MinecraftServer server) {
return get(server);
}
public synchronized Registration register(MinecraftServer server, ServerLevel level, BlockPos position, String username) {
final AntmailAddress address;
try {
address = AntmailAddress.ofUsername(username);
} catch (IllegalArgumentException exception) {
return Registration.invalid("invalid_username");
}
AntmailAddress previous = addressAt(level.dimension().location(), position);
Endpoint existing = endpoints.get(address);
if (existing != null && (!existing.dimension().equals(level.dimension().location().toString()) || existing.position() != position.asLong())) {
return Registration.invalid("username_taken");
}
if (previous != null && !previous.equals(address)) {
endpoints.remove(previous);
}
endpoints.put(address, new Endpoint(level.dimension().location().toString(), position.asLong()));
get(server).drain(server, address);
setDirty();
return Registration.accepted(address);
}
public synchronized AntmailAddress addressAt(ResourceLocation dimension, BlockPos position) {
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
if (entry.getValue().dimension().equals(dimension.toString()) && entry.getValue().position() == position.asLong()) return entry.getKey();
}
return null;
}
public synchronized boolean unregister(ResourceLocation dimension, BlockPos position) {
AntmailAddress address = addressAt(dimension, position);
if (address == null) return false;
endpoints.remove(address);
setDirty();
return true;
}
public synchronized Endpoint endpoint(AntmailAddress address) {
return endpoints.get(address);
}
public synchronized List<AntmailAddress> registeredAddresses() {
return List.copyOf(endpoints.keySet());
}
public synchronized AntmailAddress addressFor(ServerPlayer player) {
AntmailAddress address = lastUsedAddresses.get(player.getUUID());
return address != null && endpoints.containsKey(address) ? address : null;
}
public synchronized AntmailAddress lastUsedAddress(ServerPlayer player) {
AntmailAddress address = lastUsedAddresses.get(player.getUUID());
return address != null && endpoints.containsKey(address) ? address : null;
}
public synchronized void setLastUsedAddress(ServerPlayer player, AntmailAddress address) {
if (address == null) return;
lastUsedAddresses.put(player.getUUID(), address);
setDirty();
}
public synchronized boolean claimTrigger(AntmailAddress address, ResourceLocation triggerId) {
java.util.Set<ResourceLocation> ids = triggered.computeIfAbsent(address, ignored -> new java.util.LinkedHashSet<>());
if (!ids.add(triggerId)) return false;
setDirty();
return true;
}
public synchronized boolean beginRandomMailDay(long day) {
if (randomMailDay == day) return false;
randomMailDay = day;
setDirty();
return true;
}
public synchronized AntmailMailbox mailbox(AntmailAddress address) {
return mailboxes.get(address);
}
public synchronized AntmailMailbox mailboxOrCreate(AntmailAddress address) {
AntmailMailbox mailbox = mailboxes.getOrCreate(address);
setDirty();
return mailbox;
}
public synchronized long mailboxVersion(AntmailAddress address) {
return mailboxVersions.getOrDefault(address, 1L);
}
public synchronized void touchMailbox(AntmailAddress address) {
if (address == null) return;
mailboxVersions.put(address, mailboxVersion(address) + 1L);
setDirty();
}
public synchronized void drainAddress(MinecraftServer server, ServerLevel level, BlockPos position) {
AntmailAddress address = addressAt(level.dimension().location(), position);
if (address != null) drain(server, address);
}
public synchronized void drainAvailable(MinecraftServer server) {
if (pending.isEmpty() || ++retryTicks < 20) return;
retryTicks = 0;
List<AntmailAddress> available = new ArrayList<>();
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
Endpoint endpoint = entry.getValue();
ServerLevel level = server.getLevel(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, ResourceLocation.parse(endpoint.dimension())));
if (level != null && level.hasChunkAt(BlockPos.of(endpoint.position()))) available.add(entry.getKey());
}
for (AntmailAddress address : available) drain(server, address);
}
public synchronized AntmailDeliveryResult deliver(MinecraftServer server, AntmailMessage message) {
if (message == null) return AntmailDeliveryResult.failed(AntmailDeliveryResult.Status.MESSAGE_INVALID, null, "missing_message");
if (endpoint(message.recipient()) == null) return failedAndRecorded(message, AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND, "address_not_found");
AntmailMailbox destination = mailboxes.getOrCreate(message.recipient());
if (destination.inbox().size() >= AntmailValidation.MAX_MAILBOX_MESSAGES) return failedAndRecorded(message, AntmailDeliveryResult.Status.MAILBOX_FULL, "mailbox_full");
Endpoint target = endpoint(message.recipient());
ServerLevel level = server.getLevel(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, ResourceLocation.parse(target.dimension())));
if (level == null || !level.hasChunkAt(BlockPos.of(target.position()))) {
if (pending.stream().anyMatch(item -> item.message().id().equals(message.id()))) {
return AntmailDeliveryResult.failed(AntmailDeliveryResult.Status.DUPLICATE, message, "already_pending");
}
if (pending.size() >= MAX_PENDING_MESSAGES) {
return failedAndRecorded(message, AntmailDeliveryResult.Status.FAILED, "pending_queue_full");
}
message.setDeliveryStatus("QUEUED");
pending.add(new PendingMessage(message.recipient(), message));
mailboxes.getOrCreate(message.sender()).addSent(message);
touchMailbox(message.recipient());
touchMailbox(message.sender());
setDirty();
return AntmailDeliveryResult.queued(message);
}
message.setDeliveryStatus("DELIVERED");
destination.addIncoming(message);
mailboxes.getOrCreate(message.sender()).addSent(message);
touchMailbox(message.recipient());
touchMailbox(message.sender());
setDirty();
return AntmailDeliveryResult.delivered(message);
}
private AntmailDeliveryResult failedAndRecorded(AntmailMessage message, AntmailDeliveryResult.Status status, String detail) {
message.setDeliveryStatus("FAILED // " + detail.toUpperCase());
mailboxes.getOrCreate(message.sender()).addSent(message);
touchMailbox(message.sender());
setDirty();
return AntmailDeliveryResult.failed(status, message, detail);
}
public synchronized void drain(MinecraftServer server, AntmailAddress address) {
AntmailMailbox mailbox = mailboxes.getOrCreate(address);
final boolean[] delivered = {false};
Set<AntmailAddress> deliveredSenders = new LinkedHashSet<>();
pending.removeIf(pendingMessage -> {
if (!pendingMessage.recipient().equals(address) || mailbox.inbox().size() >= AntmailValidation.MAX_MAILBOX_MESSAGES) return false;
if (!mailbox.addIncoming(pendingMessage.message())) return false;
pendingMessage.message().setDeliveryStatus("DELIVERED");
delivered[0] = true;
deliveredSenders.add(pendingMessage.message().sender());
return true;
});
if (delivered[0]) {
touchMailbox(address);
for (AntmailAddress sender : deliveredSenders) touchMailbox(sender);
setDirty();
}
}
public synchronized AntmailDeliveryResult retry(MinecraftServer server, AntmailAddress sender, UUID messageId) {
AntmailMessage message = mailboxes.getOrCreate(sender).findSent(messageId);
if (message == null) return null;
pending.removeIf(item -> item.message().id().equals(messageId));
return deliver(server, message);
}
public record Endpoint(String dimension, long position) {
}
public record Registration(boolean valid, AntmailAddress address, String reason) {
static Registration accepted(AntmailAddress address) { return new Registration(true, address, ""); }
static Registration invalid(String reason) { return new Registration(false, null, reason); }
}
private record PendingMessage(AntmailAddress recipient, AntmailMessage message) {
}
}
@@ -1,67 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import java.nio.charset.StandardCharsets;
import java.util.List;
public final class AntmailValidation {
public static final int MAX_SUBJECT_LENGTH = 64;
public static final int MAX_BODY_LENGTH = 4096;
public static final int MAX_FILE_NAME_LENGTH = 32;
public static final int MAX_ATTACHMENTS = 8;
public static final int MAX_TEXT_ATTACHMENT_CHARACTERS = 16384;
public static final int MAX_PAINT_WIDTH = 32;
public static final int MAX_PAINT_HEIGHT = 24;
public static final int MAX_MESSAGE_BYTES = 65536;
public static final int MAX_ATTACHMENT_WIRE_CHARS = 65536;
public static final int MAX_MAILBOX_MESSAGES = 100;
public static final int MAX_DRAFTS = 25;
private AntmailValidation() {
}
public static Result validateMessage(AntmailAddress sender, AntmailAddress recipient, String subject, String body, List<AntmailAttachment> attachments) {
if (sender == null || recipient == null) {
return Result.invalid("missing address");
}
if (subject == null || subject.length() > MAX_SUBJECT_LENGTH) {
return Result.invalid("subject too long");
}
if (body == null || body.length() > MAX_BODY_LENGTH) {
return Result.invalid("body too long");
}
Result attachmentResult = validateAttachments(attachments);
if (!attachmentResult.valid()) return attachmentResult;
int bytes = subject.getBytes(StandardCharsets.UTF_8).length + body.getBytes(StandardCharsets.UTF_8).length;
for (AntmailAttachment attachment : attachments) bytes += attachment.byteSize();
if (bytes > MAX_MESSAGE_BYTES) {
return Result.invalid("message too large");
}
return Result.accepted();
}
public static Result validateAttachments(List<AntmailAttachment> attachments) {
if (attachments == null || attachments.size() > MAX_ATTACHMENTS) return Result.invalid("too many attachments");
int textCharacters = 0;
for (AntmailAttachment attachment : attachments) {
if (attachment == null) return Result.invalid("null attachment");
if (attachment instanceof AntmailAttachment.TextFile textFile) {
if (textFile.contents().length() > MAX_TEXT_ATTACHMENT_CHARACTERS) return Result.invalid("text attachment too large");
textCharacters += textFile.contents().length();
} else if (attachment instanceof AntmailAttachment.PaintImage paintImage) {
if (paintImage.width() != MAX_PAINT_WIDTH || paintImage.height() != MAX_PAINT_HEIGHT) return Result.invalid("unsupported paint size");
}
}
if (textCharacters > MAX_TEXT_ATTACHMENT_CHARACTERS) return Result.invalid("text attachments too large");
return Result.accepted();
}
public record Result(boolean valid, String reason) {
public static Result accepted() {
return new Result(true, "");
}
public static Result invalid(String reason) {
return new Result(false, reason);
}
}
}
@@ -1,72 +0,0 @@
package com.craisinlord.antarchy.content.antmail;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
public final class AntmailWire {
private static final int MAX_COMPRESSED_BYTES = 1024 * 1024;
private static final long MAX_DECOMPRESSED_BYTES = 8L * 1024L * 1024L;
private AntmailWire() {
}
public static String encodeAttachments(List<AntmailAttachment> attachments) {
AntmailValidation.Result validation = AntmailValidation.validateAttachments(attachments);
if (!validation.valid()) throw new IllegalArgumentException(validation.reason());
CompoundTag root = new CompoundTag();
ListTag list = new ListTag();
for (AntmailAttachment attachment : attachments) list.add(attachment.toTag());
root.put("Attachments", list);
String encoded = encodeTag(root);
if (encoded.length() > AntmailValidation.MAX_ATTACHMENT_WIRE_CHARS) throw new IllegalArgumentException("Attachment payload is too large");
return encoded;
}
public static String encodeTag(CompoundTag tag) {
if (tag == null) throw new IllegalArgumentException("Antmail tag cannot be null");
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
NbtIo.writeCompressed(tag, output);
byte[] compressed = output.toByteArray();
if (compressed.length > MAX_COMPRESSED_BYTES) throw new IllegalArgumentException("Antmail data is too large");
return Base64.getEncoder().encodeToString(compressed);
} catch (IOException exception) {
throw new IllegalArgumentException("Could not encode Antmail data", exception);
}
}
public static CompoundTag decodeTag(String encoded) {
try {
if (encoded == null || encoded.isBlank()) throw new IllegalArgumentException("Antmail data is empty");
byte[] compressed = Base64.getDecoder().decode(encoded);
if (compressed.length > MAX_COMPRESSED_BYTES) throw new IllegalArgumentException("Antmail data is too large");
return NbtIo.readCompressed(new ByteArrayInputStream(compressed), NbtAccounter.create(MAX_DECOMPRESSED_BYTES));
} catch (Exception exception) {
throw new IllegalArgumentException("Invalid Antmail data", exception);
}
}
public static List<AntmailAttachment> decodeAttachments(String encoded) {
if (encoded == null || encoded.isBlank()) return List.of();
if (encoded.length() > AntmailValidation.MAX_ATTACHMENT_WIRE_CHARS) throw new IllegalArgumentException("Attachment payload is too large");
try {
CompoundTag root = decodeTag(encoded);
ListTag list = root.getList("Attachments", 10);
List<AntmailAttachment> result = new ArrayList<>();
for (int i = 0; i < list.size(); i++) result.add(AntmailAttachment.fromTag(list.getCompound(i)));
AntmailValidation.Result validation = AntmailValidation.validateAttachments(result);
if (!validation.valid()) throw new IllegalArgumentException(validation.reason());
return List.copyOf(result);
} catch (Exception exception) {
throw new IllegalArgumentException("Invalid attachment data", exception);
}
}
}
@@ -0,0 +1,52 @@
package com.craisinlord.antarchy.content.block;
import com.craisinlord.antarchy.content.block.entity.AntNestBlockEntity;
import com.craisinlord.antarchy.content.entity.ant.BaseAntEntity;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.level.BlockGetter;
public final class AntTrapBlock extends AntNestBlock {
private static final VoxelShape SHAPE = Block.box(3.0D, 0.0D, 3.0D, 13.0D, 5.0D, 13.0D);
public AntTrapBlock(Supplier<List<EntityType<? extends BaseAntEntity>>> antTypesSupplier,
Supplier<? extends BlockEntityType<AntNestBlockEntity>> blockEntityTypeSupplier,
BlockBehaviour.Properties properties) {
super(antTypesSupplier, blockEntityTypeSupplier, 10, false, false, properties);
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPE;
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
if (!state.is(newState.getBlock()) && level instanceof ServerLevel serverLevel
&& level.getBlockEntity(pos) instanceof AntNestBlockEntity trap) {
trap.releaseAll(serverLevel);
}
super.onRemove(state, level, pos, newState, movedByPiston);
}
@Override
public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity, ItemStack stack) {
if (blockEntity instanceof AntNestBlockEntity trap && level instanceof ServerLevel serverLevel) {
trap.releaseAll(serverLevel);
}
super.playerDestroy(level, player, pos, state, blockEntity, stack);
}
}
@@ -1,147 +0,0 @@
package com.craisinlord.antarchy.content.block;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import com.craisinlord.antarchy.content.antmail.AntmailServerData;
import com.craisinlord.antarchy.content.client.AntarchyClientHooks;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
public final class ComputerBlock extends BaseEntityBlock {
public static final MapCodec<ComputerBlock> CODEC = Block.simpleCodec(ComputerBlock::new);
public static final BooleanProperty ACTIVE = BooleanProperty.create("active");
public static final DirectionProperty FACING = net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING;
private static final VoxelShape SHAPE = Block.box(1.0D, 0.0D, 1.0D, 15.0D, 16.0D, 15.0D);
public ComputerBlock(BlockBehaviour.Properties properties) {
super(properties.lightLevel(state -> state.getValue(ACTIVE) ? 7 : 0));
registerDefaultState(stateDefinition.any().setValue(ACTIVE, false).setValue(FACING, Direction.NORTH));
}
@Override
public MapCodec<ComputerBlock> codec() {
return CODEC;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(ACTIVE, FACING);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
@Override
protected BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}
@Override
protected BlockState mirror(BlockState state, Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}
@Override
protected RenderShape getRenderShape(BlockState state) {
return RenderShape.ENTITYBLOCK_ANIMATED;
}
@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPE;
}
@Override
public boolean isSignalSource(BlockState state) {
return true;
}
@Override
protected int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
return isAuthenticated(level, pos) ? 15 : 0;
}
@Override
protected int getDirectSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
return getSignal(state, level, pos, direction);
}
private static boolean isAuthenticated(BlockGetter level, BlockPos pos) {
return level.getBlockEntity(pos) instanceof ComputerBlockEntity computer && computer.isAuthenticated();
}
@Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
if (!state.is(newState.getBlock()) && level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
computer.dropDisks(level, pos);
if (level instanceof net.minecraft.server.level.ServerLevel serverLevel) {
AntmailServerData.access(serverLevel.getServer()).unregister(serverLevel.dimension().location(), pos);
}
}
super.onRemove(state, level, pos, newState, movedByPiston);
}
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new ComputerBlockEntity(pos, state, AntarchyObjects.COMPUTER_BLOCK_ENTITY);
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
return createTickerHelper(blockEntityType, AntarchyObjects.COMPUTER_BLOCK_ENTITY.get(), ComputerBlockEntity::tick);
}
@Override
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player,
InteractionHand hand, BlockHitResult hitResult) {
if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
if (level.isClientSide) {
if (stack.is(AntarchyObjects.FLOPPY_DISK.get())) {
return ItemInteractionResult.SUCCESS;
}
} else if (computer.insert(stack, player)) {
return ItemInteractionResult.CONSUME;
}
}
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
@Override
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player,
BlockHitResult hitResult) {
if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
computer.activate();
if (level.isClientSide) {
AntarchyClientHooks.openComputer(pos);
}
return net.minecraft.world.InteractionResult.SUCCESS;
}
return net.minecraft.world.InteractionResult.PASS;
}
}
@@ -6,6 +6,11 @@ import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.SpawnEggItem;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
@@ -62,6 +67,22 @@ public final class UndertrialSpawnerBlock extends BaseEntityBlock {
return new UndertrialSpawnerBlockEntity(pos, state, blockEntityType);
}
@Override
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
Player player, net.minecraft.world.InteractionHand hand,
BlockHitResult hitResult) {
if (!(stack.getItem() instanceof SpawnEggItem)) {
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
if (level.getBlockEntity(pos) instanceof UndertrialSpawnerBlockEntity spawner) {
if (!level.isClientSide) {
spawner.setSpawnEntity(((SpawnEggItem) stack.getItem()).getType(stack));
}
return ItemInteractionResult.sidedSuccess(level.isClientSide);
}
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
if (level.isClientSide) {
@@ -40,6 +40,28 @@ public class AntNestBlockEntity extends BlockEntity {
}
public static void serverTick(ServerLevel level, BlockPos pos, BlockState state, AntNestBlockEntity nest) {
if (state.getBlock() instanceof com.craisinlord.antarchy.content.block.AntTrapBlock) {
if (level.getGameTime() % 20L == 0L && nest.storedAnts.size() < nest.maxOccupants()) {
net.minecraft.world.phys.AABB searchArea = new net.minecraft.world.phys.AABB(pos).inflate(15.0D);
for (BaseAntEntity ant : level.getEntitiesOfClass(BaseAntEntity.class, searchArea, BaseAntEntity::isAlive)) {
if (ant.distanceToSqr(net.minecraft.world.phys.Vec3.atCenterOf(pos)) > 225.0D) {
continue;
}
if (!nest.canAccept(ant.getType())) {
continue;
}
if (ant.distanceToSqr(net.minecraft.world.phys.Vec3.atCenterOf(pos)) <= 2.25D) {
nest.tryStoreAnt(ant);
if (nest.storedAnts.size() >= nest.maxOccupants()) {
break;
}
} else {
ant.getNavigation().moveTo(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, 1.05D);
}
}
}
return;
}
boolean playerNearby = level.hasNearbyAlivePlayer(
pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, PLAYER_ACTIVATION_RANGE);
if (!playerNearby) {
@@ -115,12 +137,17 @@ public class AntNestBlockEntity extends BlockEntity {
}
public void releaseAll(ServerLevel level) {
while (!this.storedAnts.isEmpty()) {
this.releaseOne(level);
int remaining = this.storedAnts.size();
while (remaining-- > 0 && !this.storedAnts.isEmpty()) {
this.releaseOne(level, true);
}
}
private void releaseOne(ServerLevel level) {
this.releaseOne(level, false);
}
private void releaseOne(ServerLevel level, boolean forceRelease) {
if (this.storedAnts.isEmpty()) {
return;
}
@@ -132,20 +159,39 @@ public class AntNestBlockEntity extends BlockEntity {
return;
}
BlockPos spawnPos = this.getBlockPos().above();
if (!level.getBlockState(spawnPos).canBeReplaced()) {
BlockPos origin = this.getBlockPos();
java.util.List<BlockPos> candidates = new java.util.ArrayList<>();
for (int y = 1; y <= 3; y++) {
candidates.add(origin.above(y));
for (int radius = 1; radius <= 2; radius++) {
candidates.add(origin.offset(radius, y, 0));
candidates.add(origin.offset(-radius, y, 0));
candidates.add(origin.offset(0, y, radius));
candidates.add(origin.offset(0, y, -radius));
}
}
BlockPos spawnPos = null;
for (BlockPos candidate : candidates) {
antEntity.moveTo(candidate.getX() + SPAWN_OFFSET_XZ, candidate.getY() + SPAWN_OFFSET_Y, candidate.getZ() + SPAWN_OFFSET_XZ,
level.random.nextFloat() * 360.0F, 0.0F);
if (level.noCollision(antEntity)) {
spawnPos = candidate;
break;
}
}
if (spawnPos == null && !forceRelease) {
return;
}
antEntity.moveTo(spawnPos.getX() + SPAWN_OFFSET_XZ, spawnPos.getY() + SPAWN_OFFSET_Y, spawnPos.getZ() + SPAWN_OFFSET_XZ, level.random.nextFloat() * 360.0F, 0.0F);
if (!level.noCollision(antEntity)) {
return;
if (spawnPos == null) {
antEntity.moveTo(origin.getX() + SPAWN_OFFSET_XZ, origin.getY() + SPAWN_OFFSET_Y, origin.getZ() + SPAWN_OFFSET_XZ,
level.random.nextFloat() * 360.0F, 0.0F);
}
this.storedAnts.remove(0);
antEntity.setNestPos(this.getBlockPos());
antEntity.setNestPos(origin);
antEntity.onExitNest();
level.addFreshEntity(antEntity);
if (!level.addFreshEntity(antEntity) && !forceRelease) {
return;
}
this.storedAnts.remove(0);
this.setChanged();
}
@@ -1,458 +0,0 @@
package com.craisinlord.antarchy.content.block.entity;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antarchy.content.block.ComputerBlock;
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
import com.craisinlord.antarchy.content.computer.ComputerDesktopState;
import com.craisinlord.antarchy.content.guide.ComputerGuideData;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.Containers;
import net.minecraft.world.entity.player.Player;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import software.bernie.geckolib.animatable.GeoBlockEntity;
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.animation.AnimatableManager;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.animation.RawAnimation;
import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.util.GeckoLibUtil;
public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEntity {
private static final ResourceLocation INTRODUCTION_DISK = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
private static final String DISKS_TAG = "Disks";
private static final String ACTIVE_UNTIL_TAG = "ActiveUntil";
private static final String PASSWORD_TAG = "Password";
private static final String AUTHENTICATED_TAG = "Authenticated";
private static final String AUTHENTICATED_UNTIL_TAG = "AuthenticatedUntil";
private static final String DESKTOP_TAG = "Desktop";
private static final String BASILISK_SCORE_TAG = "BasiliskScore";
private static final String BASILISK_HIGH_SCORE_TAG = "BasiliskHighScore";
private static final String ANTMAN_SCORE_TAG = "AntmanScore";
private static final String ANTMAN_HIGH_SCORE_TAG = "AntmanHighScore";
private static final String BLOCKLE_DAY_TAG = "BlockleDay";
private static final String BLOCKLE_GUESSES_TAG = "BlockleGuesses";
private static final long AUTHENTICATION_TIMEOUT = 6000L;
private final Set<ResourceLocation> physicalDiskIds = new LinkedHashSet<>();
private final ComputerFileSystem fileSystem = new ComputerFileSystem();
private final ComputerDesktopState desktopState = new ComputerDesktopState();
private final AnimatableInstanceCache animatableCache = GeckoLibUtil.createInstanceCache(this);
private static final RawAnimation ON = RawAnimation.begin().thenLoop("on_state");
private static final RawAnimation OFF = RawAnimation.begin().thenLoop("off_state");
private static final RawAnimation TURN_ON = RawAnimation.begin().thenPlay("turn_on");
private static final RawAnimation TURN_OFF = RawAnimation.begin().thenPlay("turn_off");
private long activeUntil;
private String password = "";
private boolean authenticated;
private long authenticatedUntil;
private java.util.UUID activeUser;
private boolean lastActive;
private int basiliskScore;
private int basiliskHighScore;
private int antmanScore;
private int antmanHighScore;
private long blockleDay = -1L;
private final List<String> blockleGuesses = new ArrayList<>();
public ComputerBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<ComputerBlockEntity>> type) {
super(type.get(), pos, state);
}
public static void tick(Level level, BlockPos pos, BlockState state, ComputerBlockEntity computer) {
boolean active = computer.isActive();
if (!level.isClientSide && state.getValue(ComputerBlock.ACTIVE) != active) {
level.setBlock(pos, state.setValue(ComputerBlock.ACTIVE, active), 3);
}
if (active != computer.lastActive) {
computer.lastActive = active;
computer.triggerAnim("transitions", active ? "turn_on" : "turn_off");
}
if (!level.isClientSide && computer.activeUntil > 0L && level.getGameTime() >= computer.activeUntil && !level.hasNeighborSignal(pos)) {
computer.activeUntil = 0L;
computer.setChanged();
level.sendBlockUpdated(pos, state, state, 3);
}
if (!level.isClientSide && computer.authenticated && (level.getGameTime() >= computer.authenticatedUntil || computer.activeUser == null ||
(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getServer().getPlayerList().getPlayer(computer.activeUser) == null))) {
computer.clearAuthentication();
level.sendBlockUpdated(pos, state, state, 3);
}
}
public boolean insert(ItemStack stack, Player player) {
if (!stack.is(AntarchyObjects.FLOPPY_DISK.get())) {
return false;
}
ResourceLocation diskId = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
if (diskId == null || this.physicalDiskIds.contains(diskId)) {
stack.shrink(1);
activate();
return true;
}
this.physicalDiskIds.add(diskId);
ComputerGuideData.Disk disk = ComputerGuideData.disk(diskId);
if (disk != null && !disk.wallpaper().isBlank()) {
try {
desktopState.unlockWallpaper(ResourceLocation.parse(disk.wallpaper()));
} catch (RuntimeException ignored) {
}
}
stack.shrink(1);
activate();
setChanged();
if (level != null) {
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
}
return true;
}
public void activate() {
if (level != null && !level.isClientSide) {
activeUntil = Math.max(activeUntil, level.getGameTime() + 40L);
if (!getBlockState().getValue(ComputerBlock.ACTIVE)) {
level.setBlock(worldPosition, getBlockState().setValue(ComputerBlock.ACTIVE, true), 3);
}
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
}
}
public boolean isActive() {
return level != null && (level.hasNeighborSignal(worldPosition) || isAuthenticated());
}
@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "state", state -> state.setAndContinue(isActive() ? ON : OFF)));
controllers.add(new AnimationController<>(this, "transitions", state -> PlayState.STOP)
.triggerableAnim("turn_on", TURN_ON)
.triggerableAnim("turn_off", TURN_OFF));
}
@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return animatableCache;
}
public List<ResourceLocation> diskIds() {
List<ResourceLocation> ids = new ArrayList<>(physicalDiskIds.size() + 1);
ids.add(INTRODUCTION_DISK);
ids.addAll(physicalDiskIds);
return List.copyOf(ids);
}
public boolean ejectOne(Player player) {
if (physicalDiskIds.isEmpty() || !(player instanceof ServerPlayer serverPlayer)) {
return false;
}
return ejectOne(serverPlayer, physicalDiskIds.iterator().next());
}
public boolean ejectOne(ServerPlayer player, ResourceLocation diskId) {
if (level == null || level.isClientSide || diskId == null || !isAuthenticated() || !playerCanReach(player) || !physicalDiskIds.remove(diskId)) {
return false;
}
ItemStack disk = diskStack(diskId);
if (!player.addItem(disk)) {
player.drop(disk, false);
}
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
return true;
}
public void dropDisks(Level level, BlockPos pos) {
if (level.isClientSide) {
return;
}
for (ResourceLocation diskId : physicalDiskIds) {
Containers.dropItemStack(level, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, diskStack(diskId));
}
physicalDiskIds.clear();
}
public boolean hasPassword() {
return !password.isEmpty();
}
public boolean setPassword(ServerPlayer player, String newPassword) {
if (newPassword == null || newPassword.isBlank() || !isAuthenticated()) {
return false;
}
password = newPassword;
setChanged();
return true;
}
public boolean initializePassword(ServerPlayer player, String newPassword) {
if (hasPassword() || newPassword == null || newPassword.isBlank() || !claimUser(player)) {
return false;
}
password = newPassword;
authenticated = true;
authenticatedUntil = level.getGameTime() + AUTHENTICATION_TIMEOUT;
setChanged();
return true;
}
public boolean initializePassword(String newPassword) {
return false;
}
public boolean authenticate(ServerPlayer player, String attemptedPassword) {
if (!hasPassword() || !password.equals(attemptedPassword) || !claimUser(player)) {
return false;
}
authenticated = true;
authenticatedUntil = level == null ? 0L : level.getGameTime() + AUTHENTICATION_TIMEOUT;
setChanged();
return true;
}
public boolean authenticate(String attemptedPassword) {
return false;
}
public boolean isAuthenticated() {
if (!authenticated) {
return false;
}
if (level != null && level.getGameTime() >= authenticatedUntil) {
authenticated = false;
authenticatedUntil = 0L;
setChanged();
return false;
}
return true;
}
public boolean isAuthenticatedBy(ServerPlayer player) {
return isAuthenticated() && activeUser != null && activeUser.equals(player.getUUID());
}
public boolean claimUser(ServerPlayer player) {
if (level == null || level.isClientSide || !playerCanReach(player)) {
return false;
}
if (activeUser != null && !activeUser.equals(player.getUUID()) && isAuthenticated()) {
return false;
}
activeUser = player.getUUID();
return true;
}
public boolean playerCanReach(ServerPlayer player) {
return player.level() == level && player.distanceToSqr(worldPosition.getX() + 0.5D, worldPosition.getY() + 0.5D, worldPosition.getZ() + 0.5D) <= 64.0D;
}
public ComputerFileSystem fileSystem() {
return fileSystem;
}
public ComputerDesktopState desktopState() {
return desktopState;
}
public int basiliskScore() {
return basiliskScore;
}
public int basiliskHighScore() {
return basiliskHighScore;
}
public void setBasiliskScore(int score) {
basiliskScore = Math.max(0, score);
basiliskHighScore = Math.max(basiliskHighScore, basiliskScore);
setChanged();
}
public int antmanScore() { return antmanScore; }
public int antmanHighScore() { return antmanHighScore; }
public void setAntmanScore(int score) {
antmanScore = Math.max(0, score);
antmanHighScore = Math.max(antmanHighScore, antmanScore);
setChanged();
}
public long blockleDay() {
return blockleDay;
}
public List<String> blockleGuesses() {
return List.copyOf(blockleGuesses);
}
public void resetBlockle(long day) {
if (blockleDay == day) return;
blockleDay = day;
blockleGuesses.clear();
setChanged();
}
public boolean addBlockleGuess(String guess) {
if (blockleGuesses.size() >= 6) return false;
blockleGuesses.add(guess);
setChanged();
return true;
}
public boolean unlockWallpaper(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.unlockWallpaper(id));
}
public boolean installProgram(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.installProgram(id));
}
public boolean uninstallProgram(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.uninstallProgram(id));
}
public boolean selectWallpaper(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.selectWallpaper(id));
}
public boolean setIconPosition(ResourceLocation id, int x, int y) {
return serverDesktopMutation(() -> desktopState.setIconPosition(id, x, y));
}
public boolean removeIconPosition(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.removeIconPosition(id));
}
private boolean serverDesktopMutation(java.util.function.BooleanSupplier mutation) {
if (level == null || level.isClientSide) {
return false;
}
boolean changed = mutation.getAsBoolean();
if (changed) {
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
}
return changed;
}
public boolean canUseFileSystem(ServerPlayer player) {
return level != null && !level.isClientSide && isAuthenticated() && playerCanReach(player) && (activeUser == null || activeUser.equals(player.getUUID()));
}
public void logout() {
clearAuthentication();
}
public void releaseUser(ServerPlayer player) {
if (activeUser != null && activeUser.equals(player.getUUID())) {
activeUser = null;
setChanged();
}
}
public void clearAuthentication() {
authenticated = false;
authenticatedUntil = 0L;
activeUser = null;
setChanged();
}
public long authenticationTimeout() {
return AUTHENTICATION_TIMEOUT;
}
public boolean hasActiveUser() {
return activeUser != null && isAuthenticated();
}
private static ItemStack diskStack(ResourceLocation diskId) {
ItemStack stack = new ItemStack(AntarchyObjects.FLOPPY_DISK.get());
stack.set(AntarchyObjects.FLOPPY_DISK_COMPONENT.get(), diskId);
return stack;
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
physicalDiskIds.clear();
ListTag disks = tag.getList(DISKS_TAG, 8);
for (int index = 0; index < disks.size(); index++) {
try {
ResourceLocation diskId = ResourceLocation.parse(disks.getString(index));
if (!INTRODUCTION_DISK.equals(diskId)) {
physicalDiskIds.add(diskId);
}
} catch (Exception ignored) {
}
}
activeUntil = tag.getLong(ACTIVE_UNTIL_TAG);
password = tag.getString(PASSWORD_TAG);
fileSystem.load(tag, registries);
desktopState.load(tag.getCompound(DESKTOP_TAG));
basiliskScore = tag.getInt(BASILISK_SCORE_TAG);
basiliskHighScore = tag.getInt(BASILISK_HIGH_SCORE_TAG);
antmanScore = tag.getInt(ANTMAN_SCORE_TAG);
antmanHighScore = tag.getInt(ANTMAN_HIGH_SCORE_TAG);
blockleDay = tag.contains(BLOCKLE_DAY_TAG) ? tag.getLong(BLOCKLE_DAY_TAG) : -1L;
blockleGuesses.clear();
ListTag blockle = tag.getList(BLOCKLE_GUESSES_TAG, 8);
for (int index = 0; index < blockle.size(); index++) {
String guess = blockle.getString(index);
if (guess.length() == 5 && !blockleGuesses.contains(guess) && blockleGuesses.size() < 6) blockleGuesses.add(guess);
}
authenticated = false;
authenticatedUntil = 0L;
activeUser = null;
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
ListTag disks = new ListTag();
for (ResourceLocation diskId : physicalDiskIds) {
disks.add(StringTag.valueOf(diskId.toString()));
}
tag.put(DISKS_TAG, disks);
tag.putLong(ACTIVE_UNTIL_TAG, activeUntil);
tag.putString(PASSWORD_TAG, password);
fileSystem.save(tag, registries);
tag.put(DESKTOP_TAG, desktopState.save());
tag.putInt(BASILISK_SCORE_TAG, basiliskScore);
tag.putInt(BASILISK_HIGH_SCORE_TAG, basiliskHighScore);
tag.putInt(ANTMAN_SCORE_TAG, antmanScore);
tag.putInt(ANTMAN_HIGH_SCORE_TAG, antmanHighScore);
tag.putLong(BLOCKLE_DAY_TAG, blockleDay);
ListTag blockle = new ListTag();
for (String guess : blockleGuesses) blockle.add(StringTag.valueOf(guess));
tag.put(BLOCKLE_GUESSES_TAG, blockle);
}
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag();
tag.putBoolean("HasPassword", hasPassword());
tag.putBoolean(AUTHENTICATED_TAG, isAuthenticated());
ListTag disks = new ListTag();
for (ResourceLocation diskId : physicalDiskIds) disks.add(StringTag.valueOf(diskId.toString()));
tag.put(DISKS_TAG, disks);
return tag;
}
@Override
public net.minecraft.network.protocol.Packet<net.minecraft.network.protocol.game.ClientGamePacketListener> getUpdatePacket() {
return net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket.create(this);
}
}
@@ -30,6 +30,7 @@ import java.util.function.Supplier;
public final class UnderVaultBlockEntity extends BlockEntity {
private final VaultBlockEntity vaultData;
private boolean playerWasNearby;
public UnderVaultBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<UnderVaultBlockEntity>> type) {
super(type.get(), pos, state);
@@ -49,11 +50,12 @@ public final class UnderVaultBlockEntity extends BlockEntity {
// The vanilla vault detector can miss this ceiling-mounted wrapper because its
// block state has a separate render-facing property. Keep the state synchronized
// with the same four-block activation range before running vanilla's state machine.
if (state.getValue(UnderVaultBlock.STATE) == VaultState.INACTIVE
&& !level.getEntitiesOfClass(Player.class, new AABB(pos).inflate(4.0D), player -> !player.isSpectator()).isEmpty()) {
boolean playerNearby = !level.getEntitiesOfClass(Player.class, new AABB(pos).inflate(4.0D), player -> !player.isSpectator()).isEmpty();
if (state.getValue(UnderVaultBlock.STATE) == VaultState.INACTIVE && playerNearby && !blockEntity.playerWasNearby) {
state = state.setValue(UnderVaultBlock.STATE, VaultState.ACTIVE);
level.setBlockAndUpdate(pos, state);
}
blockEntity.playerWasNearby = playerNearby;
VaultBlockEntity.Server.tick(level, pos, state, blockEntity.vaultData.getConfig(), blockEntity.vaultData.getServerData(), blockEntity.vaultData.getSharedData());
}
@@ -20,6 +20,7 @@ import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawner;
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerConfig;
@@ -182,6 +183,16 @@ public final class UndertrialSpawnerBlockEntity extends BlockEntity implements T
return trialSpawner;
}
public void setSpawnEntity(EntityType<?> entityType) {
if (this.level == null) {
return;
}
// Match vanilla Trial Spawner interaction: override the next mob's entity id while
// preserving weighted potentials, ominous settings, and the trial's reward configuration.
this.trialSpawner.getData().setEntityId(this.trialSpawner, this.level.random, entityType);
this.markUpdated();
}
public double getClientSpin(float partialTick) {
return this.clientOldSpin + (this.clientSpin - this.clientOldSpin) * partialTick;
}
@@ -199,7 +210,7 @@ public final class UndertrialSpawnerBlockEntity extends BlockEntity implements T
private TrialSpawner createRewardlessSpawner(TrialSpawner source) {
TrialSpawner rewardless = new TrialSpawner(withoutLootTables(source.getNormalConfig()), withoutLootTables(source.getOminousConfig()),
source.getData(), source.getRequiredPlayerRange(), targetCooldownLength(source), this,
source.getData(), targetCooldownLength(source), source.getRequiredPlayerRange(), this,
source.getPlayerDetector(), source.getEntitySelector());
rewardless.overridePeacefulAndMobSpawnRule();
return rewardless;
@@ -1,21 +0,0 @@
package com.craisinlord.antarchy.content.client;
import net.minecraft.core.BlockPos;
import java.util.function.Consumer;
public final class AntarchyClientHooks {
private static Consumer<BlockPos> computerOpener = pos -> {
};
private AntarchyClientHooks() {
}
public static void setComputerOpener(Consumer<BlockPos> opener) {
computerOpener = opener;
}
public static void openComputer(BlockPos pos) {
computerOpener.accept(pos);
}
}
@@ -0,0 +1,32 @@
package com.craisinlord.antarchy.content.client;
import com.craisinlord.antarchy.content.network.AntarchyGamePayload;
import com.craisinlord.antarchy.content.network.AntarchyGameResultPayload;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.core.BlockPos;
/** Client state for Antarchy-owned games hosted by AntOS. */
public final class AntarchyGameClientState {
private static final Map<String, String> GAME_STATES = new ConcurrentHashMap<>();
private AntarchyGameClientState() { }
public static void update(AntarchyGameResultPayload result) {
String[] envelope = result.data().split("\u0000", 3);
if (envelope.length < 3) return;
if (envelope[0].equals(Integer.toString(AntarchyGamePayload.BASILISK_STATE))
|| envelope[0].equals(Integer.toString(AntarchyGamePayload.ANTMAN_STATE))) {
GAME_STATES.put(key(result.pos(), envelope[0]),
result.success() ? envelope[2] : "");
} else if (envelope[0].equals(Integer.toString(AntarchyGamePayload.BLOCKLE_STATE))
|| envelope[0].equals(Integer.toString(AntarchyGamePayload.BLOCKLE_GUESS))) {
if (!result.success()) return;
BlockleClientState.update(result);
}
}
public static String getGameState(BlockPos pos, int action) { return GAME_STATES.get(key(pos, Integer.toString(action))); }
public static void clearGameState(BlockPos pos, int action) { GAME_STATES.remove(key(pos, Integer.toString(action))); }
private static String key(BlockPos pos, String action) { return pos.asLong() + "|" + action; }
}
@@ -1,81 +0,0 @@
package com.craisinlord.antarchy.content.client;
import com.craisinlord.antarchy.content.network.AntmailResultPayload;
import com.craisinlord.antarchy.content.antmail.AntmailDebug;
import com.craisinlord.antarchy.content.antmail.AntmailMessage;
import net.minecraft.core.BlockPos;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class AntmailClientState {
private static final int MAX_CACHED_DETAILS_PER_MAILBOX = 8;
private static final Map<BlockPos, AntmailResultPayload> RESULTS = new ConcurrentHashMap<>();
private static final Map<BlockPos, AntmailResultPayload> MAILBOXES = new ConcurrentHashMap<>();
private static final Map<BlockPos, Long> VERSIONS = new ConcurrentHashMap<>();
private static final Map<BlockPos, Map<java.util.UUID, AntmailMessage>> DETAILS = new ConcurrentHashMap<>();
private AntmailClientState() {
}
public static void update(AntmailResultPayload payload) {
AntmailDebug.log("S2C result pos=" + payload.pos() + " status=" + payload.status() + " detail=" + payload.detail()
+ " addressPresent=" + !payload.address().isBlank() + " dataChars=" + payload.data().length()
+ " version=" + payload.version());
RESULTS.put(payload.pos(), payload);
if ("unchanged".equals(payload.detail())) {
if (MAILBOXES.containsKey(payload.pos())) {
VERSIONS.put(payload.pos(), payload.version());
} else {
// An unchanged response cannot initialize a client that has no cached snapshot.
// Keep the next poll unconditional instead of getting stuck on "unchanged".
AntmailDebug.log("received unchanged without cached mailbox at " + payload.pos() + "; forcing full snapshot on next poll");
VERSIONS.remove(payload.pos());
}
return;
} else if ("message_detail".equals(payload.detail()) && !payload.data().isBlank()) {
try {
AntmailMessage message = AntmailMessage.fromTag(com.craisinlord.antarchy.content.antmail.AntmailWire.decodeTag(payload.data()));
Map<java.util.UUID, AntmailMessage> details = DETAILS.computeIfAbsent(payload.pos(), ignored -> new ConcurrentHashMap<>());
details.put(message.id(), message);
while (details.size() > MAX_CACHED_DETAILS_PER_MAILBOX) {
java.util.UUID oldest = details.keySet().iterator().next();
details.remove(oldest);
}
} catch (RuntimeException ignored) {
}
} else if (!payload.data().isBlank()) {
MAILBOXES.put(payload.pos(), payload);
VERSIONS.put(payload.pos(), payload.version());
} else {
// A failed or unconfigured response is authoritative; never show a
// mailbox from a previous address/computer at this position.
MAILBOXES.remove(payload.pos());
VERSIONS.remove(payload.pos());
}
}
public static void clear(BlockPos pos) {
RESULTS.remove(pos);
MAILBOXES.remove(pos);
VERSIONS.remove(pos);
DETAILS.remove(pos);
}
public static AntmailResultPayload get(BlockPos pos) {
return RESULTS.get(pos);
}
public static AntmailResultPayload getMailbox(BlockPos pos) {
return MAILBOXES.get(pos);
}
public static long getVersion(BlockPos pos) {
return VERSIONS.getOrDefault(pos, 0L);
}
public static AntmailMessage getMessage(BlockPos pos, java.util.UUID id) {
Map<java.util.UUID, AntmailMessage> details = DETAILS.get(pos);
return details == null ? null : details.get(id);
}
}
@@ -1,22 +1,22 @@
package com.craisinlord.antarchy.content.client;
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
import com.craisinlord.antarchy.content.network.AntarchyGameResultPayload;
import net.minecraft.core.BlockPos;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class BlockleClientState {
private static final Map<BlockPos, ComputerAccessResultPayload> RESULTS = new ConcurrentHashMap<>();
private static final Map<BlockPos, AntarchyGameResultPayload> RESULTS = new ConcurrentHashMap<>();
private BlockleClientState() {
}
public static void update(ComputerAccessResultPayload result) {
public static void update(AntarchyGameResultPayload result) {
RESULTS.put(result.pos(), result);
}
public static ComputerAccessResultPayload get(BlockPos pos) {
public static AntarchyGameResultPayload get(BlockPos pos) {
return RESULTS.get(pos);
}
@@ -1,39 +0,0 @@
package com.craisinlord.antarchy.content.client;
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
import net.minecraft.core.BlockPos;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class ComputerAccessClientState {
private static final Map<BlockPos, ComputerAccessResultPayload> RESULTS = new ConcurrentHashMap<>();
private ComputerAccessClientState() {
}
public static void update(ComputerAccessResultPayload result) {
ComputerStructureLocatorClientState.update(result);
if (result.data().startsWith(com.craisinlord.antarchy.content.network.ComputerAccessPayload.ARCHIVE_STATE + "\0")) {
String[] envelope = result.data().split("\u0000", 3);
if (envelope.length == 3) {
com.craisinlord.antarchy.content.guide.ComputerGuideData.applyNetworkSnapshot(envelope[2]);
}
}
if (result.data().startsWith("18\u0000") || result.data().startsWith("19\u0000")) {
BlockleClientState.update(result);
}
// File, terminal, desktop, and wallpaper responses carry their own operation
// status. They must not replace the last authoritative login state.
if (result.data().isBlank()) RESULTS.put(result.pos(), result);
ComputerFileSystemClientState.update(result);
}
public static void clear(BlockPos pos) {
RESULTS.remove(pos);
}
public static ComputerAccessResultPayload get(BlockPos pos) {
return RESULTS.get(pos);
}
}
@@ -1,94 +0,0 @@
package com.craisinlord.antarchy.content.client;
import com.craisinlord.antarchy.content.network.ComputerAccessPayload;
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
import net.minecraft.core.BlockPos;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class ComputerFileSystemClientState {
private static final Map<BlockPos, State> STATES = new ConcurrentHashMap<>();
private ComputerFileSystemClientState() {
}
public static void update(ComputerAccessResultPayload payload) {
if (payload.data().isEmpty()) return;
String[] envelope = payload.data().split("\u0000", 3);
if (envelope.length < 3) return;
int action;
try {
action = Integer.parseInt(envelope[0]);
} catch (NumberFormatException ignored) {
return;
}
// Terminal, desktop, and game responses use the same payload envelope,
// but their status must not leak into the file explorer's shared state.
if (action != ComputerAccessPayload.FILE_LIST
&& action != ComputerAccessPayload.FILE_OPEN
&& action != ComputerAccessPayload.FILE_CREATE
&& action != ComputerAccessPayload.FILE_SAVE
&& action != ComputerAccessPayload.FILE_DELETE
&& action != ComputerAccessPayload.FILE_MOVE) {
return;
}
State state = STATES.computeIfAbsent(payload.pos(), ignored -> new State());
state.success = payload.result() == ComputerAccessResultPayload.SUCCESS;
state.error = envelope[1];
if (state.success) state.error = "";
if (action == ComputerAccessPayload.FILE_CREATE || action == ComputerAccessPayload.FILE_SAVE
|| action == ComputerAccessPayload.FILE_DELETE || action == ComputerAccessPayload.FILE_MOVE) {
state.lastMutationAction = action;
state.lastMutationPath = envelope[2];
state.lastMutationSuccess = state.success;
state.lastMutationError = envelope[1];
}
if (action == ComputerAccessPayload.FILE_LIST) {
state.files = new ArrayList<>();
if (state.success && !envelope[2].isEmpty()) state.files.addAll(List.of(envelope[2].split("\\n")));
} else if (action == ComputerAccessPayload.FILE_OPEN) {
if (state.success) {
String[] file = envelope[2].split("\u0000", 2);
if (file.length == 2) {
state.openedPath = file[0];
state.openedContents = file[1];
}
} else {
state.openedPath = "";
state.openedContents = "";
}
} else if ((action == ComputerAccessPayload.FILE_DELETE || action == ComputerAccessPayload.FILE_MOVE) && state.success) {
state.openedPath = "";
state.openedContents = "";
}
}
public static State get(BlockPos pos) {
return STATES.computeIfAbsent(pos, ignored -> new State());
}
public static final class State {
private List<String> files = List.of();
private String openedPath = "";
private String openedContents = "";
private String error = "";
private boolean success;
private int lastMutationAction = -1;
private String lastMutationPath = "";
private boolean lastMutationSuccess;
private String lastMutationError = "";
public List<String> files() { return List.copyOf(files); }
public String openedPath() { return openedPath; }
public String openedContents() { return openedContents; }
public String error() { return error; }
public boolean success() { return success; }
public int lastMutationAction() { return lastMutationAction; }
public String lastMutationPath() { return lastMutationPath; }
public boolean lastMutationSuccess() { return lastMutationSuccess; }
public String lastMutationError() { return lastMutationError; }
}
}
@@ -1,43 +0,0 @@
package com.craisinlord.antarchy.content.client;
import com.craisinlord.antarchy.content.network.ComputerAccessPayload;
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
import net.minecraft.core.BlockPos;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class ComputerStructureLocatorClientState {
private static final Map<BlockPos, String> RESULTS = new ConcurrentHashMap<>();
private ComputerStructureLocatorClientState() {
}
public static void searching(BlockPos pos, String dimensionId) {
String dimension = dimensionId == null || dimensionId.isBlank() ? "TARGET DIMENSION" : dimensionId.toUpperCase(java.util.Locale.ROOT);
RESULTS.put(pos, "SEARCHING IN " + dimension + "...");
}
public static void update(ComputerAccessResultPayload payload) {
String[] envelope = payload.data().split("\u0000", 3);
if (envelope.length != 3) return;
try {
if (Integer.parseInt(envelope[0]) != ComputerAccessPayload.LOCATE_STRUCTURE) return;
} catch (NumberFormatException ignored) {
return;
}
String message = switch (envelope[1]) {
case "not_found" -> "NO STRUCTURE FOUND WITHIN RANGE";
case "dimension_unavailable" -> "TARGET DIMENSION UNAVAILABLE";
default -> "LOCATOR UNAVAILABLE";
};
RESULTS.put(payload.pos(), payload.result() == ComputerAccessResultPayload.SUCCESS
? "STRUCTURE START // " + envelope[2]
: message);
}
public static String get(BlockPos pos) {
return RESULTS.getOrDefault(pos, "COORDINATES // SEARCH WHEN SELECTED");
}
}
@@ -0,0 +1,79 @@
package com.craisinlord.antarchy.content.client.game;
import com.craisinlord.antos.api.client.game.ComputerGame;
import com.craisinlord.antos.api.client.game.ComputerGameRegistry;
import com.craisinlord.antarchy.content.network.AntarchyGameNetworking;
import com.craisinlord.antarchy.content.network.AntarchyGamePayload;
import com.craisinlord.antarchy.content.client.AntarchyGameClientState;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
/** Registers Antarchy's native games with the AntOS Games app. */
public final class AntarchyComputerGames {
private AntarchyComputerGames() {}
public static void register() {
ComputerGameRegistry.register(game("basilisk", "BASILISK", pos -> {
BasiliskProgram program = new BasiliskProgram(true, 0, 0, score -> AntarchyGameNetworking.saveBasiliskScore(pos, score));
var loaded = new java.util.concurrent.atomic.AtomicBoolean();
AntarchyGameNetworking.requestBasiliskState(pos);
return new GameSession(() -> { if (!loaded.get()) loaded.set(loadScores(pos, AntarchyGamePayload.BASILISK_STATE, program::setStoredScores)); program.tick(); }, program::render, key -> loaded.get() && program.keyPressed(key), ignored -> false);
}));
ComputerGameRegistry.register(game("antman", "ANTMAN.EXE", pos -> {
AntmanProgram program = new AntmanProgram(true, 0, 0, score -> AntarchyGameNetworking.saveAntmanScore(pos, score));
var loaded = new java.util.concurrent.atomic.AtomicBoolean();
AntarchyGameNetworking.requestAntmanState(pos);
return new GameSession(() -> { if (!loaded.get()) loaded.set(loadScores(pos, AntarchyGamePayload.ANTMAN_STATE, program::setStoredScores)); program.tick(); }, program::render, key -> loaded.get() && program.keyPressed(key), ignored -> false);
}));
ComputerGameRegistry.register(game("blockle", "BLOCKLE", pos -> {
BlockleProgram program = new BlockleProgram(pos);
program.setInstalled(true);
return new GameSession(program::tick, program::render, program::keyPressed, program::charTyped);
}));
}
private static boolean loadScores(BlockPos pos, int action, java.util.function.BiConsumer<Integer, Integer> setter) {
String response = AntarchyGameClientState.getGameState(pos, action);
if (response == null) return false;
if (response.isBlank()) { AntarchyGameClientState.clearGameState(pos, action); return true; }
String[] scores = response.split("\0", 2);
if (scores.length != 2) return false;
try { setter.accept(Integer.parseInt(scores[0]), Integer.parseInt(scores[1])); }
catch (NumberFormatException ignored) { return false; }
AntarchyGameClientState.clearGameState(pos, action);
return true;
}
private static ComputerGame game(String path, String title, ProgramFactory factory) {
ResourceLocation id = ResourceLocation.fromNamespaceAndPath("antarchy", path);
ResourceLocation disk = ResourceLocation.fromNamespaceAndPath("antarchy", path + "_game");
return new ComputerGame() {
@Override public ResourceLocation id() { return id; }
@Override public ResourceLocation diskId() { return disk; }
@Override public String title() { return title; }
@Override public Session create(BlockPos position) { return factory.create(position); }
};
}
@FunctionalInterface
private interface ProgramFactory { ComputerGame.Session create(BlockPos position); }
@FunctionalInterface private interface GameRenderer { void render(GuiGraphics graphics, Font font, int x, int y, int width, int height); }
private static final class GameSession implements ComputerGame.Session {
private final Runnable ticker;
private final GameRenderer renderer;
private final IntPredicate keyHandler;
private final Predicate<Character> charHandler;
private GameSession(Runnable ticker, GameRenderer renderer, IntPredicate keyHandler, Predicate<Character> charHandler) {
this.ticker = ticker; this.renderer = renderer; this.keyHandler = keyHandler; this.charHandler = charHandler;
}
@Override public void tick() { ticker.run(); }
@Override public void render(GuiGraphics graphics, Font font, int x, int y, int width, int height) { renderer.render(graphics, font, x, y, width, height); }
@Override public boolean keyPressed(int keyCode) { return keyHandler.test(keyCode); }
@Override public boolean charTyped(char codePoint) { return charHandler.test(codePoint); }
}
}
@@ -1,8 +1,8 @@
package com.craisinlord.antarchy.content.client.game;
import com.craisinlord.antarchy.content.client.BlockleClientState;
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
import com.craisinlord.antarchy.content.network.ComputerNetworking;
import com.craisinlord.antarchy.content.network.AntarchyGameResultPayload;
import com.craisinlord.antarchy.content.network.AntarchyGameNetworking;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.BlockPos;
@@ -40,11 +40,11 @@ public final class BlockleProgram {
}
public void requestState() {
if (installed) ComputerNetworking.requestBlockleState(position);
if (installed) AntarchyGameNetworking.requestBlockleState(position);
}
public void tick() {
ComputerAccessResultPayload result = BlockleClientState.get(position);
AntarchyGameResultPayload result = BlockleClientState.get(position);
if (result == null) return;
if (result.data().equals(lastResponse)) return;
lastResponse = result.data();
@@ -72,7 +72,7 @@ public final class BlockleProgram {
if (keyCode == 257 || keyCode == 335) {
if (!received) return true;
if (input.length() == 5 && phase.equals("PLAYING") && !pending) {
ComputerNetworking.submitBlockleGuess(position, input);
AntarchyGameNetworking.submitBlockleGuess(position, input);
pending = true;
status = "SUBMITTING";
}
@@ -1,27 +0,0 @@
package com.craisinlord.antarchy.content.client.model;
import com.craisinlord.antarchy.Antarchy;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import net.minecraft.resources.ResourceLocation;
import software.bernie.geckolib.model.GeoModel;
public final class ComputerModel extends GeoModel<ComputerBlockEntity> {
private static final ResourceLocation MODEL = ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "geo/computer.geo.json");
private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "textures/block/computer/computer.png");
private static final ResourceLocation ANIMATION = ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "animations/computer.animation.json");
@Override
public ResourceLocation getModelResource(ComputerBlockEntity animatable) {
return MODEL;
}
@Override
public ResourceLocation getTextureResource(ComputerBlockEntity animatable) {
return TEXTURE;
}
@Override
public ResourceLocation getAnimationResource(ComputerBlockEntity animatable) {
return ANIMATION;
}
}
@@ -0,0 +1,52 @@
package com.craisinlord.antarchy.content.client.particle;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.particle.Particle;
import net.minecraft.client.particle.ParticleProvider;
import net.minecraft.client.particle.ParticleRenderType;
import net.minecraft.client.particle.SpriteSet;
import net.minecraft.client.particle.TextureSheetParticle;
import net.minecraft.core.particles.SimpleParticleType;
public final class BugSprayParticle extends TextureSheetParticle {
private final SpriteSet sprites;
private BugSprayParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SpriteSet sprites) {
super(level, x, y, z, xd, yd, zd);
this.sprites = sprites;
this.setSprite(sprites.get(level.random));
this.lifetime = 14 + this.random.nextInt(8);
this.quadSize = 0.16F + this.random.nextFloat() * 0.14F;
this.friction = 0.88F;
this.gravity = -0.01F;
this.alpha = 0.9F;
}
@Override
public void tick() {
super.tick();
if (!this.removed) {
this.setSprite(this.sprites.get(Math.min(this.age, this.lifetime - 1), this.lifetime));
this.alpha = 0.9F * (1.0F - (float) this.age / this.lifetime);
}
}
@Override
public ParticleRenderType getRenderType() {
return ParticleRenderType.PARTICLE_SHEET_TRANSLUCENT;
}
public static final class Provider implements ParticleProvider<SimpleParticleType> {
private final SpriteSet sprites;
public Provider(SpriteSet sprites) {
this.sprites = sprites;
}
@Override
public Particle createParticle(SimpleParticleType type, ClientLevel level, double x, double y, double z,
double xd, double yd, double zd) {
return new BugSprayParticle(level, x, y, z, xd, yd, zd, this.sprites);
}
}
}
@@ -1,17 +0,0 @@
package com.craisinlord.antarchy.content.client.renderer;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import com.craisinlord.antarchy.content.client.model.ComputerModel;
import net.minecraft.core.Direction;
import software.bernie.geckolib.renderer.GeoBlockRenderer;
public final class ComputerRenderer extends GeoBlockRenderer<ComputerBlockEntity> {
public ComputerRenderer(net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context context) {
super(new ComputerModel());
}
@Override
protected Direction getFacing(ComputerBlockEntity computer) {
return computer.getBlockState().getValue(com.craisinlord.antarchy.content.block.ComputerBlock.FACING);
}
}
@@ -1,86 +0,0 @@
package com.craisinlord.antarchy.content.client.renderer;
import com.craisinlord.antarchy.content.item.RoyalAssailantBattleAxeItem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.world.entity.HumanoidArm;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
public final class RoyalAssailantAxeAfterimageLayer extends RenderLayer<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>> {
private static final int[] TINTS = {
0x66FFD966,
0x4DFFC94D,
0x33FFB52E,
0x1FFF8C14
};
public RoyalAssailantAxeAfterimageLayer(RenderLayerParent<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>> renderer) {
super(renderer);
}
@Override
public void render(PoseStack poseStack, MultiBufferSource buffer, int packedLight, AbstractClientPlayer player,
float limbSwing, float limbSwingAmount, float partialTick, float ageInTicks, float netHeadYaw,
float headPitch) {
if (player.isSpectator() || player.isInvisible() || player.getAttackAnim(partialTick) <= 0.01F) {
return;
}
ItemStack stack = player.getMainHandItem();
HumanoidArm arm = player.getMainArm();
if (!(stack.getItem() instanceof RoyalAssailantBattleAxeItem)) {
stack = player.getOffhandItem();
arm = player.getMainArm() == HumanoidArm.RIGHT ? HumanoidArm.LEFT : HumanoidArm.RIGHT;
}
if (!(stack.getItem() instanceof RoyalAssailantBattleAxeItem)) {
return;
}
VertexConsumer vertexConsumer = buffer.getBuffer(RenderType.entityTranslucent(this.getTextureLocation(player)));
float swing = player.getAttackAnim(partialTick);
for (int i = TINTS.length - 1; i >= 0; i--) {
poseStack.pushPose();
float offset = (i + 1) * 0.08F;
poseStack.translate(0.0D, 0.0D, offset);
poseStack.mulPose(Axis.YP.rotation((i + 1) * 0.12F * (swing < 0.5F ? -1.0F : 1.0F)));
this.getParentModel().renderToBuffer(
poseStack,
vertexConsumer,
LightTexture.FULL_BRIGHT,
LivingEntityRenderer.getOverlayCoords(player, 0.0F),
TINTS[i]
);
poseStack.popPose();
}
for (int i = TINTS.length - 1; i >= 0; i--) {
poseStack.pushPose();
this.getParentModel().translateToHand(arm, poseStack);
poseStack.mulPose(Axis.YP.rotation((i + 1) * 0.12F * (swing < 0.5F ? -1.0F : 1.0F)));
Minecraft.getInstance().getItemRenderer().renderStatic(
player,
stack,
arm == HumanoidArm.RIGHT ? ItemDisplayContext.THIRD_PERSON_RIGHT_HAND : ItemDisplayContext.THIRD_PERSON_LEFT_HAND,
arm == HumanoidArm.LEFT,
poseStack,
buffer,
player.level(),
packedLight,
LivingEntityRenderer.getOverlayCoords(player, 0.0F),
player.getId()
);
poseStack.popPose();
}
}
}
@@ -49,11 +49,20 @@ public final class RoyalIndicatorRenderer {
private static void crownPoints(VertexConsumer out, PoseStack.Pose pose, float radius, float rotation,
float r, float g, float b, float a) {
for (int i = 0; i < 3; i++) {
double angle = rotation + Math.PI * 2.0D * i / 3.0D;
float x = (float) Math.cos(angle) * radius;
float z = (float) Math.sin(angle) * radius;
line(out, pose, x, 0.0F, z, x * 0.68F, 0.19F, z * 0.68F, r, g, b, a);
final int spikes = 5;
float innerRadius = radius * 0.72F;
for (int i = 0; i < spikes; i++) {
double outerAngle = rotation + Math.PI * 2.0D * i / spikes;
double innerAngle = rotation + Math.PI * 2.0D * (i + 0.5D) / spikes;
float outerX = (float) Math.cos(outerAngle) * radius;
float outerZ = (float) Math.sin(outerAngle) * radius;
float innerX = (float) Math.cos(innerAngle) * innerRadius;
float innerZ = (float) Math.sin(innerAngle) * innerRadius;
float nextOuterX = (float) Math.cos(rotation + Math.PI * 2.0D * (i + 1) / spikes) * radius;
float nextOuterZ = (float) Math.sin(rotation + Math.PI * 2.0D * (i + 1) / spikes) * radius;
line(out, pose, outerX, 0.0F, outerZ, innerX, 0.34F, innerZ, r, g, b, a);
line(out, pose, innerX, 0.34F, innerZ, nextOuterX, 0.0F, nextOuterZ, r, g, b, a);
line(out, pose, outerX, 0.0F, outerZ, nextOuterX, 0.0F, nextOuterZ, r, g, b, a);
}
}
@@ -0,0 +1,75 @@
package com.craisinlord.antarchy.content.computer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.saveddata.SavedData;
/** Persistent Antarchy game progress keyed by AntOS computer location. */
public final class AntarchyGameSavedData extends SavedData {
private static final String ID = "antarchy_computer_games";
private final Map<String, GameState> games = new HashMap<>();
public static AntarchyGameSavedData create() { return new AntarchyGameSavedData(); }
public static AntarchyGameSavedData load(CompoundTag tag, HolderLookup.Provider registries) {
AntarchyGameSavedData data = new AntarchyGameSavedData();
for (net.minecraft.nbt.Tag raw : tag.getList("Games", net.minecraft.nbt.Tag.TAG_COMPOUND)) {
CompoundTag item = (CompoundTag) raw;
GameState state = new GameState();
state.basiliskScore = item.getInt("BasiliskScore");
state.basiliskHighScore = item.getInt("BasiliskHighScore");
state.antmanScore = item.getInt("AntmanScore");
state.antmanHighScore = item.getInt("AntmanHighScore");
state.blockleDay = item.getLong("BlockleDay");
for (net.minecraft.nbt.Tag guess : item.getList("BlockleGuesses", net.minecraft.nbt.Tag.TAG_STRING)) state.blockleGuesses.add(guess.getAsString());
data.games.put(item.getString("Computer"), state);
}
return data;
}
@Override public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
ListTag list = new ListTag();
games.forEach((key, state) -> {
CompoundTag item = new CompoundTag();
item.putString("Computer", key);
item.putInt("BasiliskScore", state.basiliskScore);
item.putInt("BasiliskHighScore", state.basiliskHighScore);
item.putInt("AntmanScore", state.antmanScore);
item.putInt("AntmanHighScore", state.antmanHighScore);
item.putLong("BlockleDay", state.blockleDay);
ListTag guesses = new ListTag();
state.blockleGuesses.forEach(guess -> guesses.add(net.minecraft.nbt.StringTag.valueOf(guess)));
item.put("BlockleGuesses", guesses);
list.add(item);
});
tag.put("Games", list);
return tag;
}
public static GameState get(MinecraftServer server, String key) {
AntarchyGameSavedData data = server.overworld().getDataStorage().computeIfAbsent(
new SavedData.Factory<>(AntarchyGameSavedData::create, AntarchyGameSavedData::load, null), ID);
return data.games.computeIfAbsent(key, ignored -> new GameState());
}
public static void changed(MinecraftServer server) {
server.overworld().getDataStorage().computeIfAbsent(
new SavedData.Factory<>(AntarchyGameSavedData::create, AntarchyGameSavedData::load, null), ID).setDirty();
}
public static String key(net.minecraft.resources.ResourceLocation dimension, net.minecraft.core.BlockPos pos) {
return dimension + "|" + pos.asLong();
}
public static final class GameState {
public int basiliskScore, basiliskHighScore, antmanScore, antmanHighScore;
public long blockleDay = Long.MIN_VALUE;
public final List<String> blockleGuesses = new ArrayList<>();
}
}
@@ -1,197 +0,0 @@
package com.craisinlord.antarchy.content.computer;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.resources.ResourceLocation;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
public final class ComputerDesktopState {
public static final ResourceLocation DEFAULT_WALLPAPER = ResourceLocation.fromNamespaceAndPath("antarchy", "grid_ant");
public static final int MAX_UNLOCKED_WALLPAPERS = 64;
public static final int MAX_INSTALLED_PROGRAMS = 64;
public static final int MAX_ICON_POSITIONS = 64;
public static final int MAX_ID_LENGTH = 128;
public static final int MIN_ICON_COORDINATE = 0;
public static final int MAX_ICON_COORDINATE = 4096;
private static final String SELECTED_WALLPAPER_TAG = "SelectedWallpaper";
private static final String UNLOCKED_WALLPAPERS_TAG = "UnlockedWallpapers";
private static final String INSTALLED_PROGRAMS_TAG = "InstalledPrograms";
private static final String ICON_POSITIONS_TAG = "IconPositions";
private static final String ID_TAG = "Id";
private static final String X_TAG = "X";
private static final String Y_TAG = "Y";
private ResourceLocation selectedWallpaper;
private final Set<ResourceLocation> unlockedWallpaperIds = new LinkedHashSet<>();
private final Set<ResourceLocation> installedProgramIds = new LinkedHashSet<>();
private final Map<ResourceLocation, IconPosition> iconPositions = new LinkedHashMap<>();
public ResourceLocation selectedWallpaper() {
return selectedWallpaper;
}
public Set<ResourceLocation> unlockedWallpaperIds() {
return Collections.unmodifiableSet(new LinkedHashSet<>(unlockedWallpaperIds));
}
public Set<ResourceLocation> installedProgramIds() {
return Collections.unmodifiableSet(new LinkedHashSet<>(installedProgramIds));
}
public Map<ResourceLocation, IconPosition> iconPositions() {
return Collections.unmodifiableMap(new LinkedHashMap<>(iconPositions));
}
public boolean hasWallpaper(ResourceLocation id) {
return id != null && unlockedWallpaperIds.contains(id);
}
public boolean hasProgram(ResourceLocation id) {
return id != null && installedProgramIds.contains(id);
}
public IconPosition iconPosition(ResourceLocation id) {
return iconPositions.get(id);
}
public boolean unlockWallpaper(ResourceLocation id) {
if (!validId(id) || unlockedWallpaperIds.contains(id)) {
return false;
}
if (unlockedWallpaperIds.size() >= MAX_UNLOCKED_WALLPAPERS) {
return false;
}
return unlockedWallpaperIds.add(id);
}
public boolean installProgram(ResourceLocation id) {
if (!validId(id) || installedProgramIds.size() >= MAX_INSTALLED_PROGRAMS && !installedProgramIds.contains(id)) {
return false;
}
return installedProgramIds.add(id);
}
public boolean uninstallProgram(ResourceLocation id) {
if (id == null || !installedProgramIds.remove(id)) {
return false;
}
iconPositions.remove(id);
return true;
}
public boolean selectWallpaper(ResourceLocation id) {
if (id != null && !unlockedWallpaperIds.contains(id)) {
return false;
}
if (id == null ? selectedWallpaper == null : id.equals(selectedWallpaper)) {
return false;
}
selectedWallpaper = id;
return true;
}
public boolean setIconPosition(ResourceLocation id, int x, int y) {
if (!validId(id) || x < MIN_ICON_COORDINATE || x > MAX_ICON_COORDINATE || y < MIN_ICON_COORDINATE || y > MAX_ICON_COORDINATE) {
return false;
}
IconPosition position = new IconPosition(x, y);
if (position.equals(iconPositions.get(id))) {
return false;
}
if (!iconPositions.containsKey(id) && iconPositions.size() >= MAX_ICON_POSITIONS) {
return false;
}
iconPositions.put(id, position);
return true;
}
public boolean removeIconPosition(ResourceLocation id) {
return id != null && iconPositions.remove(id) != null;
}
public void clear() {
unlockedWallpaperIds.clear();
installedProgramIds.clear();
iconPositions.clear();
unlockedWallpaperIds.add(DEFAULT_WALLPAPER);
selectedWallpaper = DEFAULT_WALLPAPER;
}
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
if (selectedWallpaper != null) {
tag.putString(SELECTED_WALLPAPER_TAG, selectedWallpaper.toString());
}
tag.put(UNLOCKED_WALLPAPERS_TAG, saveIds(unlockedWallpaperIds));
tag.put(INSTALLED_PROGRAMS_TAG, saveIds(installedProgramIds));
ListTag positions = new ListTag();
for (Map.Entry<ResourceLocation, IconPosition> entry : iconPositions.entrySet()) {
CompoundTag position = new CompoundTag();
position.putString(ID_TAG, entry.getKey().toString());
position.putInt(X_TAG, entry.getValue().x());
position.putInt(Y_TAG, entry.getValue().y());
positions.add(position);
}
tag.put(ICON_POSITIONS_TAG, positions);
return tag;
}
public void load(CompoundTag tag) {
clear();
loadIds(tag.getList(UNLOCKED_WALLPAPERS_TAG, 8), unlockedWallpaperIds, MAX_UNLOCKED_WALLPAPERS);
ResourceLocation storedSelection = parseId(tag.getString(SELECTED_WALLPAPER_TAG));
selectedWallpaper = storedSelection != null && unlockedWallpaperIds.contains(storedSelection) ? storedSelection : DEFAULT_WALLPAPER;
loadIds(tag.getList(INSTALLED_PROGRAMS_TAG, 8), installedProgramIds, MAX_INSTALLED_PROGRAMS);
ListTag positions = tag.getList(ICON_POSITIONS_TAG, 10);
for (int index = 0; index < positions.size() && iconPositions.size() < MAX_ICON_POSITIONS; index++) {
CompoundTag stored = positions.getCompound(index);
ResourceLocation id = parseId(stored.getString(ID_TAG));
int x = stored.getInt(X_TAG);
int y = stored.getInt(Y_TAG);
if (id != null && x >= MIN_ICON_COORDINATE && x <= MAX_ICON_COORDINATE && y >= MIN_ICON_COORDINATE && y <= MAX_ICON_COORDINATE) {
iconPositions.putIfAbsent(id, new IconPosition(x, y));
}
}
}
private static ListTag saveIds(Collection<ResourceLocation> ids) {
ListTag list = new ListTag();
for (ResourceLocation id : ids) {
list.add(StringTag.valueOf(id.toString()));
}
return list;
}
private static void loadIds(ListTag list, Set<ResourceLocation> destination, int limit) {
for (int index = 0; index < list.size() && destination.size() < limit; index++) {
ResourceLocation id = parseId(list.getString(index));
if (id != null) {
destination.add(id);
}
}
}
private static ResourceLocation parseId(String value) {
if (value == null || value.isBlank() || value.length() > MAX_ID_LENGTH) {
return null;
}
try {
return ResourceLocation.parse(value);
} catch (Exception ignored) {
return null;
}
}
private static boolean validId(ResourceLocation id) {
return id != null && id.toString().length() <= MAX_ID_LENGTH;
}
public record IconPosition(int x, int y) {
}
}
@@ -1,220 +0,0 @@
package com.craisinlord.antarchy.content.computer;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class ComputerFileSystem {
public static final int MAX_FILE_SIZE = 16 * 1024;
public static final int MAX_TOTAL_SIZE = 64 * 1024;
public static final int MAX_NODES = 128;
public static final int MAX_PATH_LENGTH = 64;
public static final String ROOT = "/";
public static final String DOCUMENTS = "/documents";
public static final String PICTURES = "/pictures";
public static final String SYSTEM = "/system";
private static final String FILES_TAG = "Files";
private static final String PATH_TAG = "Path";
private static final String TYPE_TAG = "Type";
private static final String CONTENTS_TAG = "Contents";
private final Map<String, ComputerFile> nodes = new LinkedHashMap<>();
private int totalBytes;
public ComputerFileSystem() {
reset();
}
public void reset() {
nodes.clear();
nodes.put(ROOT, ComputerFile.directory(ROOT));
nodes.put(DOCUMENTS, ComputerFile.directory(DOCUMENTS));
nodes.put(PICTURES, ComputerFile.directory(PICTURES));
nodes.put(SYSTEM, ComputerFile.directory(SYSTEM));
totalBytes = 0;
}
public ComputerFile get(String path) {
return nodes.get(normalize(path));
}
public List<ComputerFile> list() {
return List.copyOf(nodes.values());
}
public List<ComputerFile> listTextFiles() {
return nodes.values().stream().filter(file -> file.type() == ComputerFile.Type.TEXT).toList();
}
public Result createDirectory(String path) {
String normalized = normalize(path);
if (normalized.isEmpty() || ROOT.equals(normalized) || isProtected(normalized) || nodes.containsKey(normalized) || nodes.size() >= MAX_NODES) return Result.failure("invalid_path");
int slash = normalized.lastIndexOf('/');
if (slash > 0 && !nodesDirectory(normalized.substring(0, slash))) return Result.failure("missing_directory");
nodes.put(normalized, ComputerFile.directory(normalized));
return Result.ok();
}
public Result createTextFile(String path, String contents) {
String normalized = normalize(path);
Result validation = validateTextPath(normalized, contents);
if (!validation.successful()) return validation;
if (nodes.containsKey(normalized)) return Result.failure("already_exists");
if (nodes.size() >= MAX_NODES) return Result.failure("node_limit");
int size = contents.length();
if (totalBytes + size > MAX_TOTAL_SIZE) return Result.failure("storage_limit");
nodes.put(normalized, ComputerFile.text(normalized, contents));
totalBytes += size;
return Result.ok();
}
public Result writeTextFile(String path, String contents) {
String normalized = normalize(path);
Result validation = validateTextPath(normalized, contents);
if (!validation.successful()) return validation;
ComputerFile current = nodes.get(normalized);
if (current == null) return Result.failure("not_found");
if (current.type() != ComputerFile.Type.TEXT && current.type() != ComputerFile.Type.IMAGE) return Result.failure("not_file");
int replacement = totalBytes - current.contents().length() + contents.length();
if (replacement > MAX_TOTAL_SIZE) return Result.failure("storage_limit");
nodes.put(normalized, ComputerFile.text(normalized, contents));
totalBytes = replacement;
return Result.ok();
}
public Result createOrWriteTextFile(String path, String contents) {
return nodes.containsKey(normalize(path)) ? writeTextFile(path, contents) : createTextFile(path, contents);
}
public Result move(String source, String destination) {
String from = normalize(source);
String to = normalize(destination);
if (from.isEmpty() || to.isEmpty() || ROOT.equals(from) || ROOT.equals(to) || isProtected(from) || isProtected(to)) return Result.failure("invalid_path");
ComputerFile file = nodes.get(from);
if (file == null) return Result.failure("not_found");
if (nodes.containsKey(to)) return Result.failure("already_exists");
int slash = to.lastIndexOf('/');
if (slash > 0 && !nodesDirectory(to.substring(0, slash))) return Result.failure("missing_directory");
if (file.type() == ComputerFile.Type.DIRECTORY && to.startsWith(from + "/")) return Result.failure("invalid_path");
if (file.type() == ComputerFile.Type.DIRECTORY) {
List<String> descendants = nodes.keySet().stream()
.filter(path -> path.startsWith(from + "/"))
.toList();
for (String descendant : descendants) {
String movedPath = to + descendant.substring(from.length());
if (nodes.containsKey(movedPath)) return Result.failure("already_exists");
}
Map<String, ComputerFile> moved = new LinkedHashMap<>();
moved.put(to, ComputerFile.directory(to));
for (String descendant : descendants) {
ComputerFile child = nodes.get(descendant);
String movedPath = to + descendant.substring(from.length());
moved.put(movedPath, child.type() == ComputerFile.Type.DIRECTORY
? ComputerFile.directory(movedPath)
: ComputerFile.text(movedPath, child.contents()));
}
nodes.keySet().removeIf(path -> path.equals(from) || path.startsWith(from + "/"));
nodes.putAll(moved);
} else {
nodes.remove(from);
nodes.put(to, ComputerFile.text(to, file.contents()));
}
return Result.ok();
}
public Result delete(String path) {
String normalized = normalize(path);
if (isProtected(normalized)) return Result.failure("protected");
ComputerFile file = nodes.get(normalized);
if (file == null) return Result.failure("not_found");
if (file.type() == ComputerFile.Type.DIRECTORY && nodes.keySet().stream().anyMatch(key -> !key.equals(normalized) && key.startsWith(normalized + "/"))) {
return Result.failure("not_empty");
}
nodes.remove(normalized);
totalBytes -= file.contents().length();
return Result.ok();
}
public static String normalize(String path) {
if (path == null || path.isBlank()) return "";
String value = path.trim().replace('\\', '/');
if (!value.startsWith("/")) value = "/" + value;
while (value.contains("//")) value = value.replace("//", "/");
if (value.length() > 1 && value.endsWith("/")) value = value.substring(0, value.length() - 1);
return value;
}
public static boolean isProtected(String path) {
String normalized = normalize(path);
return normalized.equals(SYSTEM) || normalized.startsWith(SYSTEM + "/");
}
public void save(CompoundTag tag, HolderLookup.Provider registries) {
ListTag files = new ListTag();
for (ComputerFile file : nodes.values()) {
if (ROOT.equals(file.path())) continue;
CompoundTag stored = new CompoundTag();
stored.putString(PATH_TAG, file.path());
stored.putString(TYPE_TAG, file.type().name());
if (file.type() == ComputerFile.Type.TEXT || file.type() == ComputerFile.Type.IMAGE) stored.putString(CONTENTS_TAG, file.contents());
files.add(stored);
}
tag.put(FILES_TAG, files);
}
public void load(CompoundTag tag, HolderLookup.Provider registries) {
reset();
ListTag files = tag.getList(FILES_TAG, 10);
for (int index = 0; index < files.size(); index++) {
CompoundTag stored = files.getCompound(index);
String path = normalize(stored.getString(PATH_TAG));
if (path.isEmpty() || nodes.containsKey(path) || path.length() > MAX_PATH_LENGTH || isProtected(path)) continue;
String type = stored.getString(TYPE_TAG);
if ("DIRECTORY".equals(type)) {
if (nodes.size() < MAX_NODES) nodes.put(path, ComputerFile.directory(path));
} else if ("TEXT".equals(type) || "IMAGE".equals(type)) {
String contents = stored.getString(CONTENTS_TAG);
if (contents.length() <= MAX_FILE_SIZE && nodes.size() < MAX_NODES && totalBytes + contents.length() <= MAX_TOTAL_SIZE) {
nodes.put(path, "IMAGE".equals(type) ? ComputerFile.image(path, contents) : ComputerFile.text(path, contents));
totalBytes += contents.length();
}
}
}
}
private Result validateTextPath(String path, String contents) {
if (path.isEmpty() || path.length() > MAX_PATH_LENGTH || ROOT.equals(path) || isProtected(path)) return Result.failure("invalid_path");
if (contents == null || contents.length() > MAX_FILE_SIZE) return Result.failure("file_limit");
if (path.contains("..") || path.indexOf('\0') >= 0 || path.indexOf('\n') >= 0 || path.indexOf('\r') >= 0) return Result.failure("invalid_path");
int slash = path.lastIndexOf('/');
if (slash > 0 && !nodesDirectory(path.substring(0, slash))) return Result.failure("missing_directory");
return Result.ok();
}
private boolean nodesDirectory(String path) {
ComputerFile parent = nodes.get(path);
return parent != null && parent.type() == ComputerFile.Type.DIRECTORY;
}
public int totalBytes() {
return totalBytes;
}
public record Result(boolean successful, String error) {
public static Result ok() { return new Result(true, ""); }
public static Result failure(String error) { return new Result(false, error); }
}
public record ComputerFile(String path, Type type, String contents) {
public static ComputerFile directory(String path) { return new ComputerFile(path, Type.DIRECTORY, ""); }
public static ComputerFile text(String path, String contents) { return path.endsWith(".antpaint") ? image(path, contents) : new ComputerFile(path, Type.TEXT, contents); }
public static ComputerFile image(String path, String contents) { return new ComputerFile(path, Type.IMAGE, contents); }
public enum Type { DIRECTORY, TEXT, IMAGE }
}
}
@@ -1,114 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.util.ArrayDeque;
import java.util.Arrays;
public final class AntPaintCanvas {
public static final int WIDTH = 32;
public static final int HEIGHT = 24;
public static final int PIXEL_COUNT = WIDTH * HEIGHT;
public static final int PACKED_LENGTH = (PIXEL_COUNT + 7) / 8;
private final boolean[] pixels;
public AntPaintCanvas() {
this.pixels = new boolean[PIXEL_COUNT];
}
private AntPaintCanvas(boolean[] pixels) {
this.pixels = pixels;
}
public boolean get(int x, int y) {
return pixels[index(x, y)];
}
public void set(int x, int y, boolean value) {
pixels[index(x, y)] = value;
}
public boolean draw(int x, int y) {
return setIfChanged(x, y, true);
}
public boolean erase(int x, int y) {
return setIfChanged(x, y, false);
}
public void clear() {
Arrays.fill(pixels, false);
}
public int floodFill(int x, int y, boolean value) {
int start = index(x, y);
boolean original = pixels[start];
if (original == value) {
return 0;
}
ArrayDeque<Integer> queue = new ArrayDeque<>();
boolean[] visited = new boolean[PIXEL_COUNT];
queue.add(start);
int changed = 0;
while (!queue.isEmpty()) {
int current = queue.removeFirst();
if (visited[current] || pixels[current] != original) {
continue;
}
visited[current] = true;
pixels[current] = value;
changed++;
int px = current % WIDTH;
int py = current / WIDTH;
if (px > 0) queue.add(current - 1);
if (px + 1 < WIDTH) queue.add(current + 1);
if (py > 0) queue.add(current - WIDTH);
if (py + 1 < HEIGHT) queue.add(current + WIDTH);
}
return changed;
}
public byte[] pack() {
byte[] packed = new byte[PACKED_LENGTH];
for (int i = 0; i < PIXEL_COUNT; i++) {
if (pixels[i]) {
packed[i / 8] |= (byte) (1 << (i % 8));
}
}
return packed;
}
public static AntPaintCanvas unpack(byte[] packed) {
if (packed == null || packed.length != PACKED_LENGTH) {
throw new IllegalArgumentException("Invalid AntPaint pixel data length");
}
boolean[] pixels = new boolean[PIXEL_COUNT];
for (int i = 0; i < PIXEL_COUNT; i++) {
pixels[i] = (packed[i / 8] & (1 << (i % 8))) != 0;
}
return new AntPaintCanvas(pixels);
}
public AntPaintCanvas copy() {
return new AntPaintCanvas(Arrays.copyOf(pixels, pixels.length));
}
public boolean[] copyPixels() {
return Arrays.copyOf(pixels, pixels.length);
}
private boolean setIfChanged(int x, int y, boolean value) {
int index = index(x, y);
if (pixels[index] == value) {
return false;
}
pixels[index] = value;
return true;
}
private static int index(int x, int y) {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) {
throw new IndexOutOfBoundsException("AntPaint pixel outside canvas");
}
return y * WIDTH + x;
}
}
@@ -1,26 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.util.Objects;
public record AntPaintFile(String fileId, String filename, AntPaintCanvas canvas, long createdTime, long modifiedTime) {
public AntPaintFile {
Objects.requireNonNull(fileId, "fileId");
filename = AntPaintFilename.normalize(filename);
Objects.requireNonNull(canvas, "canvas");
if (fileId.isBlank()) {
throw new IllegalArgumentException("fileId cannot be blank");
}
}
public AntPaintFile copy() {
return new AntPaintFile(fileId, filename, canvas.copy(), createdTime, modifiedTime);
}
public AntPaintFile withCanvas(AntPaintCanvas value, long modified) {
return new AntPaintFile(fileId, filename, value, createdTime, modified);
}
public AntPaintFile withFilename(String value, long modified) {
return new AntPaintFile(fileId, value, canvas, createdTime, modified);
}
}
@@ -1,86 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public final class AntPaintFileCodec {
public static final int VERSION = 1;
public static final int MAX_ENCODED_BYTES = 1024;
private static final int MAGIC = 0x41505431;
private AntPaintFileCodec() {
}
public static byte[] encode(AntPaintFile file) {
try {
ByteArrayOutputStream output = new ByteArrayOutputStream(MAX_ENCODED_BYTES);
DataOutputStream data = new DataOutputStream(output);
data.writeInt(MAGIC);
data.writeByte(VERSION);
writeString(data, file.fileId());
writeString(data, file.filename());
data.writeLong(file.createdTime());
data.writeLong(file.modifiedTime());
data.writeByte(AntPaintCanvas.WIDTH);
data.writeByte(AntPaintCanvas.HEIGHT);
data.write(file.canvas().pack());
data.flush();
byte[] encoded = output.toByteArray();
if (encoded.length > MAX_ENCODED_BYTES) {
throw new IllegalArgumentException("AntPaint file exceeds encoded size limit");
}
return encoded;
} catch (IOException exception) {
throw new IllegalStateException("Unable to encode AntPaint file", exception);
}
}
public static AntPaintFile decode(byte[] encoded) {
if (encoded == null || encoded.length > MAX_ENCODED_BYTES) {
throw new IllegalArgumentException("Invalid AntPaint file size");
}
try {
DataInputStream data = new DataInputStream(new ByteArrayInputStream(encoded));
if (data.readInt() != MAGIC || data.readUnsignedByte() != VERSION) {
throw new IllegalArgumentException("Unsupported AntPaint file format");
}
String fileId = readString(data, 128);
String filename = readString(data, AntPaintFilename.MAX_LENGTH + AntPaintFilename.EXTENSION.length());
long created = data.readLong();
long modified = data.readLong();
if (data.readUnsignedByte() != AntPaintCanvas.WIDTH || data.readUnsignedByte() != AntPaintCanvas.HEIGHT) {
throw new IllegalArgumentException("Unsupported AntPaint canvas size");
}
byte[] pixels = data.readNBytes(AntPaintCanvas.PACKED_LENGTH);
if (pixels.length != AntPaintCanvas.PACKED_LENGTH || data.available() != 0) {
throw new IllegalArgumentException("Invalid AntPaint pixel payload");
}
return new AntPaintFile(fileId, filename, AntPaintCanvas.unpack(pixels), created, modified);
} catch (IOException exception) {
throw new IllegalArgumentException("Invalid AntPaint file data", exception);
}
}
private static void writeString(DataOutputStream data, String value) throws IOException {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
if (bytes.length > 128) {
throw new IllegalArgumentException("AntPaint string exceeds size limit");
}
data.writeShort(bytes.length);
data.write(bytes);
}
private static String readString(DataInputStream data, int maxBytes) throws IOException {
int length = data.readUnsignedShort();
if (length > maxBytes) {
throw new IllegalArgumentException("AntPaint string exceeds size limit");
}
byte[] bytes = new byte[length];
data.readFully(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
}
@@ -1,38 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.util.Locale;
import java.util.regex.Pattern;
public final class AntPaintFilename {
public static final int MAX_LENGTH = 32;
public static final String EXTENSION = ".antpaint";
private static final Pattern SAFE_NAME = Pattern.compile("[A-Za-z0-9 _.-]+");
private AntPaintFilename() {
}
public static boolean isValid(String filename) {
if (filename == null || filename.isBlank() || filename.length() > MAX_LENGTH) {
return false;
}
if (filename.equals(".") || filename.equals("..") || filename.contains("..")) {
return false;
}
return SAFE_NAME.matcher(filename).matches();
}
public static String normalize(String filename) {
if (filename == null) {
throw new IllegalArgumentException("Filename cannot be null");
}
String value = filename.trim();
if (value.toLowerCase(Locale.ROOT).endsWith(EXTENSION)) {
value = value.substring(0, value.length() - EXTENSION.length());
}
value = value.trim();
if (!isValid(value)) {
throw new IllegalArgumentException("Invalid AntPaint filename");
}
return value + EXTENSION;
}
}
@@ -1,47 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.util.ArrayDeque;
public final class AntPaintHistory {
public static final int MAX_STATES = 32;
private final ArrayDeque<AntPaintCanvas> undo = new ArrayDeque<>();
private final ArrayDeque<AntPaintCanvas> redo = new ArrayDeque<>();
public void record(AntPaintCanvas canvas) {
undo.addLast(canvas.copy());
while (undo.size() > MAX_STATES) {
undo.removeFirst();
}
redo.clear();
}
public boolean canUndo() {
return !undo.isEmpty();
}
public boolean canRedo() {
return !redo.isEmpty();
}
public AntPaintCanvas undo(AntPaintCanvas current) {
if (undo.isEmpty()) {
return current;
}
redo.addLast(current.copy());
return undo.removeLast().copy();
}
public AntPaintCanvas redo(AntPaintCanvas current) {
if (redo.isEmpty()) {
return current;
}
undo.addLast(current.copy());
return redo.removeLast().copy();
}
public void clear() {
undo.clear();
redo.clear();
}
}
@@ -1,35 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.util.Objects;
public final class AntPaintRequests {
private AntPaintRequests() {
}
public record Save(String computerId, AntPaintFile file) {
public Save {
Objects.requireNonNull(computerId, "computerId");
Objects.requireNonNull(file, "file");
}
}
public record Load(String computerId, String fileId) {
public Load {
Objects.requireNonNull(computerId, "computerId");
Objects.requireNonNull(fileId, "fileId");
}
}
public record List(String computerId) {
public List {
Objects.requireNonNull(computerId, "computerId");
}
}
public record Delete(String computerId, String fileId) {
public Delete {
Objects.requireNonNull(computerId, "computerId");
Objects.requireNonNull(fileId, "fileId");
}
}
}
@@ -1,40 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
import java.util.List;
public final class AntPaintResults {
private AntPaintResults() {
}
public enum Code {
SUCCESS,
UNAUTHENTICATED,
OUT_OF_RANGE,
INVALID_FILENAME,
INVALID_FILE,
FILE_NOT_FOUND,
DUPLICATE_FILE,
STORAGE_FULL,
PAYLOAD_TOO_LARGE,
BUSY,
UNKNOWN_ERROR
}
public record Save(Code code, String fileId) {
}
public record Load(Code code, AntPaintFile file) {
}
public record List(Code code, java.util.List<AntPaintFileMetadata> files) {
public List {
files = files == null ? java.util.List.of() : java.util.List.copyOf(files);
}
}
public record Delete(Code code, String fileId) {
}
public record AntPaintFileMetadata(String fileId, String filename, long modifiedTime) {
}
}
@@ -1,7 +0,0 @@
package com.craisinlord.antarchy.content.computer.paint;
public enum AntPaintTool {
PENCIL,
ERASER,
FILL
}
@@ -1,11 +0,0 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.List;
public record TerminalCommand(String name, List<String> arguments, String rawInput) {
public TerminalCommand {
name = name.toLowerCase(java.util.Locale.ROOT);
arguments = List.copyOf(arguments);
rawInput = rawInput == null ? "" : rawInput;
}
}
@@ -1,79 +0,0 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.ArrayList;
import java.util.List;
public final class TerminalCommandParser {
private static final int MAX_INPUT_LENGTH = 256;
private TerminalCommandParser() {
}
public static ParseResult parse(String input) {
if (input == null) {
return ParseResult.error("ERROR: EMPTY INPUT");
}
if (input.length() > MAX_INPUT_LENGTH) {
return ParseResult.error("ERROR: COMMAND TOO LONG");
}
String trimmed = input.trim();
if (trimmed.isEmpty()) {
return ParseResult.error("ERROR: EMPTY INPUT");
}
List<String> tokens = new ArrayList<>();
StringBuilder token = new StringBuilder();
Character quote = null;
boolean escaped = false;
for (int index = 0; index < trimmed.length(); index++) {
char character = trimmed.charAt(index);
if (escaped) {
token.append(character);
escaped = false;
} else if (character == '\\') {
escaped = true;
} else if (quote != null) {
if (character == quote) {
quote = null;
} else {
token.append(character);
}
} else if (character == '\'' || character == '"') {
quote = character;
} else if (Character.isWhitespace(character)) {
if (!token.isEmpty()) {
tokens.add(token.toString());
token.setLength(0);
}
} else {
token.append(character);
}
}
if (escaped) {
token.append('\\');
}
if (quote != null) {
return ParseResult.error("ERROR: UNTERMINATED QUOTE");
}
if (!token.isEmpty()) {
tokens.add(token.toString());
}
if (tokens.isEmpty()) {
return ParseResult.error("ERROR: EMPTY INPUT");
}
return ParseResult.success(new TerminalCommand(tokens.get(0), tokens.subList(1, tokens.size()), input));
}
public record ParseResult(TerminalCommand command, String error) {
public boolean isSuccess() {
return command != null;
}
public static ParseResult success(TerminalCommand command) {
return new ParseResult(command, null);
}
public static ParseResult error(String message) {
return new ParseResult(null, message);
}
}
}
@@ -1,207 +0,0 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
public final class TerminalCommandService {
private static final int MAX_ARGUMENT_LENGTH = 128;
private static final List<String> COMMANDS = List.of(
"help", "clear", "pwd", "ls", "cd", "mkdir", "touch", "cat", "write", "rm", "rmdir", "mv", "open"
);
public TerminalResult execute(TerminalFileSystem fileSystem, String workingDirectory, String input) {
String currentDirectory = normalizeWorkingDirectory(fileSystem, workingDirectory);
TerminalCommandParser.ParseResult parsed = TerminalCommandParser.parse(input);
if (!parsed.isSuccess()) {
return TerminalResult.error(currentDirectory, parsed.error());
}
TerminalCommand command = parsed.command();
if (command.arguments().stream().anyMatch(argument -> argument.length() > MAX_ARGUMENT_LENGTH)) {
return TerminalResult.error(currentDirectory, "ERROR: ARGUMENT TOO LONG");
}
return switch (command.name()) {
case "help" -> help(currentDirectory, command);
case "clear" -> requireArguments(currentDirectory, command, 0, TerminalResult.clear(currentDirectory));
case "pwd" -> requireArguments(currentDirectory, command, 0, TerminalResult.success(currentDirectory, currentDirectory));
case "ls" -> list(fileSystem, currentDirectory, command);
case "cd" -> changeDirectory(fileSystem, currentDirectory, command);
case "mkdir" -> createDirectory(fileSystem, currentDirectory, command);
case "touch" -> createFile(fileSystem, currentDirectory, command);
case "cat" -> readFile(fileSystem, currentDirectory, command);
case "write" -> writeFile(fileSystem, currentDirectory, command);
case "rm" -> removeFile(fileSystem, currentDirectory, command);
case "rmdir" -> removeDirectory(fileSystem, currentDirectory, command);
case "mv" -> move(fileSystem, currentDirectory, command);
case "open" -> open(fileSystem, currentDirectory, command);
default -> TerminalResult.error(currentDirectory, "ERROR: INVALID COMMAND: " + command.name());
};
}
private TerminalResult help(String workingDirectory, TerminalCommand command) {
if (command.arguments().isEmpty()) {
return TerminalResult.success(workingDirectory,
"AVAILABLE COMMANDS",
"HELP CLEAR PWD LS CD MKDIR TOUCH CAT WRITE RM RMDIR MV OPEN");
}
if (command.arguments().size() != 1) {
return usage(workingDirectory, "help [command]");
}
return switch (command.arguments().get(0).toLowerCase(Locale.ROOT)) {
case "help" -> TerminalResult.success(workingDirectory, "HELP [COMMAND] // SHOW COMMAND HELP");
case "clear" -> TerminalResult.success(workingDirectory, "CLEAR // CLEAR TERMINAL OUTPUT");
case "pwd" -> TerminalResult.success(workingDirectory, "PWD // PRINT WORKING DIRECTORY");
case "ls" -> TerminalResult.success(workingDirectory, "LS [PATH] // LIST DIRECTORY CONTENTS");
case "cd" -> TerminalResult.success(workingDirectory, "CD [PATH] // CHANGE WORKING DIRECTORY");
case "mkdir" -> TerminalResult.success(workingDirectory, "MKDIR <PATH> // CREATE DIRECTORY");
case "touch" -> TerminalResult.success(workingDirectory, "TOUCH <PATH> // CREATE EMPTY TEXT FILE");
case "cat" -> TerminalResult.success(workingDirectory, "CAT <PATH> // PRINT TEXT FILE");
case "write" -> TerminalResult.success(workingDirectory, "WRITE <PATH> [TEXT] // REPLACE TEXT FILE CONTENTS");
case "rm" -> TerminalResult.success(workingDirectory, "RM <PATH> // DELETE TEXT FILE");
case "rmdir" -> TerminalResult.success(workingDirectory, "RMDIR <PATH> // DELETE EMPTY DIRECTORY");
case "mv" -> TerminalResult.success(workingDirectory, "MV <SOURCE> <DESTINATION> // MOVE OR RENAME");
case "open" -> TerminalResult.success(workingDirectory, "OPEN <PATH> // OPEN TEXT FILE IN ANTTEXT");
default -> TerminalResult.error(workingDirectory, "ERROR: NO HELP FOR " + command.arguments().get(0));
};
}
private TerminalResult list(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() > 1) {
return usage(workingDirectory, "ls [path]");
}
String path = command.arguments().isEmpty() ? workingDirectory : command.arguments().get(0);
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, path, workingDirectory);
if (entry.isEmpty()) {
return missing(workingDirectory, path);
}
if (!entry.get().isDirectory()) {
return TerminalResult.success(workingDirectory, entry.get().name());
}
List<TerminalFileSystem.Entry> entries = new ArrayList<>(fileSystem.list(path, workingDirectory));
entries.sort(Comparator.comparing(TerminalFileSystem.Entry::isDirectory).reversed().thenComparing(TerminalFileSystem.Entry::name));
if (entries.isEmpty()) {
return TerminalResult.success(workingDirectory, "DIRECTORY EMPTY");
}
return new TerminalResult(TerminalResult.Status.SUCCESS, entries.stream()
.map(value -> (value.isDirectory() ? "[DIR] " : " ") + value.name()).toList(), workingDirectory, false, null);
}
private TerminalResult changeDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() > 1) {
return usage(workingDirectory, "cd [path]");
}
String path = command.arguments().isEmpty() ? TerminalFileSystem.ROOT_PATH : command.arguments().get(0);
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, path, workingDirectory);
if (entry.isEmpty() || !entry.get().isDirectory()) {
return missing(workingDirectory, path);
}
return TerminalResult.success(entry.get().path(), entry.get().path());
}
private TerminalResult createDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "mkdir <path>");
}
return fileSystem.createDirectory(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT CREATE DIRECTORY");
}
private TerminalResult createFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "touch <path>");
}
return fileSystem.createFile(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT CREATE FILE");
}
private TerminalResult readFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "cat <path>");
}
Optional<String> contents = fileSystem.readText(command.arguments().get(0), workingDirectory);
return contents.map(value -> TerminalResult.success(workingDirectory, value.split("\\R", -1)))
.orElseGet(() -> missing(workingDirectory, command.arguments().get(0)));
}
private TerminalResult writeFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() < 1 || command.arguments().size() > 2) {
return usage(workingDirectory, "write <path> [text]");
}
String contents = command.arguments().size() == 2 ? command.arguments().get(1) : "";
return fileSystem.writeText(command.arguments().get(0), workingDirectory, contents)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT WRITE FILE");
}
private TerminalResult removeFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "rm <path>");
}
return fileSystem.deleteFile(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT DELETE FILE");
}
private TerminalResult removeDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "rmdir <path>");
}
return fileSystem.deleteEmptyDirectory(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: DIRECTORY NOT EMPTY OR NOT FOUND");
}
private TerminalResult move(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 2) {
return usage(workingDirectory, "mv <source> <destination>");
}
return fileSystem.move(command.arguments().get(0), command.arguments().get(1), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT MOVE PATH");
}
private TerminalResult open(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "open <path>");
}
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, command.arguments().get(0), workingDirectory);
if (entry.isEmpty() || entry.get().isDirectory()) {
return missing(workingDirectory, command.arguments().get(0));
}
return TerminalResult.open(workingDirectory, entry.get().path());
}
private Optional<TerminalFileSystem.Entry> find(TerminalFileSystem fileSystem, String path, String workingDirectory) {
try {
return fileSystem.find(path, workingDirectory);
} catch (RuntimeException ignored) {
return Optional.empty();
}
}
private String normalizeWorkingDirectory(TerminalFileSystem fileSystem, String workingDirectory) {
try {
String normalized = fileSystem.normalize(workingDirectory, TerminalFileSystem.ROOT_PATH);
Optional<TerminalFileSystem.Entry> entry = fileSystem.find(normalized, TerminalFileSystem.ROOT_PATH);
return entry.filter(TerminalFileSystem.Entry::isDirectory).map(TerminalFileSystem.Entry::path).orElse(TerminalFileSystem.ROOT_PATH);
} catch (RuntimeException ignored) {
return TerminalFileSystem.ROOT_PATH;
}
}
private TerminalResult missing(String workingDirectory, String path) {
return TerminalResult.error(workingDirectory, "ERROR: PATH NOT FOUND: " + path);
}
private TerminalResult usage(String workingDirectory, String usage) {
return TerminalResult.error(workingDirectory, "USAGE: " + usage);
}
private TerminalResult requireArguments(String workingDirectory, TerminalCommand command, int count, TerminalResult result) {
return command.arguments().size() == count ? result : usage(workingDirectory, command.name() + " requires " + count + " arguments");
}
}
@@ -1,39 +0,0 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.List;
import java.util.Optional;
public interface TerminalFileSystem {
String ROOT_PATH = "/";
Optional<Entry> find(String path, String workingDirectory);
List<Entry> list(String path, String workingDirectory);
boolean createDirectory(String path, String workingDirectory);
boolean createFile(String path, String workingDirectory);
Optional<String> readText(String path, String workingDirectory);
boolean writeText(String path, String workingDirectory, String contents);
boolean deleteFile(String path, String workingDirectory);
boolean deleteEmptyDirectory(String path, String workingDirectory);
boolean move(String source, String destination, String workingDirectory);
String normalize(String path, String workingDirectory);
record Entry(String name, String path, EntryType type) {
public boolean isDirectory() {
return type == EntryType.DIRECTORY;
}
}
enum EntryType {
FILE,
DIRECTORY
}
}
@@ -1,33 +0,0 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.List;
public record TerminalResult(Status status, List<String> lines, String workingDirectory, boolean clearOutput, String openPath) {
public TerminalResult {
status = status == null ? Status.ERROR : status;
lines = List.copyOf(lines);
workingDirectory = workingDirectory == null || workingDirectory.isBlank() ? TerminalFileSystem.ROOT_PATH : workingDirectory;
}
public static TerminalResult success(String workingDirectory, String... lines) {
return new TerminalResult(Status.SUCCESS, List.of(lines), workingDirectory, false, null);
}
public static TerminalResult error(String workingDirectory, String... lines) {
return new TerminalResult(Status.ERROR, List.of(lines), workingDirectory, false, null);
}
public static TerminalResult clear(String workingDirectory) {
return new TerminalResult(Status.SUCCESS, List.of(), workingDirectory, true, null);
}
public static TerminalResult open(String workingDirectory, String path) {
return new TerminalResult(Status.OPEN, List.of("OPENING " + path), workingDirectory, false, path);
}
public enum Status {
SUCCESS,
ERROR,
OPEN
}
}
@@ -38,6 +38,8 @@ public final class CreativeTabContents {
}
private static final List<CatalogEntry> CATALOG = List.of(
entry("bug_spray", "tools"),
entry("ant_trap", "building_blocks"),
entry("ouranwood_log", "building_blocks"),
entry("ouranwood_wood", "building_blocks"),
entry("mossy_ouranwood_log", "building_blocks"),
@@ -395,8 +397,6 @@ public final class CreativeTabContents {
entry("lucid_anchor", "tools_and_utilities"),
entry("undertrial_spawner", "tools_and_utilities"),
entry("undervault", "tools_and_utilities"),
entry("computer", "functional_blocks"),
entry("floppy_disk", "tools_and_utilities"),
entry("undertrial_key", "tools_and_utilities"),
entry("reverie_bottle", "tools_and_utilities"),
entry("glimmer_bottle", "tools_and_utilities"),
@@ -641,8 +641,8 @@ public final class CreativeTabContents {
}
for (String diskId : List.of("antarchy:introduction", "antarchy:chronosphere", "antarchy:toreterror_arena", "antarchy:high_fructose_corn_syrup",
"antarchy:elythia", "antarchy:flying_squirrel", "antarchy:cavaryn", "antarchy:thoraxis", "antarchy:temporal_fields", "antarchy:bloodglass", "antarchy:vortex")) {
ItemStack disk = new ItemStack(AntarchyObjects.FLOPPY_DISK.get());
disk.set(AntarchyObjects.FLOPPY_DISK_COMPONENT.get(), ResourceLocation.parse(diskId));
ItemStack disk = new ItemStack(com.craisinlord.antos.content.AntOSObjects.FLOPPY_DISK.get());
disk.set(com.craisinlord.antos.content.AntOSObjects.FLOPPY_DISK_COMPONENT.get(), ResourceLocation.parse(diskId));
output.accept(disk);
}
}
@@ -6,6 +6,7 @@ import com.craisinlord.antarchy.content.gravity.AntarchyGravityTransition;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.EntityType;
public final class InvertedMobEffect extends MobEffect {
private static final AntarchyGravityTransition EFFECT_TRANSITION = new AntarchyGravityTransition(12);
@@ -17,6 +18,14 @@ public final class InvertedMobEffect extends MobEffect {
@Override
public boolean applyEffectTick(LivingEntity entity, int amplifier) {
// Vex flight steering is authored around vanilla gravity and ground contact. Rotating its
// gravity frame makes the native flight controller drive it into terrain.
if (entity.getType() == EntityType.VEX) {
if (!entity.level().isClientSide()) {
AntarchyGravityApi.clearForcedGravity(entity);
}
return true;
}
if (!entity.level().isClientSide()) {
if (AntarchyGravityApi.getGravityDirection(entity) != AntarchyGravityDirection.UP || !AntarchyGravityApi.isGravityForced(entity)) {
AntarchyGravityApi.setForcedGravityDirection(entity, AntarchyGravityDirection.UP, EFFECT_TRANSITION);
@@ -2,6 +2,7 @@ package com.craisinlord.antarchy.content.entity;
import com.craisinlord.antarchy.Antarchy;
import com.craisinlord.antarchy.config.AntarchySettings;
import com.craisinlord.antarchy.content.AntarchySoundEvents;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityApi;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityDirection;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityRotationUtil;
@@ -67,6 +68,14 @@ public class HoverboardEntity extends PathfinderMob implements GeoEntity {
this.setPersistenceRequired();
}
@Override
public void tick() {
super.tick();
if (!this.level().isClientSide && this.isVehicle() && this.tickCount % 100 == 0) {
this.playSound(AntarchySoundEvents.HOVERBOARD_IDLE.get(), 0.55F, 1.0F);
}
}
@Override
public boolean canBeAffected(MobEffectInstance effectInstance) {
return false;
@@ -139,12 +148,21 @@ public class HoverboardEntity extends PathfinderMob implements GeoEntity {
if (!this.isVehicle() && !player.isSecondaryUseActive()) {
if (!this.level().isClientSide) {
player.startRiding(this);
this.playSound(AntarchySoundEvents.HOVERBOARD_MOUNT.get(), 1.0F, 1.0F);
}
return InteractionResult.sidedSuccess(this.level().isClientSide);
}
return super.mobInteract(player, hand);
}
@Override
protected void removePassenger(Entity passenger) {
super.removePassenger(passenger);
if (!this.level().isClientSide) {
this.playSound(AntarchySoundEvents.HOVERBOARD_DISMOUNT.get(), 1.0F, 1.0F);
}
}
@Override
@Nullable
public LivingEntity getControllingPassenger() {
@@ -16,6 +16,7 @@ import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.RotatedPillarBlock;
import net.minecraft.world.level.block.SoundType;
@@ -32,6 +33,8 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
private static final int WOOD_REPATH_INTERVAL_TICKS = 6;
private static final int WOOD_SEARCH_RADIUS_HORIZONTAL = 12;
private static final int WOOD_SEARCH_RADIUS_VERTICAL = 4;
private static final int LIGHT_FLEE_SEARCH_RADIUS = 6;
private static final int LIGHT_FLEE_SEARCH_INTERVAL_TICKS = 10;
private static final Vec3i[] IMMEDIATE_WOOD_OFFSETS = {
new Vec3i(0, 1, 0),
new Vec3i(0, 0, -1),
@@ -48,6 +51,7 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
private int nextWoodBiteTick;
private int nextWoodSearchTick;
private int nextWoodRepathTick;
private int nextLightFleeSearchTick;
public TermiteEntity(EntityType<? extends BaseAntEntity> entityType, Level level) {
super(entityType, level);
@@ -113,6 +117,12 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
@Override
protected boolean handlePriorityForaging() {
if (this.isBlockLit(this.blockPosition())) {
this.targetWoodPos = null;
this.fleeBlockLight();
return true;
}
if (!this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)
|| !this.level().getGameRules().getBoolean(AntarchyGameRules.RULE_DO_TERMITE_GREIFING)) {
this.targetWoodPos = null;
@@ -142,6 +152,45 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
return true;
}
private void fleeBlockLight() {
if (this.tickCount < this.nextLightFleeSearchTick && !this.getNavigation().isDone()) {
return;
}
this.nextLightFleeSearchTick = this.tickCount + LIGHT_FLEE_SEARCH_INTERVAL_TICKS;
BlockPos origin = this.blockPosition();
BlockPos bestPos = null;
int bestLight = this.level().getBrightness(LightLayer.BLOCK, origin);
double bestDistance = -1.0D;
BlockPos.MutableBlockPos candidate = new BlockPos.MutableBlockPos();
for (int dx = -LIGHT_FLEE_SEARCH_RADIUS; dx <= LIGHT_FLEE_SEARCH_RADIUS; dx++) {
for (int dz = -LIGHT_FLEE_SEARCH_RADIUS; dz <= LIGHT_FLEE_SEARCH_RADIUS; dz++) {
if (dx * dx + dz * dz > LIGHT_FLEE_SEARCH_RADIUS * LIGHT_FLEE_SEARCH_RADIUS) continue;
for (int dy = -2; dy <= 2; dy++) {
candidate.set(origin.getX() + dx, origin.getY() + dy, origin.getZ() + dz);
if (!this.isWalkableStandPos(candidate)) continue;
int light = this.level().getBrightness(LightLayer.BLOCK, candidate);
double distance = candidate.distSqr(origin);
if (light > bestLight || (light == bestLight && distance <= bestDistance)) continue;
bestPos = candidate.immutable();
bestLight = light;
bestDistance = distance;
}
}
}
if (bestPos == null) {
this.getNavigation().stop();
return;
}
Path path = this.getNavigation().createPath(bestPos, 0);
if (path == null || !path.canReach()) {
this.getNavigation().stop();
return;
}
this.getNavigation().moveTo(bestPos.getX() + 0.5D, bestPos.getY(), bestPos.getZ() + 0.5D, 1.2D);
}
private boolean tryEatNearbyWood() {
if (this.tickCount < this.nextWoodBiteTick) {
return false;
@@ -151,7 +200,7 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
for (Vec3i offset : IMMEDIATE_WOOD_OFFSETS) {
BlockPos targetPos = basePos.offset(offset);
BlockState targetState = this.level().getBlockState(targetPos);
if (!this.isTermiteEdible(targetState)) {
if (this.isBlockLit(targetPos) || !this.isTermiteEdible(targetState)) {
continue;
}
@@ -244,7 +293,12 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
}
private boolean isValidWoodTarget(BlockPos targetPos) {
return this.isTermiteEdible(this.level().getBlockState(targetPos));
return !this.isBlockLit(targetPos) && this.isTermiteEdible(this.level().getBlockState(targetPos));
}
private boolean isBlockLit(BlockPos pos) {
return AntarchySettings.termitesFearBlockLight()
&& this.level().getBrightness(LightLayer.BLOCK, pos) > 0;
}
private boolean isTermiteEdible(BlockState state) {
@@ -14,6 +14,8 @@ import net.minecraft.sounds.SoundSource;
import net.minecraft.network.chat.Component;
import net.minecraft.ChatFormatting;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.BlockTags;
import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.network.protocol.game.ClientboundClearTitlesPacket;
import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket;
@@ -252,7 +254,7 @@ public class KingEntity extends RoyalBossEntity {
return true;
}
LivingEntity target = this.getTarget();
if (target == null || this.activeDecree != null || this.royalChargeOpeningDelayTicks > 0
if (target == null || this.royalChargeOpeningDelayTicks > 0
|| this.kingLandingStage != KingLandingStage.AERIAL
|| !this.attackScheduler.ready("royal_charge", RoyalAttackLane.MOVEMENT)) {
return false;
@@ -543,6 +545,9 @@ public class KingEntity extends RoyalBossEntity {
return;
}
this.tickRoyalBoundary((ServerLevel) this.level());
if (this.tickCount % 10 == 0 && this.isRoyalFlying() && this.getDeltaMovement().horizontalDistanceSqr() > 0.01D) {
this.clearLeavesAhead((ServerLevel) this.level());
}
this.tickComeNoCloserIndicator();
this.tickRoyalPunishment();
this.tickPendingExileAnimation();
@@ -762,6 +767,28 @@ public class KingEntity extends RoyalBossEntity {
return true;
}
private void clearLeavesAhead(ServerLevel level) {
if (!level.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING)) {
return;
}
Vec3 movement = this.getDeltaMovement().normalize().scale(1.5D);
AABB forward = this.getBoundingBox().move(movement).inflate(0.2D, 0.0D, 0.2D);
BlockPos min = BlockPos.containing(forward.minX, forward.minY, forward.minZ);
BlockPos max = BlockPos.containing(forward.maxX, forward.maxY, forward.maxZ);
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
for (int y = min.getY(); y <= max.getY(); y++) {
for (int x = min.getX(); x <= max.getX(); x++) {
for (int z = min.getZ(); z <= max.getZ(); z++) {
cursor.set(x, y, z);
if (level.getBlockState(cursor).is(BlockTags.LEAVES)) {
level.destroyBlock(cursor, true, this);
return;
}
}
}
}
}
private void tickKingCombatMovement(LivingEntity target) {
if (this.royalChargeActive) {
this.getNavigation().stop();
@@ -1102,7 +1129,9 @@ public class KingEntity extends RoyalBossEntity {
public RoyalDecree.Evaluation evaluateCloseQuarters(LivingEntity target) {
UUID targetId = target.getUUID();
double distanceSquared = target.distanceToSqr(this);
double dx = target.getX() - this.getX();
double dz = target.getZ() - this.getZ();
double distanceSquared = dx * dx + dz * dz;
if (distanceSquared <= CLOSE_QUARTERS_RADIUS * CLOSE_QUARTERS_RADIUS) {
this.closeQuartersEntered.add(targetId);
this.closeQuartersOutsideTicks.remove(targetId);
@@ -1719,8 +1748,8 @@ public class KingEntity extends RoyalBossEntity {
}
@Override public void onActive(int elapsedTicks) {
Vec3 origin = KingEntity.this.headAnchor(KingEntity.this.royalHead(RoyalHead.Slot.LEFT));
Vec3 predicted = target.position().add(target.getDeltaMovement().scale(10.0D));
Vec3 direction = predicted.add(0.0D, target.getBbHeight() * 0.5D, 0.0D).subtract(origin).normalize();
Vec3 direction = RoyalElementalProjectileEntity.aimDirection(
origin, target, RoyalBeamElement.FIRE);
RoyalElementalProjectileEntity fireball = RoyalElementalProjectileEntity.create(level, KingEntity.this,
RoyalBeamElement.FIRE, origin, direction);
level.addFreshEntity(fireball);
@@ -1739,7 +1768,8 @@ public class KingEntity extends RoyalBossEntity {
}
@Override public void onActive(int elapsedTicks) {
Vec3 origin = KingEntity.this.headAnchor(KingEntity.this.royalHead(RoyalHead.Slot.RIGHT));
Vec3 direction = target.getEyePosition().subtract(origin).normalize();
Vec3 direction = RoyalElementalProjectileEntity.aimDirection(
origin, target, RoyalBeamElement.ICE);
RoyalElementalProjectileEntity iceball = RoyalElementalProjectileEntity.create(level, KingEntity.this,
RoyalBeamElement.ICE, origin, direction);
level.addFreshEntity(iceball);
@@ -10,6 +10,8 @@ import com.craisinlord.antarchy.content.gravity.AntarchyGravityApi;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityDirection;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityRotationUtil;
import com.craisinlord.antarchy.content.gravity.AntarchyGravityTransition;
import com.craisinlord.antarchy.content.time.TimeDilationApi;
import com.craisinlord.antarchy.content.time.TimeDilationFieldEntity;
import com.craisinlord.antarchy.content.worldgen.thoraxis.ThoraxisUndersideManager;
import java.util.HashMap;
import java.util.Iterator;
@@ -73,6 +75,9 @@ public class QueenEntity extends RoyalBossEntity {
private static final double CRUSHING_GRAVITY_RADIUS = 18.0D;
private static final double CRUSHING_GRAVITY_STRENGTH = 0.12D;
private static final int ACCELERATION_COOLDOWN = 500;
private static final int QUEEN_CHRONOSPHERE_COOLDOWN = 700;
private static final int QUEEN_CHRONOSPHERE_DURATION = 200;
private static final double QUEEN_CHRONOSPHERE_RADIUS = 20.0D;
private static final int ACCELERATION_DURATION = 140;
private static final int BLACK_HOLE_COOLDOWN = 340;
private static final double QUEEN_BITE_COOLDOWN_MULTIPLIER = 1.5D;
@@ -83,10 +88,10 @@ public class QueenEntity extends RoyalBossEntity {
private static final int ACCELERATION_TELEGRAPH_TICKS = 32;
private static final int LANDING_TELEGRAPH_TICKS = 30;
private static final int LANDING_IMPACT_TICKS = 18;
private static final int GROUND_COMBAT_TICKS = 140;
private static final int GROUND_COMBAT_TICKS = 160;
private static final int TAKEOFF_TICKS = 30;
private static final int LANDING_COOLDOWN_MIN = 360;
private static final int LANDING_COOLDOWN_VARIANCE = 240;
private static final int LANDING_COOLDOWN_MIN = 300;
private static final int LANDING_COOLDOWN_VARIANCE = 120;
private static final int BLACK_HOLE_FOLLOW_UP_DELAY = 22;
private static final int CRUSHING_LANDING_DELAY = 40;
private static final int MOMENTUM_VOLLEY_DELAY = MOMENTUM_LOCK_DURATION + 5;
@@ -99,12 +104,14 @@ public class QueenEntity extends RoyalBossEntity {
private int momentumLockCooldownTicks;
private int crushingGravityCooldownTicks;
private int accelerationCooldownTicks;
private int queenChronosphereCooldownTicks;
private int blackHoleCooldownTicks;
private int gravityStompTelegraphTicks;
private int blackHoleTelegraphTicks;
private int momentumLockTelegraphTicks;
private int crushingGravityTelegraphTicks;
private int accelerationTelegraphTicks;
private int queenChronosphereTelegraphTicks;
@Nullable
private Vec3 pendingBlackHoleAnchor;
private int queenLandingCooldownTicks = 300;
@@ -238,6 +245,11 @@ public class QueenEntity extends RoyalBossEntity {
return 1.55D;
}
@Override
protected double combatStandoffDistance() {
return Math.min(super.combatStandoffDistance(), 14.0D);
}
@Override
protected int maxConcurrentHeadAttacks(Phase phase) {
return switch (phase) {
@@ -851,6 +863,7 @@ public class QueenEntity extends RoyalBossEntity {
if (this.momentumLockCooldownTicks > 0) this.momentumLockCooldownTicks--;
if (this.crushingGravityCooldownTicks > 0) this.crushingGravityCooldownTicks--;
if (this.accelerationCooldownTicks > 0) this.accelerationCooldownTicks--;
if (this.queenChronosphereCooldownTicks > 0) this.queenChronosphereCooldownTicks--;
if (this.blackHoleCooldownTicks > 0) this.blackHoleCooldownTicks--;
if (!(this.level() instanceof ServerLevel level)) {
@@ -871,7 +884,7 @@ public class QueenEntity extends RoyalBossEntity {
String[] candidates = switch (phase) {
case ONE -> new String[] {"gravity_stomp", "black_hole"};
case TWO -> new String[] {"gravity_stomp", "black_hole", "momentum_lock", "crushing_gravity"};
case THREE -> new String[] {"gravity_stomp", "black_hole", "momentum_lock", "crushing_gravity", "acceleration"};
case THREE -> new String[] {"gravity_stomp", "black_hole", "momentum_lock", "crushing_gravity", "acceleration", "queen_chronosphere"};
};
String forcedAttack = this.pendingPhaseSignature == Phase.TWO ? "momentum_lock"
: this.pendingPhaseSignature == Phase.THREE ? "acceleration" : null;
@@ -888,12 +901,14 @@ public class QueenEntity extends RoyalBossEntity {
case "black_hole" -> BLACK_HOLE_COOLDOWN;
case "momentum_lock" -> MOMENTUM_LOCK_COOLDOWN;
case "crushing_gravity" -> CRUSHING_GRAVITY_COOLDOWN;
case "queen_chronosphere" -> QUEEN_CHRONOSPHERE_COOLDOWN;
default -> ACCELERATION_COOLDOWN;
};
RoyalAttackLane lane = "black_hole".equals(selected) ? RoyalAttackLane.CENTER_HEAD : RoyalAttackLane.HAZARD;
int actionWindup = this.queenAbilityWindup(selected);
int actionAnimationTicks = "momentum_lock".equals(selected)
|| "crushing_gravity".equals(selected) || "acceleration".equals(selected) ? 55 : 25;
|| "crushing_gravity".equals(selected) || "acceleration".equals(selected)
|| "queen_chronosphere".equals(selected) ? 55 : 25;
int actionRecovery = this.animationRecovery(actionAnimationTicks, actionWindup, 1, 14);
if (!this.beginRoyalAttack(selected, lane, this.cooldown(cooldown),
actionWindup, 1, actionRecovery, new com.craisinlord.antarchy.content.entity.royal.attack.RoyalAttackScheduler.Action() {
@@ -902,7 +917,8 @@ public class QueenEntity extends RoyalBossEntity {
QueenEntity.this.triggerAnim("body_action", "stomp");
} else if ("momentum_lock".equals(selected)
|| "crushing_gravity".equals(selected)
|| "acceleration".equals(selected)) {
|| "acceleration".equals(selected)
|| "queen_chronosphere".equals(selected)) {
QueenEntity.this.triggerAnim("wing_action", "wing_gust");
} else if ("black_hole".equals(selected)) {
QueenEntity.this.royalHead(RoyalHead.Slot.CENTER).startShoot();
@@ -965,6 +981,13 @@ public class QueenEntity extends RoyalBossEntity {
this.startRoyalAcceleration();
this.queueQueenFollowUp(QueenFollowUp.ACCELERATION_VOLLEY, ACCELERATION_VOLLEY_DELAY);
}
case "queen_chronosphere" -> {
this.queenChronosphereCooldownTicks = cooldown;
TimeDilationFieldEntity field = TimeDilationApi.createField(level, this.position(),
QUEEN_CHRONOSPHERE_RADIUS, 0.08D, QUEEN_CHRONOSPHERE_DURATION, this.getUUID());
field.configureQueenChronosphere(this);
this.startRoyalRecovery(35);
}
}
}
@@ -998,6 +1021,7 @@ public class QueenEntity extends RoyalBossEntity {
case "momentum_lock" -> !this.royalEffects.active("momentum_lock") && this.momentumLockCooldownTicks <= 0;
case "crushing_gravity" -> phase != Phase.ONE && !this.royalEffects.active("crushing_gravity") && this.crushingGravityCooldownTicks <= 0;
case "acceleration" -> phase == Phase.THREE && !this.royalEffects.active("acceleration") && this.accelerationCooldownTicks <= 0;
case "queen_chronosphere" -> phase == Phase.THREE && this.queenChronosphereCooldownTicks <= 0;
default -> false;
};
}
@@ -1013,6 +1037,7 @@ public class QueenEntity extends RoyalBossEntity {
};
case THREE -> switch (id) {
case "acceleration" -> 7;
case "queen_chronosphere" -> 6;
case "black_hole", "crushing_gravity" -> 5;
case "gravity_stomp" -> 4;
case "momentum_lock" -> 2;
@@ -1084,6 +1109,7 @@ public class QueenEntity extends RoyalBossEntity {
case "momentum_lock" -> MOMENTUM_LOCK_TELEGRAPH_TICKS;
case "crushing_gravity" -> CRUSHING_GRAVITY_TELEGRAPH_TICKS;
case "acceleration" -> ACCELERATION_TELEGRAPH_TICKS;
case "queen_chronosphere" -> 40;
default -> 10;
};
}
@@ -1111,6 +1137,10 @@ public class QueenEntity extends RoyalBossEntity {
this.accelerationTelegraphTicks = ACCELERATION_TELEGRAPH_TICKS;
this.playRoyalSound(AntarchySoundEvents.QUEEN_ROAR.get(), 1.22F);
}
case "queen_chronosphere" -> {
this.queenChronosphereTelegraphTicks = 40;
this.playRoyalSound(AntarchySoundEvents.QUEEN_ROAR.get(), 0.85F);
}
}
}
@@ -1121,13 +1151,14 @@ public class QueenEntity extends RoyalBossEntity {
case "momentum_lock" -> this.momentumLockTelegraphTicks = 0;
case "crushing_gravity" -> this.crushingGravityTelegraphTicks = 0;
case "acceleration" -> this.accelerationTelegraphTicks = 0;
case "queen_chronosphere" -> this.queenChronosphereTelegraphTicks = 0;
}
}
private boolean isQueenAbilityTelegraphActive() {
return this.gravityStompTelegraphTicks > 0 || this.blackHoleTelegraphTicks > 0
|| this.momentumLockTelegraphTicks > 0 || this.crushingGravityTelegraphTicks > 0
|| this.accelerationTelegraphTicks > 0;
|| this.accelerationTelegraphTicks > 0 || this.queenChronosphereTelegraphTicks > 0;
}
private void clearQueenAbilityTelegraphs() {
@@ -1136,6 +1167,7 @@ public class QueenEntity extends RoyalBossEntity {
this.momentumLockTelegraphTicks = 0;
this.crushingGravityTelegraphTicks = 0;
this.accelerationTelegraphTicks = 0;
this.queenChronosphereTelegraphTicks = 0;
this.pendingBlackHoleAnchor = null;
}
@@ -1150,6 +1182,7 @@ public class QueenEntity extends RoyalBossEntity {
this.tickMomentumLockTelegraph(level);
this.tickCrushingGravityTelegraph(level);
this.tickAccelerationTelegraph(level);
this.tickQueenChronosphereTelegraph(level);
}
private void tickGravityStompTelegraph(ServerLevel level) {
@@ -1235,6 +1268,18 @@ public class QueenEntity extends RoyalBossEntity {
this.accelerationTelegraphTicks--;
}
private void tickQueenChronosphereTelegraph(ServerLevel level) {
if (this.queenChronosphereTelegraphTicks <= 0) return;
int elapsed = 40 - this.queenChronosphereTelegraphTicks;
Vec3 center = this.position().add(0.0D, this.getBbHeight() * 0.5D, 0.0D);
if (this.tickCount % 2 == 0) {
double radius = Mth.lerp(elapsed / 40.0D, 3.0D, QUEEN_CHRONOSPHERE_RADIUS);
this.spawnQueenTelegraphRing(level, center, radius, 96, ParticleTypes.REVERSE_PORTAL);
}
this.playQueenTelegraphBeat(level, this.queenChronosphereTelegraphTicks, 40, 0.5F);
this.queenChronosphereTelegraphTicks--;
}
private void spawnQueenTelegraphSpokes(ServerLevel level, Vec3 center, double radius) {
int spokes = 10;
int segments = 6;
@@ -1524,6 +1569,7 @@ public class QueenEntity extends RoyalBossEntity {
tag.putInt("MomentumLockCooldownTicks", this.momentumLockCooldownTicks);
tag.putInt("CrushingGravityCooldownTicks", this.crushingGravityCooldownTicks);
tag.putInt("AccelerationCooldownTicks", this.accelerationCooldownTicks);
tag.putInt("QueenChronosphereCooldownTicks", this.queenChronosphereCooldownTicks);
tag.putInt("BlackHoleCooldownTicks", this.blackHoleCooldownTicks);
tag.putInt("QueenLandingCooldownTicks", this.queenLandingCooldownTicks);
tag.putString("QueuedQueenFollowUp", this.queuedQueenFollowUp.name());
@@ -1547,6 +1593,7 @@ public class QueenEntity extends RoyalBossEntity {
this.momentumLockCooldownTicks = Math.max(0, tag.getInt("MomentumLockCooldownTicks"));
this.crushingGravityCooldownTicks = Math.max(0, tag.getInt("CrushingGravityCooldownTicks"));
this.accelerationCooldownTicks = Math.max(0, tag.getInt("AccelerationCooldownTicks"));
this.queenChronosphereCooldownTicks = Math.max(0, tag.getInt("QueenChronosphereCooldownTicks"));
this.blackHoleCooldownTicks = Math.max(0, tag.getInt("BlackHoleCooldownTicks"));
this.queenLandingCooldownTicks = tag.contains("QueenLandingCooldownTicks")
? Math.max(0, tag.getInt("QueenLandingCooldownTicks")) : 300;
@@ -850,8 +850,7 @@ public abstract class RoyalBossEntity extends Monster implements GeoEntity, Mult
Vec3 awayFromTarget = horizontalDistance > 1.0E-4D
? new Vec3(-dx / horizontalDistance, 0.0D, -dz / horizontalDistance)
: this.getViewVector(1.0F).multiply(-1.0D, 0.0D, -1.0D).normalize();
double standoff = Math.max(this.getBbWidth() * 0.55D,
BEAM_MUZZLE_FORWARD - this.biteReach() * 0.65D + 1.0D);
double standoff = this.combatStandoffDistance();
Vec3 wanted = target.position().add(awayFromTarget.scale(standoff));
boolean atStandoff = this.position().multiply(1.0D, 0.0D, 1.0D)
.distanceToSqr(wanted.multiply(1.0D, 0.0D, 1.0D)) <= 4.0D;
@@ -908,6 +907,11 @@ public abstract class RoyalBossEntity extends Monster implements GeoEntity, Mult
return false;
}
protected double combatStandoffDistance() {
return Math.max(this.getBbWidth() * 0.55D,
BEAM_MUZZLE_FORWARD - this.biteReach() * 0.65D + 1.0D);
}
private void tickCombatLocomotionMode(LivingEntity target) {
if (!this.isFlyingBoss() || this.managesOwnCombatLocomotion()) {
return;
@@ -42,6 +42,11 @@ public class RoyalElementalProjectileEntity extends ThrowableProjectile implemen
private static final int ICEBALL = 1;
private static final int DREAM_FIREBALL = 2;
private static final int MAX_LIFETIME = 100;
private static final float FIREBALL_SPEED = 1.5F;
private static final float ICEBALL_SPEED = 1.2F;
private static final float DREAM_FIREBALL_SPEED = 1.05F;
private static final double PROJECTILE_INERTIA = 0.985D;
private static final float DREAM_FIREBALL_INERTIA = 0.96F;
private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this);
public RoyalElementalProjectileEntity(EntityType<? extends RoyalElementalProjectileEntity> type, Level level) {
@@ -57,10 +62,38 @@ public class RoyalElementalProjectileEntity extends ThrowableProjectile implemen
projectile.setPos(position.x, position.y, position.z);
projectile.setAttackType(element == RoyalBeamElement.ICE ? ICEBALL
: element == RoyalBeamElement.DREAM_FIRE ? DREAM_FIREBALL : FIREBALL);
projectile.shoot(direction.x, direction.y, direction.z, element == RoyalBeamElement.ICE ? 0.8F : 1.05F, 0.0F);
float speed = element == RoyalBeamElement.ICE ? ICEBALL_SPEED
: element == RoyalBeamElement.DREAM_FIRE ? DREAM_FIREBALL_SPEED : FIREBALL_SPEED;
projectile.shoot(direction.x, direction.y, direction.z, speed, 0.0F);
return projectile;
}
/** Solves a discrete ballistic lead against the projectile's drag and gravity. */
public static Vec3 aimDirection(Vec3 origin, LivingEntity target, RoyalBeamElement element) {
double speed = element == RoyalBeamElement.ICE ? ICEBALL_SPEED : FIREBALL_SPEED;
double gravity = element == RoyalBeamElement.ICE ? 0.006D : 0.018D;
double inertia = PROJECTILE_INERTIA;
Vec3 bestDirection = target.getEyePosition().subtract(origin).normalize();
double bestError = Double.MAX_VALUE;
for (int ticks = 1; ticks < MAX_LIFETIME; ticks++) {
double dragDistance = (1.0D - Math.pow(inertia, ticks)) / (1.0D - inertia);
Vec3 predictedTarget = target.getEyePosition().add(target.getDeltaMovement().scale(ticks));
Vec3 displacement = predictedTarget.subtract(origin);
double gravityCompensation = gravity * (ticks - dragDistance) / (1.0D - inertia);
Vec3 initialVelocity = new Vec3(
displacement.x / dragDistance,
(displacement.y + gravityCompensation) / dragDistance,
displacement.z / dragDistance);
double error = Math.abs(initialVelocity.length() - speed);
if (error < bestError) {
bestError = error;
bestDirection = initialVelocity.normalize();
}
}
return bestDirection;
}
@Override
protected void defineSynchedData(SynchedEntityData.Builder builder) {
builder.define(ATTACK_TYPE, FIREBALL);
@@ -80,11 +113,11 @@ public class RoyalElementalProjectileEntity extends ThrowableProjectile implemen
@Override
protected double getDefaultGravity() {
return this.isIceball() ? 0.012D : 0.018D;
return this.isIceball() ? 0.006D : 0.018D;
}
protected float getInertia() {
return this.isIceball() ? 0.985F : 0.96F;
return this.isDreamFireball() ? DREAM_FIREBALL_INERTIA : (float) PROJECTILE_INERTIA;
}
@Override
@@ -10,7 +10,7 @@ import com.craisinlord.antarchy.content.entity.royal.beam.RoyalBeamTerrainMode;
import com.craisinlord.antarchy.content.effect.RoyalEffectEligibility;
import com.craisinlord.antarchy.content.effect.RoyalEffectHooks;
import com.craisinlord.antarchy.content.time.TimeDilationApi;
import java.util.UUID;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.EntityDataAccessor;
@@ -34,6 +34,7 @@ import net.minecraft.world.entity.MoverType;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.TamableAnimal;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.control.FlyingMoveControl;
import net.minecraft.world.entity.ai.control.MoveControl;
@@ -71,6 +72,7 @@ import software.bernie.geckolib.animation.RawAnimation;
import software.bernie.geckolib.util.GeckoLibUtil;
public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntity, FlyingAnimal {
private static final ResourceLocation GROWTH_HEALTH_MODIFIER = ResourceLocation.fromNamespaceAndPath("antarchy", "royal_mount_growth_health");
public static final int ANIM_IDLE = 0;
public static final int ANIM_WALK = 1;
public static final int ANIM_FLY = 2;
@@ -309,6 +311,9 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
this.navigation = flying ? this.flyingNavigation : this.groundNavigation;
this.setNoGravity(flying);
}
if (GROWTH_PROGRESS.equals(key)) {
this.refreshDimensions();
}
}
public void setFlying(boolean flying) {
@@ -350,11 +355,30 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
public void setGrowthProgress(float progress) {
float clamped = Mth.clamp(progress, 0.0F, 1.0F);
if (this.getGrowthProgress() != clamped) {
float oldHealth = this.getHealth();
this.entityData.set(GROWTH_PROGRESS, clamped);
var maxHealth = this.getAttribute(Attributes.MAX_HEALTH);
if (maxHealth != null) {
maxHealth.removeModifier(GROWTH_HEALTH_MODIFIER);
if (clamped > 0.0F) {
maxHealth.addTransientModifier(new AttributeModifier(GROWTH_HEALTH_MODIFIER,
this.getGrowthScale() - 1.0D, AttributeModifier.Operation.ADD_MULTIPLIED_BASE));
}
this.setHealth(Math.min(oldHealth, this.getMaxHealth()));
}
this.refreshDimensions();
}
}
@Override
public boolean hurt(DamageSource source, float amount) {
Entity attacker = source.getEntity();
if (attacker != null && this.hasPassenger(attacker)) {
return false;
}
return super.hurt(source, amount);
}
@Override
public EntityDimensions getDefaultDimensions(Pose pose) {
return super.getDefaultDimensions(pose).scale(this.getGrowthScale());
@@ -369,19 +393,37 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
public InteractionResult mobInteract(Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (this.isFood(stack)) {
if (stack.is(Items.GOLDEN_APPLE) && !this.isTame()) {
if (!this.level().isClientSide) {
if (!player.getAbilities().instabuild) {
stack.shrink(1);
}
this.tame(player);
this.heal(8.0F);
this.level().broadcastEntityEvent(this, (byte) 7);
}
return InteractionResult.sidedSuccess(this.level().isClientSide);
}
if (stack.is(Items.ENCHANTED_GOLDEN_APPLE) && this.isTame() && this.isOwnedBy(player)) {
if (!this.level().isClientSide) {
if (!player.getAbilities().instabuild) {
stack.shrink(1);
}
this.heal(8.0F);
this.setGrowthProgress(this.getGrowthProgress() + (1.0F - this.getGrowthProgress()) * 0.1F);
if (!this.isTame() && this.random.nextInt(3) == 0) {
this.tame(player);
this.level().broadcastEntityEvent(this, (byte) 7);
} else {
this.level().broadcastEntityEvent(this, (byte) 6);
this.level().broadcastEntityEvent(this, (byte) 6);
}
return InteractionResult.sidedSuccess(this.level().isClientSide);
}
if (this.isFood(stack)) {
if (!this.level().isClientSide && this.isTame() && this.isOwnedBy(player) && this.getHealth() < this.getMaxHealth()) {
if (!player.getAbilities().instabuild) {
stack.shrink(1);
}
this.heal(8.0F);
this.level().broadcastEntityEvent(this, (byte) 6);
}
return InteractionResult.sidedSuccess(this.level().isClientSide);
}
@@ -399,12 +441,7 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
}
if (stack.isEmpty() && player.isSecondaryUseActive()) {
if (this.isSaddled()) {
if (!this.level().isClientSide) {
this.setSaddled(false);
this.spawnAtLocation(new ItemStack(Items.SADDLE));
}
} else if (!this.level().isClientSide) {
if (!this.level().isClientSide) {
this.setOrderedToSit(!this.isOrderedToSit());
this.setInSittingPose(this.isOrderedToSit());
this.navigation.stop();
@@ -762,8 +799,10 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
if (!this.hasPassenger(passenger)) {
return;
}
double yOffset = (this.isFlying() ? 1.55D : 1.35D) * this.getGrowthScale() - 1.0D;
Vec3 back = this.getLookAngle().scale(-0.35D * this.getGrowthScale());
// Seat near the back of the torso, outside the scaled body instead of inside its growing hitbox.
double yOffset = Math.max(this.getBbHeight() * 0.82D, this.getBbHeight() - passenger.getBbHeight() * 0.65D);
double backDistance = this.getBbWidth() * 0.5D + passenger.getBbWidth() * 0.5D + 0.15D;
Vec3 back = this.getLookAngle().scale(-backDistance);
moveFunction.accept(passenger, this.getX() + back.x, this.getY() + yOffset, this.getZ() + back.z);
}
@@ -11,9 +11,14 @@ public final class StandYourGroundDecree implements RoyalDecree {
private final Map<UUID, Vec3> anchors = new HashMap<>();
public String translationKey() { return "decree.antarchy.stand_your_ground"; }
public void apply(ServerLevel level, KingEntity king, LivingEntity target) {
Vec3 anchor = this.anchors.computeIfAbsent(target.getUUID(), ignored -> target.position());
this.anchors.computeIfAbsent(target.getUUID(), ignored -> target.position());
}
@Override public Evaluation evaluate(ServerLevel level, KingEntity king, LivingEntity target) {
if (target instanceof net.minecraft.server.level.ServerPlayer player && player.isFallFlying()) {
// Gliding carries unavoidable momentum; begin measuring again from the landing point.
this.anchors.put(target.getUUID(), target.position());
return Evaluation.COMPLIANT;
}
Vec3 anchor = this.anchors.computeIfAbsent(target.getUUID(), ignored -> target.position());
return target.position().distanceToSqr(anchor) > ALLOWED_DRIFT * ALLOWED_DRIFT
? Evaluation.VIOLATED : Evaluation.COMPLIANT;
@@ -1,6 +1,6 @@
package com.craisinlord.antarchy.content.entity.trades;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antos.content.AntOSObjects;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -118,8 +118,8 @@ public final class ComputerScientistTradeManager extends SimpleJsonResourceReloa
ResourceLocation diskId = diskType.equals("random_general")
? GENERAL_DISKS.get(random.nextInt(GENERAL_DISKS.size()))
: ResourceLocation.parse(diskType);
ItemStack disk = new ItemStack(AntarchyObjects.FLOPPY_DISK.get(), count);
disk.set(AntarchyObjects.FLOPPY_DISK_COMPONENT.get(), diskId);
ItemStack disk = new ItemStack(AntOSObjects.FLOPPY_DISK.get(), count);
disk.set(AntOSObjects.FLOPPY_DISK_COMPONENT.get(), diskId);
return disk;
}
@@ -0,0 +1,76 @@
package com.craisinlord.antarchy.content.entity.trades;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
import net.minecraft.util.RandomSource;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.entity.npc.VillagerTrades;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.trading.ItemCost;
import net.minecraft.world.item.trading.MerchantOffer;
public final class PestControlTradeManager extends SimpleJsonResourceReloadListener {
private static final Gson GSON = new Gson();
private static volatile List<Entry> trades = List.of();
public PestControlTradeManager() {
super(GSON, "pest_control_trades");
}
public static int[] slotsPerLevel() {
return new int[] {2, 2, 2, 1, 1};
}
public static VillagerTrades.ItemListing listing(int level, int index) {
return (villager, random) -> trades.stream().filter(entry -> entry.level == level && entry.index == index)
.findFirst().map(entry -> entry.offer(random)).orElse(null);
}
@Override
protected void apply(Map<ResourceLocation, JsonElement> resources, ResourceManager manager, ProfilerFiller profiler) {
List<Entry> parsed = new ArrayList<>();
for (JsonElement resource : resources.values()) {
if (!resource.isJsonObject()) continue;
JsonObject root = resource.getAsJsonObject();
if (root.has("trades")) {
for (JsonElement element : root.getAsJsonArray("trades")) if (element.isJsonObject()) parsed.add(Entry.parse(element.getAsJsonObject()));
} else parsed.add(Entry.parse(root));
}
trades = List.copyOf(parsed);
}
private record Entry(int level, int index, String buy, int buyCount, String sell, int sellCount,
String secondBuy, int secondBuyCount, int maxUses, int xp) {
private MerchantOffer offer(RandomSource random) {
var buyItem = BuiltInRegistries.ITEM.get(ResourceLocation.parse(buy));
var sellItem = BuiltInRegistries.ITEM.get(ResourceLocation.parse(sell));
var secondCost = secondBuy == null ? java.util.Optional.<ItemCost>empty()
: java.util.Optional.of(new ItemCost(BuiltInRegistries.ITEM.get(ResourceLocation.parse(secondBuy)), secondBuyCount));
return new MerchantOffer(new ItemCost(buyItem, buyCount), secondCost, new ItemStack(sellItem, sellCount), maxUses, xp, 0.05F);
}
private static Entry parse(JsonObject json) {
JsonObject buy = json.getAsJsonObject("buy");
JsonObject sell = json.getAsJsonObject("sell");
JsonObject second = json.has("second_buy") ? json.getAsJsonObject("second_buy") : null;
return new Entry(json.get("level").getAsInt(), json.get("index").getAsInt(), buy.get("item").getAsString(),
count(buy), sell.get("item").getAsString(), count(sell), second == null ? null : second.get("item").getAsString(),
second == null ? 0 : count(second), json.has("max_uses") ? json.get("max_uses").getAsInt() : 12,
json.has("villager_xp") ? json.get("villager_xp").getAsInt() : 5);
}
private static int count(JsonObject stack) {
if (stack.has("min") && stack.has("max")) return stack.get("min").getAsInt();
return stack.has("count") ? stack.get("count").getAsInt() : stack.get("min").getAsInt();
}
}
}
@@ -1,290 +0,0 @@
package com.craisinlord.antarchy.content.guide;
import com.craisinlord.antarchy.Antarchy;
import com.craisinlord.antarchy.config.AntarchySettings;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class ComputerGuideData extends SimplePreparableReloadListener<ComputerGuideData.LoadedData> {
private static final ComputerGuideData INSTANCE = new ComputerGuideData();
private static final ResourceLocation INTRODUCTION = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
private static volatile Map<ResourceLocation, Entry> entries = Map.of();
private static volatile Map<ResourceLocation, Disk> disks = Map.of();
private ComputerGuideData() {
}
public static ComputerGuideData instance() {
return INSTANCE;
}
public static Entry entry(ResourceLocation id) {
return entries.get(id);
}
public static Disk disk(ResourceLocation id) {
return disks.get(id);
}
public static List<Entry> entriesFor(List<ResourceLocation> diskIds) {
if (AntarchySettings.unlockAllArchives()) {
Map<ResourceLocation, Entry> result = new LinkedHashMap<>();
result.put(INTRODUCTION, introduction());
result.putAll(entries);
return result.values().stream().sorted(Comparator.comparing(entry -> entry.titleKey().toString())).toList();
}
Map<ResourceLocation, Entry> result = new LinkedHashMap<>();
for (ResourceLocation diskId : diskIds) {
if (INTRODUCTION.equals(diskId)) {
result.put(INTRODUCTION, introduction());
continue;
}
Disk disk = disks.get(diskId);
if (disk == null) {
continue;
}
for (ResourceLocation entryId : disk.entries()) {
Entry entry = entries.get(entryId);
if (entry != null) {
result.putIfAbsent(entryId, entry);
}
}
}
return result.values().stream().sorted(Comparator.comparing(entry -> entry.titleKey().toString())).toList();
}
private static Entry introduction() {
return new Entry(INTRODUCTION, "article", "general", "guide.antarchy.entry.introduction.title",
"guide.antarchy.entry.introduction.subtitle", List.of("guide.antarchy.entry.introduction.description"),
"", "", "", "", "", "", "", 0, "", "", "", 0.0F, 1.0F);
}
/** Encodes the server data-pack view so remote clients do not need a duplicate asset copy. */
public static String encodeNetworkSnapshot() {
JsonObject root = new JsonObject();
JsonArray entryArray = new JsonArray();
entries.values().stream().sorted(Comparator.comparing(entry -> entry.id().toString())).forEach(entry -> {
JsonObject object = new JsonObject();
object.addProperty("id", entry.id().toString());
object.addProperty("type", entry.type());
object.addProperty("category", entry.category());
object.addProperty("title", entry.titleKey());
object.addProperty("subtitle", entry.subtitleKey());
JsonArray descriptions = new JsonArray();
entry.descriptionKeys().forEach(descriptions::add);
object.add("description", descriptions);
object.addProperty("item", entry.itemId());
object.addProperty("entity", entry.entityId());
object.addProperty("enchantment", entry.enchantmentId());
object.addProperty("recipe", entry.recipeId());
object.addProperty("structure", entry.structureId());
object.addProperty("structure_tag", entry.structureTagId());
object.addProperty("dimension", entry.dimensionId());
object.addProperty("search_radius", entry.searchRadius());
object.addProperty("cover_item", entry.coverItemId());
object.addProperty("cover_entity", entry.coverEntityId());
object.addProperty("cover_potion", entry.coverPotionId());
object.addProperty("rotation", entry.rotation());
object.addProperty("render_scale", entry.renderScale());
entryArray.add(object);
});
root.add("entries", entryArray);
JsonArray diskArray = new JsonArray();
disks.values().stream().sorted(Comparator.comparing(disk -> disk.id().toString())).forEach(disk -> {
JsonObject object = new JsonObject();
object.addProperty("id", disk.id().toString());
object.addProperty("category", disk.category());
object.addProperty("title", disk.titleKey());
JsonArray diskEntries = new JsonArray();
disk.entries().forEach(id -> diskEntries.add(id.toString()));
object.add("entries", diskEntries);
object.addProperty("wallpaper", disk.wallpaper());
diskArray.add(object);
});
root.add("disks", diskArray);
return root.toString();
}
/** Applies the authoritative server snapshot on a multiplayer client. */
public static void applyNetworkSnapshot(String json) {
try {
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
Map<ResourceLocation, Entry> loadedEntries = new HashMap<>();
Map<ResourceLocation, Disk> loadedDisks = new HashMap<>();
JsonArray entryArray = root.getAsJsonArray("entries");
if (entryArray != null) {
for (JsonElement element : entryArray) {
JsonObject object = element.getAsJsonObject();
parseEntry(ResourceLocation.parse(string(object, "id", "")), object, loadedEntries);
}
}
JsonArray diskArray = root.getAsJsonArray("disks");
if (diskArray != null) {
for (JsonElement element : diskArray) {
JsonObject object = element.getAsJsonObject();
parseDisk(ResourceLocation.parse(string(object, "id", "")), object, loadedDisks);
}
}
entries = Map.copyOf(loadedEntries);
disks = Map.copyOf(loadedDisks);
Antarchy.LOGGER.info("Loaded {} computer archive entries and {} disks from the server", entries.size(), disks.size());
} catch (RuntimeException exception) {
Antarchy.LOGGER.error("Failed to decode computer archive data from the server", exception);
}
}
@Override
protected LoadedData prepare(ResourceManager resourceManager, ProfilerFiller profiler) {
Map<ResourceLocation, Entry> loadedEntries = new HashMap<>();
Map<ResourceLocation, Disk> loadedDisks = new HashMap<>();
for (Map.Entry<ResourceLocation, net.minecraft.server.packs.resources.Resource> resource : resourceManager.listResources("computer/entry", path -> path.getPath().endsWith(".json")).entrySet()) {
ResourceLocation id = resourceId(resource.getKey(), "computer/entry");
try {
JsonElement json = JsonParser.parseReader(resource.getValue().openAsReader());
parseEntry(id, json, loadedEntries);
} catch (Exception exception) {
Antarchy.LOGGER.error("Failed to load computer archive entry {}", resource.getKey(), exception);
}
}
for (Map.Entry<ResourceLocation, net.minecraft.server.packs.resources.Resource> resource : resourceManager.listResources("computer/disk", path -> path.getPath().endsWith(".json")).entrySet()) {
ResourceLocation id = resourceId(resource.getKey(), "computer/disk");
try {
JsonElement json = JsonParser.parseReader(resource.getValue().openAsReader());
parseDisk(id, json, loadedDisks);
} catch (Exception exception) {
Antarchy.LOGGER.error("Failed to load computer archive disk {}", resource.getKey(), exception);
}
}
return new LoadedData(Map.copyOf(loadedEntries), Map.copyOf(loadedDisks));
}
@Override
protected void apply(LoadedData data, ResourceManager resourceManager, ProfilerFiller profiler) {
entries = data.entries();
disks = data.disks();
}
private static ResourceLocation resourceId(ResourceLocation path, String directory) {
String prefix = directory + "/";
String value = path.getPath().startsWith(prefix) ? path.getPath().substring(prefix.length()) : path.getPath();
if (value.endsWith(".json")) {
value = value.substring(0, value.length() - 5);
}
return ResourceLocation.fromNamespaceAndPath(path.getNamespace(), value);
}
private static void parseEntry(ResourceLocation id, JsonElement element, Map<ResourceLocation, Entry> target) {
if (!element.isJsonObject()) {
return;
}
JsonObject object = element.getAsJsonObject();
String type = string(object, "type", "article");
String category = string(object, "category", "general");
String title = string(object, "title", "guide.antarchy.missing_title");
String subtitle = string(object, "subtitle", "");
List<String> description = strings(object.get("description"));
String item = string(object, "item", "");
String entity = string(object, "entity", "");
String enchantment = string(object, "enchantment", "");
String recipe = string(object, "recipe", "");
String structure = string(object, "structure", "");
String structureTag = string(object, "structure_tag", "");
String dimension = string(object, "dimension", "");
int searchRadius = object.has("search_radius") && object.get("search_radius").isJsonPrimitive()
? Math.max(0, Math.min(1000, object.get("search_radius").getAsInt())) : 0;
String coverItem = string(object, "cover_item", "");
String coverEntity = string(object, "cover_entity", "");
String coverPotion = string(object, "cover_potion", "");
float rotation = object.has("rotation") && object.get("rotation").isJsonPrimitive()
? object.get("rotation").getAsFloat() : 0.0F;
float renderScale = object.has("render_scale") && object.get("render_scale").isJsonPrimitive()
? object.get("render_scale").getAsFloat() : 1.0F;
renderScale = Math.max(0.1F, Math.min(3.0F, renderScale));
target.put(id, new Entry(id, type, category, title, subtitle, description, item, entity, enchantment, recipe, structure, structureTag, dimension, searchRadius,
coverItem, coverEntity, coverPotion, rotation, renderScale));
}
private static void parseDisk(ResourceLocation id, JsonElement element, Map<ResourceLocation, Disk> target) {
if (!element.isJsonObject()) {
return;
}
JsonObject object = element.getAsJsonObject();
String category = string(object, "category", "general");
String title = string(object, "title", "guide.antarchy.missing_disk_title");
List<ResourceLocation> entryIds = new ArrayList<>();
JsonElement entriesElement = object.get("entries");
if (entriesElement != null && entriesElement.isJsonArray()) {
for (JsonElement entry : entriesElement.getAsJsonArray()) {
if (entry.isJsonPrimitive() && entry.getAsJsonPrimitive().isString()) {
try {
entryIds.add(ResourceLocation.parse(entry.getAsString()));
} catch (Exception ignored) {
}
}
}
}
String wallpaper = string(object, "wallpaper", "");
target.put(id, new Disk(id, category, title, List.copyOf(entryIds), wallpaper));
}
private static String string(JsonObject object, String key, String fallback) {
JsonElement element = object.get(key);
return element != null && element.isJsonPrimitive() ? element.getAsString() : fallback;
}
private static List<String> strings(JsonElement element) {
if (element == null) {
return List.of();
}
if (element.isJsonPrimitive()) {
return List.of(element.getAsString());
}
if (!element.isJsonArray()) {
return List.of();
}
List<String> values = new ArrayList<>();
JsonArray array = element.getAsJsonArray();
for (JsonElement value : array) {
if (value.isJsonPrimitive()) {
values.add(value.getAsString());
} else if (value.isJsonObject()) {
JsonObject render = value.getAsJsonObject();
String item = string(render, "item", "");
String entity = string(render, "entity", "");
String enchantment = string(render, "enchantment", "");
String recipe = string(render, "recipe", "");
if (!item.isBlank()) values.add("@item:" + item);
else if (!entity.isBlank()) values.add("@entity:" + entity);
else if (!enchantment.isBlank()) values.add("@enchantment:" + enchantment);
else if (!recipe.isBlank()) values.add("@recipe:" + recipe);
}
}
return List.copyOf(values);
}
public record Entry(ResourceLocation id, String type, String category, String titleKey, String subtitleKey,
List<String> descriptionKeys, String itemId, String entityId, String enchantmentId,
String recipeId, String structureId, String structureTagId, String dimensionId, int searchRadius, String coverItemId,
String coverEntityId, String coverPotionId, float rotation, float renderScale) {
}
public record Disk(ResourceLocation id, String category, String titleKey, List<ResourceLocation> entries, String wallpaper) {
}
record LoadedData(Map<ResourceLocation, Entry> entries, Map<ResourceLocation, Disk> disks) {
}
}
@@ -110,8 +110,7 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (player.isShiftKeyDown()) {
cycleMode(level, player, stack);
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
return InteractionResultHolder.pass(stack);
}
BossMode mode = getMode(stack);
@@ -390,8 +389,8 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
}
}
public boolean tryCycleModeWhileCoolingDown(Level level, Player player, ItemStack stack) {
if (player.getMainHandItem() != stack || !player.isShiftKeyDown() || !player.getCooldowns().isOnCooldown(this)) {
public boolean tryCycleModeFromInput(Level level, Player player, ItemStack stack) {
if (player.getMainHandItem() != stack) {
return false;
}
@@ -0,0 +1,60 @@
package com.craisinlord.antarchy.content.item;
import com.craisinlord.antarchy.content.AntarchyObjects;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Mob;
import net.minecraft.tags.EntityTypeTags;
import net.minecraft.tags.TagKey;
import net.minecraft.resources.ResourceLocation;
import com.craisinlord.antarchy.content.entity.ant.BaseAntEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
public final class BugSprayItem extends Item {
private static final TagKey<net.minecraft.world.entity.EntityType<?>> ANARCHY_ARTHROPODS = TagKey.create(
net.minecraft.core.registries.Registries.ENTITY_TYPE, ResourceLocation.fromNamespaceAndPath("antarchy", "arthropods"));
public BugSprayItem(Properties properties) {
super(properties);
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (!level.isClientSide && level instanceof ServerLevel serverLevel) {
Vec3 look = player.getLookAngle().normalize();
Vec3 origin = player.getEyePosition().add(look.scale(0.6D));
var sprayParticle = AntarchyObjects.BUG_SPRAY_PARTICLE.get();
serverLevel.sendParticles(sprayParticle, origin.x, origin.y, origin.z, 18,
look.x * 0.45D, look.y * 0.45D, look.z * 0.45D, 0.06D);
AABB bounds = new AABB(origin, origin.add(look.scale(7.0D))).inflate(2.2D);
for (Entity entity : level.getEntities(player, bounds, candidate -> candidate instanceof Mob mob
&& (mob.getType().is(EntityTypeTags.SENSITIVE_TO_BANE_OF_ARTHROPODS)
|| mob.getType().is(ANARCHY_ARTHROPODS) || mob instanceof BaseAntEntity))) {
if (!(entity instanceof Mob mob)) continue;
Vec3 toward = mob.position().subtract(origin);
double distance = toward.length();
if (distance > 9.0D || distance < 0.001D || toward.normalize().dot(look) < 0.55D) continue;
Vec3 flee = mob.position().subtract(player.position()).normalize();
mob.getNavigation().moveTo(mob.getX() + flee.x * 6.0D, mob.getY(), mob.getZ() + flee.z * 6.0D, 1.25D);
mob.setTarget(null);
mob.setLastHurtByMob(null);
mob.setDeltaMovement(mob.getDeltaMovement().add(flee.scale(0.55D)).add(0.0D, 0.15D, 0.0D));
mob.hurtMarked = true;
serverLevel.sendParticles(sprayParticle, mob.getX(), mob.getY() + mob.getBbHeight() * 0.5D, mob.getZ(), 5, 0.12D, 0.12D, 0.12D, 0.02D);
}
level.playSound(null, player.blockPosition(), SoundEvents.FIRE_EXTINGUISH, SoundSource.PLAYERS, 0.7F, 1.2F);
stack.shrink(1);
}
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
}
}
@@ -1,10 +0,0 @@
package com.craisinlord.antarchy.content.item;
import com.craisinlord.antarchy.content.block.ComputerBlock;
import net.minecraft.world.item.BlockItem;
public final class ComputerItem extends BlockItem {
public ComputerItem(ComputerBlock block, Properties properties) {
super(block, properties);
}
}
@@ -1,53 +0,0 @@
package com.craisinlord.antarchy.content.item;
import com.craisinlord.antarchy.content.AntarchyObjects;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import java.util.List;
public final class FloppyDiskItem extends Item {
public FloppyDiskItem(Properties properties) {
super(properties.stacksTo(1));
}
public static float categoryModelProperty(ItemStack stack) {
ResourceLocation id = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
if (id == null) return 0.0F;
var disk = com.craisinlord.antarchy.content.guide.ComputerGuideData.disk(id);
if (disk == null) return 0.0F;
return switch (disk.category()) {
case "elythia" -> 1.0F;
case "cavaryn" -> 2.0F;
case "thoraxis" -> 3.0F;
default -> 0.0F;
};
}
@Override
public Component getName(ItemStack stack) {
ResourceLocation id = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
if (id != null) {
var disk = com.craisinlord.antarchy.content.guide.ComputerGuideData.disk(id);
if (disk != null) {
return Component.translatable(disk.titleKey());
}
}
return Component.translatable("item.antarchy.floppy_disk");
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context, List<Component> tooltip, TooltipFlag flag) {
tooltip.add(Component.translatable("item.antarchy.floppy_disk.tooltip"));
ResourceLocation id = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
if (id != null) {
var disk = com.craisinlord.antarchy.content.guide.ComputerGuideData.disk(id);
if (disk != null) {
tooltip.add(Component.translatable("guide.antarchy.category." + disk.category()));
}
}
}
}
@@ -2,6 +2,7 @@ package com.craisinlord.antarchy.content.item;
import com.craisinlord.antarchy.config.AntarchySettings;
import com.craisinlord.antarchy.content.effect.CommandedEntityAccess;
import com.craisinlord.antarchy.content.AntarchySoundEvents;
import com.craisinlord.antarchy.content.entity.royal.RoyalIceSpikeEntity;
import java.util.HashSet;
import java.util.Set;
@@ -126,21 +127,23 @@ public final class RoyalGuardianSwordAbilities {
if (forward.lengthSqr() < 1.0E-5D) forward = new Vec3(0.0D, 0.0D, 1.0D);
forward = forward.normalize();
Vec3 side = new Vec3(-forward.z, 0.0D, forward.x);
for (int distanceStep = 1; distanceStep <= 4; distanceStep++) {
double distance = 2.5D + distanceStep * 2.7D;
for (int distanceStep = 1; distanceStep <= 5; distanceStep++) {
double distance = distanceStep * 4.0D;
for (int lane = -1; lane <= 1; lane++) {
Vec3 point = primary.position().add(forward.scale(distance)).add(side.scale(lane * 1.8D));
BlockPos surface = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, BlockPos.containing(point));
Vec3 spawn = new Vec3(point.x, surface.getY() + 0.1D, point.z);
Vec3 point = owner.position().add(forward.scale(distance))
.add(side.scale(lane * (distanceStep % 2 == 0 ? 2.0D : 0.0D)));
int groundY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
net.minecraft.util.Mth.floor(point.x), net.minecraft.util.Mth.floor(point.z));
Vec3 spawn = new Vec3(point.x, groundY + 0.1D, point.z);
RoyalIceSpikeEntity spike = RoyalIceSpikeEntity.create(level, spawn, owner,
(float) AntarchySettings.royalGuardianSwordFrostSpikeDamage(), 1.55D, 100, 60, 1);
(float) AntarchySettings.royalGuardianSwordFrostSpikeDamage(), 2.2D, 100, 0, 0);
level.addFreshEntity(spike);
}
}
Vec3 center = primary.position().add(0.0D, 0.8D, 0.0D);
level.sendParticles(ParticleTypes.SNOWFLAKE, center.x, center.y, center.z, 70, 1.3D, 1.1D, 1.3D, 0.1D);
level.sendParticles(ParticleTypes.SNOWFLAKE, owner.getX(), owner.getY() + 1.0D, owner.getZ(), 18, 0.7D, 0.5D, 0.7D, 0.04D);
level.playSound(null, primary.blockPosition(), SoundEvents.GLASS_BREAK, SoundSource.PLAYERS, 1.0F, 0.6F);
level.playSound(null, primary.blockPosition(), AntarchySoundEvents.KING_ICE_SPIKES.get(), SoundSource.PLAYERS, 0.8F, 0.95F);
}
private static void chainLightning(ServerLevel level, Player owner, LivingEntity primary) {
@@ -187,9 +190,10 @@ public final class RoyalGuardianSwordAbilities {
double length = direction.length();
if (length > 0.001D) {
Vec3 step = direction.scale(1.0D / length);
for (double d = 0.0D; d < length; d += 1.2D) {
for (double d = 0.0D; d < length; d += 0.35D) {
Vec3 point = start.add(step.scale(d));
level.sendParticles(STORM_GOLD, point.x, point.y, point.z, 2, 0.08D, 0.08D, 0.08D, 0.04D);
level.sendParticles(ParticleTypes.ELECTRIC_SPARK, point.x, point.y, point.z, 1, 0.025D, 0.025D, 0.025D, 0.0D);
level.sendParticles(STORM_GOLD, point.x, point.y, point.z, 1, 0.025D, 0.025D, 0.025D, 0.0D);
}
}
level.sendParticles(ParticleTypes.ELECTRIC_SPARK, end.x, end.y, end.z, 18, 0.25D, 0.4D, 0.25D, 0.08D);
@@ -77,11 +77,11 @@ public class RoyalGuardianSwordItem extends SwordItem implements GeoItem {
public InteractionResultHolder<ItemStack> use(net.minecraft.world.level.Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (!player.isShiftKeyDown()) return InteractionResultHolder.pass(stack);
cycleMode(level, player, stack);
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
return InteractionResultHolder.pass(stack);
}
private void cycleMode(net.minecraft.world.level.Level level, Player player, ItemStack stack) {
public boolean tryCycleModeFromInput(net.minecraft.world.level.Level level, Player player, ItemStack stack) {
if (player.getMainHandItem() != stack) return false;
Mode next = getMode(stack).next();
setMode(stack, next);
if (!level.isClientSide) {
@@ -89,6 +89,7 @@ public class RoyalGuardianSwordItem extends SwordItem implements GeoItem {
Component.translatable(next.translationKey)), true);
level.playSound(null, player.blockPosition(), SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.8F, 0.8F + next.ordinal() * 0.2F);
}
return true;
}
private static void setMode(ItemStack stack, Mode mode) {
@@ -0,0 +1,121 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.config.AntarchySettings;
import com.craisinlord.antarchy.content.computer.AntarchyGameSavedData;
import com.craisinlord.antarchy.content.computer.blockle.BlockleAnswers;
import com.craisinlord.antarchy.content.computer.blockle.BlockleDictionary;
import com.craisinlord.antarchy.content.computer.blockle.BlockleGame;
import java.util.function.BiConsumer;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
/** Handles Antarchy-specific game progress sent by AntOS game extensions. */
public final class AntarchyGameNetworkHandler {
private static BiConsumer<ServerPlayer, AntarchyGameResultPayload> resultSender = (player, result) -> { };
private AntarchyGameNetworkHandler() { }
public static void setResultSender(BiConsumer<ServerPlayer, AntarchyGameResultPayload> sender) {
resultSender = sender;
}
public static void handle(ServerPlayer player, AntarchyGamePayload payload) {
BlockPos pos = payload.pos();
if (payload.value().length() > 65536 || player.level().isClientSide || !player.serverLevel().hasChunkAt(pos)
|| player.distanceToSqr(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D) > 64.0D) return;
var blockEntity = player.serverLevel().getBlockEntity(pos);
if (!(blockEntity instanceof com.craisinlord.antos.content.block.entity.ComputerBlockEntity computer)
|| !computer.canUseFileSystem(player)) {
send(player, payload, false, "", "unauthorized");
return;
}
String key = AntarchyGameSavedData.key(player.serverLevel().dimension().location(), pos);
AntarchyGameSavedData.GameState state = AntarchyGameSavedData.get(player.server, key);
switch (payload.action()) {
case AntarchyGamePayload.BASILISK_STATE -> basilisk(player, payload, state);
case AntarchyGamePayload.ANTMAN_STATE -> antman(player, payload, state);
case AntarchyGamePayload.BLOCKLE_STATE -> blockle(player, computer, payload, state, false);
case AntarchyGamePayload.BLOCKLE_GUESS -> blockle(player, computer, payload, state, true);
default -> send(player, payload, false, "", "invalid");
}
}
private static void basilisk(ServerPlayer player, AntarchyGamePayload payload, AntarchyGameSavedData.GameState state) {
if (!payload.value().isBlank()) {
Integer score = parseScore(payload.value());
if (score == null) { send(player, payload, false, "", "invalid_score"); return; }
state.basiliskScore = score;
state.basiliskHighScore = Math.max(state.basiliskHighScore, score);
AntarchyGameSavedData.changed(player.server);
}
send(player, payload, true, state.basiliskScore + "\0" + state.basiliskHighScore, "");
}
private static void antman(ServerPlayer player, AntarchyGamePayload payload, AntarchyGameSavedData.GameState state) {
if (!payload.value().isBlank()) {
Integer score = parseScore(payload.value());
if (score == null) { send(player, payload, false, "", "invalid_score"); return; }
state.antmanScore = score;
state.antmanHighScore = Math.max(state.antmanHighScore, score);
AntarchyGameSavedData.changed(player.server);
}
send(player, payload, true, state.antmanScore + "\0" + state.antmanHighScore, "");
}
private static Integer parseScore(String value) {
try { return Math.min(1_000_000, Math.max(0, Integer.parseInt(value))); }
catch (NumberFormatException ignored) { return null; }
}
private static void blockle(ServerPlayer player,
com.craisinlord.antos.content.block.entity.ComputerBlockEntity computer,
AntarchyGamePayload payload, AntarchyGameSavedData.GameState state, boolean submit) {
ResourceLocation disk = ResourceLocation.fromNamespaceAndPath("antarchy", "blockle_game");
if (!AntarchySettings.unlockAllArchives() && !computer.diskIds().contains(disk)) {
send(player, payload, false, "", "not_installed"); return;
}
ServerLevel overworld = player.server.getLevel(Level.OVERWORLD);
if (overworld == null) { send(player, payload, false, "", "overworld_unavailable"); return; }
long day = overworld.getDayTime() / 24000L;
if (state.blockleDay != day) {
state.blockleDay = day;
state.blockleGuesses.clear();
AntarchyGameSavedData.changed(player.server);
}
BlockleAnswers.Answer answer = BlockleAnswers.answerForDay(day);
if (answer == null) { send(player, payload, false, "", "answers_unavailable"); return; }
if (submit) {
String guess = payload.value().toLowerCase(java.util.Locale.ROOT);
if (guess.length() != 5 || !guess.chars().allMatch(value -> value >= 'a' && value <= 'z') || !BlockleDictionary.contains(guess)) {
send(player, payload, false, "", "invalid_word"); return;
}
if (state.blockleGuesses.size() >= 6 || state.blockleGuesses.contains(answer.word())) {
send(player, payload, false, "", "game_over"); return;
}
state.blockleGuesses.add(guess);
AntarchyGameSavedData.changed(player.server);
}
send(player, payload, true, encodeBlockle(day, answer, state.blockleGuesses), "");
}
private static String encodeBlockle(long day, BlockleAnswers.Answer answer, java.util.List<String> guesses) {
StringBuilder result = new StringBuilder(Long.toString(day));
boolean solved = false;
for (String guess : guesses) {
result.append('|').append(guess).append(',').append(BlockleGame.evaluate(answer.word(), guess));
if (guess.equals(answer.word())) solved = true;
}
if (solved || guesses.size() >= 6) result.append('|').append(solved ? "SOLVED" : "FAILED").append('|').append(answer.word()).append('|').append(answer.itemId());
else result.append('|').append("PLAYING");
return result.toString();
}
private static void send(ServerPlayer player, AntarchyGamePayload payload, boolean success, String data, String error) {
resultSender.accept(player, new AntarchyGameResultPayload(payload.pos(), success,
payload.action() + "\0" + error + "\0" + data));
}
}
@@ -0,0 +1,20 @@
package com.craisinlord.antarchy.content.network;
import net.minecraft.core.BlockPos;
import java.util.function.Consumer;
/** Small Antarchy game-progress bridge over the AntOS computer session. */
public final class AntarchyGameNetworking {
private static Consumer<AntarchyGamePayload> sender = payload -> { };
private AntarchyGameNetworking() { }
public static void setSender(Consumer<AntarchyGamePayload> sender) { AntarchyGameNetworking.sender = sender; }
public static void requestBasiliskState(BlockPos pos) { send(pos, AntarchyGamePayload.BASILISK_STATE, ""); }
public static void saveBasiliskScore(BlockPos pos, int score) { send(pos, AntarchyGamePayload.BASILISK_STATE, Integer.toString(Math.max(0, score))); }
public static void requestAntmanState(BlockPos pos) { send(pos, AntarchyGamePayload.ANTMAN_STATE, ""); }
public static void saveAntmanScore(BlockPos pos, int score) { send(pos, AntarchyGamePayload.ANTMAN_STATE, Integer.toString(Math.max(0, score))); }
public static void requestBlockleState(BlockPos pos) { send(pos, AntarchyGamePayload.BLOCKLE_STATE, ""); }
public static void submitBlockleGuess(BlockPos pos, String guess) { send(pos, AntarchyGamePayload.BLOCKLE_GUESS, guess); }
private static void send(BlockPos pos, int action, String value) { sender.accept(new AntarchyGamePayload(pos, action, value)); }
}
@@ -0,0 +1,32 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntarchyGamePayload(BlockPos pos, int action, String value) implements CustomPacketPayload {
public static final int BASILISK_STATE = 16;
public static final int ANTMAN_STATE = 17;
public static final int BLOCKLE_STATE = 18;
public static final int BLOCKLE_GUESS = 19;
public static final Type<AntarchyGamePayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antarchy_game"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntarchyGamePayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, AntarchyGamePayload::pos,
ByteBufCodecs.VAR_INT, AntarchyGamePayload::action,
ByteBufCodecs.stringUtf8(65536), AntarchyGamePayload::value,
AntarchyGamePayload::new
);
public AntarchyGamePayload(BlockPos pos, int action) {
this(pos, action, "");
}
@Override
public Type<AntarchyGamePayload> type() {
return TYPE;
}
}
@@ -0,0 +1,24 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntarchyGameResultPayload(BlockPos pos, boolean success, String data) implements CustomPacketPayload {
public static final Type<AntarchyGameResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antarchy_game_result"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntarchyGameResultPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, AntarchyGameResultPayload::pos,
ByteBufCodecs.BOOL, AntarchyGameResultPayload::success,
ByteBufCodecs.stringUtf8(65536), AntarchyGameResultPayload::data,
AntarchyGameResultPayload::new
);
@Override
public Type<AntarchyGameResultPayload> type() {
return TYPE;
}
}
@@ -1,15 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailDeletePayload(BlockPos pos, String messageId, boolean sent) implements CustomPacketPayload {
public static final Type<AntmailDeletePayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_delete"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailDeletePayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailDeletePayload::pos, ByteBufCodecs.stringUtf8(64), AntmailDeletePayload::messageId, ByteBufCodecs.BOOL, AntmailDeletePayload::sent, AntmailDeletePayload::new);
@Override public Type<AntmailDeletePayload> type() { return TYPE; }
}
@@ -1,17 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailDraftPayload(BlockPos pos, int action, String data) implements CustomPacketPayload {
public static final int SAVE = 0;
public static final int DELETE = 1;
public static final Type<AntmailDraftPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_draft"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailDraftPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailDraftPayload::pos, ByteBufCodecs.VAR_INT, AntmailDraftPayload::action, ByteBufCodecs.stringUtf8(65536), AntmailDraftPayload::data, AntmailDraftPayload::new);
@Override public Type<AntmailDraftPayload> type() { return TYPE; }
}
@@ -1,23 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailMessageRequestPayload(BlockPos pos, String messageId) implements CustomPacketPayload {
public static final Type<AntmailMessageRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_message_request"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailMessageRequestPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, AntmailMessageRequestPayload::pos,
ByteBufCodecs.stringUtf8(64), AntmailMessageRequestPayload::messageId,
AntmailMessageRequestPayload::new
);
@Override
public Type<AntmailMessageRequestPayload> type() {
return TYPE;
}
}
@@ -1,83 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.content.antmail.AntmailAttachment;
import com.craisinlord.antarchy.content.antmail.AntmailDebug;
import com.craisinlord.antarchy.content.antmail.AntmailDraft;
import com.craisinlord.antarchy.content.antmail.AntmailWire;
import net.minecraft.core.BlockPos;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public final class AntmailNetworking {
private static Consumer<Object> sender = payload -> { };
private static volatile boolean senderConfigured;
private static final AtomicBoolean senderErrorLogged = new AtomicBoolean();
private AntmailNetworking() {
}
public static void setSender(Consumer<Object> sender) {
AntmailNetworking.sender = sender;
senderConfigured = sender != null;
}
public static void setup(BlockPos pos, String username) {
AntmailDebug.log("C2S setup request pos=" + pos + " usernameLength=" + username.length());
sendPayload(new AntmailSetupPayload(pos, username));
}
public static void requestState(BlockPos pos) {
requestState(pos, AntmailStateRequestPayload.INBOX, 0, 0L);
}
public static void requestState(BlockPos pos, int folder, int page) {
requestState(pos, folder, page, 0L);
}
public static void requestState(BlockPos pos, int folder, int page, long knownVersion) {
AntmailDebug.log("C2S state request pos=" + pos + " folder=" + folder + " page=" + page + " knownVersion=" + knownVersion);
sendPayload(new AntmailStateRequestPayload(pos, folder, page, knownVersion));
}
public static void requestMessage(BlockPos pos, UUID messageId) {
sendPayload(new AntmailMessageRequestPayload(pos, messageId.toString()));
}
public static void send(BlockPos pos, String recipient, String subject, String body, List<AntmailAttachment> attachments) {
sendPayload(new AntmailSendPayload(pos, recipient, subject, body, AntmailWire.encodeAttachments(attachments)));
}
public static void markRead(BlockPos pos, UUID messageId) {
sendPayload(new AntmailReadPayload(pos, messageId.toString(), true));
}
public static void markUnread(BlockPos pos, UUID messageId) {
sendPayload(new AntmailReadPayload(pos, messageId.toString(), false));
}
public static void delete(BlockPos pos, UUID messageId, boolean sent) {
sendPayload(new AntmailDeletePayload(pos, messageId.toString(), sent));
}
public static void saveDraft(BlockPos pos, AntmailDraft draft) {
sendPayload(new AntmailDraftPayload(pos, AntmailDraftPayload.SAVE, AntmailWire.encodeTag(draft.toTag())));
}
public static void deleteDraft(BlockPos pos, UUID draftId) {
sendPayload(new AntmailDraftPayload(pos, AntmailDraftPayload.DELETE, draftId.toString()));
}
public static void retry(BlockPos pos, UUID messageId) {
sendPayload(new AntmailRetryPayload(pos, messageId.toString()));
}
private static void sendPayload(Object payload) {
if (!senderConfigured) {
if (senderErrorLogged.compareAndSet(false, true)) AntmailDebug.error("Network sender is not configured; dropping " + payload.getClass().getSimpleName(), new IllegalStateException("AntmailNetworking.setSender was not called"));
}
else sender.accept(payload);
}
}
@@ -1,24 +0,0 @@
package com.craisinlord.antarchy.content.network;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import java.util.List;
public final class AntmailPayloadTypes {
private AntmailPayloadTypes() {
}
public static List<CustomPacketPayload.Type<?>> all() {
return List.of(
AntmailSetupPayload.TYPE,
AntmailStateRequestPayload.TYPE,
AntmailMessageRequestPayload.TYPE,
AntmailSendPayload.TYPE,
AntmailReadPayload.TYPE,
AntmailDeletePayload.TYPE,
AntmailDraftPayload.TYPE,
AntmailRetryPayload.TYPE,
AntmailResultPayload.TYPE
);
}
}
@@ -1,15 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailReadPayload(BlockPos pos, String messageId, boolean read) implements CustomPacketPayload {
public static final Type<AntmailReadPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_read"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailReadPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailReadPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailReadPayload::messageId, ByteBufCodecs.BOOL, AntmailReadPayload::read, AntmailReadPayload::new);
@Override public Type<AntmailReadPayload> type() { return TYPE; }
}
@@ -1,33 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailResultPayload(BlockPos pos, int status, String address, String messageId, String detail, String data, long version) implements CustomPacketPayload {
public static final Type<AntmailResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_result"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailResultPayload> STREAM_CODEC = StreamCodec.of(
(buf, payload) -> {
BlockPos.STREAM_CODEC.encode(buf, payload.pos());
ByteBufCodecs.VAR_INT.encode(buf, payload.status());
ByteBufCodecs.stringUtf8(64).encode(buf, payload.address());
ByteBufCodecs.stringUtf8(64).encode(buf, payload.messageId());
ByteBufCodecs.stringUtf8(128).encode(buf, payload.detail());
ByteBufCodecs.stringUtf8(65536).encode(buf, payload.data());
ByteBufCodecs.VAR_LONG.encode(buf, payload.version());
},
buf -> new AntmailResultPayload(
BlockPos.STREAM_CODEC.decode(buf),
ByteBufCodecs.VAR_INT.decode(buf),
ByteBufCodecs.stringUtf8(64).decode(buf),
ByteBufCodecs.stringUtf8(64).decode(buf),
ByteBufCodecs.stringUtf8(128).decode(buf),
ByteBufCodecs.stringUtf8(65536).decode(buf),
ByteBufCodecs.VAR_LONG.decode(buf)
));
@Override public Type<AntmailResultPayload> type() { return TYPE; }
}
@@ -1,15 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailRetryPayload(BlockPos pos, String messageId) implements CustomPacketPayload {
public static final Type<AntmailRetryPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_retry"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailRetryPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailRetryPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailRetryPayload::messageId, AntmailRetryPayload::new);
@Override public Type<AntmailRetryPayload> type() { return TYPE; }
}
@@ -1,15 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailSendPayload(BlockPos pos, String recipient, String subject, String body, String attachments) implements CustomPacketPayload {
public static final Type<AntmailSendPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_send"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailSendPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSendPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailSendPayload::recipient, ByteBufCodecs.stringUtf8(64), AntmailSendPayload::subject, ByteBufCodecs.stringUtf8(16384), AntmailSendPayload::body, ByteBufCodecs.stringUtf8(65536), AntmailSendPayload::attachments, AntmailSendPayload::new);
@Override public Type<AntmailSendPayload> type() { return TYPE; }
}
@@ -1,266 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.content.antmail.AntmailAddress;
import com.craisinlord.antarchy.content.antmail.AntmailDebug;
import com.craisinlord.antarchy.content.antmail.AntmailAttachment;
import com.craisinlord.antarchy.content.antmail.AntmailDeliveryResult;
import com.craisinlord.antarchy.content.antmail.AntmailDraft;
import com.craisinlord.antarchy.content.antmail.AntmailMessage;
import com.craisinlord.antarchy.content.antmail.AntmailMailbox;
import com.craisinlord.antarchy.content.antmail.AntmailServerData;
import com.craisinlord.antarchy.content.antmail.AntmailValidation;
import com.craisinlord.antarchy.content.antmail.AntmailWire;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.entity.BlockEntity;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
public final class AntmailServerHandler {
private static BiConsumer<ServerPlayer, AntmailResultPayload> resultSender = (player, payload) -> { };
private static volatile boolean resultSenderConfigured;
private AntmailServerHandler() {
}
public static void setResultSender(BiConsumer<ServerPlayer, AntmailResultPayload> sender) {
resultSender = sender;
resultSenderConfigured = sender != null;
}
public static void handle(ServerPlayer player, AntmailSetupPayload payload) {
AntmailDebug.log("C2S setup received player=" + player.getGameProfile().getName() + " pos=" + payload.pos()
+ " usernameLength=" + payload.username().length());
ComputerBlockEntity computer = computer(player, payload.pos());
if (computer == null || !computer.canUseFileSystem(player)) {
AntmailDebug.log("setup rejected: computer missing, out of reach, unauthenticated, or owned by another user");
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "registration_failed:unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailServerData.Registration registration = data.register(player.server, player.serverLevel(), payload.pos(), payload.username());
if (registration.valid()) data.setLastUsedAddress(player, registration.address());
String mailbox = registration.valid() ? AntmailWire.encodeTag(data.mailboxOrCreate(registration.address()).toTag(false)) : "";
String detail = registration.valid() ? "registration_success" : "registration_failed:" + registration.reason();
result(player, payload.pos(), registration.valid() ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), registration.valid() ? registration.address().fullAddress() : "", "", detail, mailbox);
}
public static void handle(ServerPlayer player, AntmailStateRequestPayload payload) {
AntmailDebug.log("state request received player=" + player.getGameProfile().getName() + " pos=" + payload.pos()
+ " folder=" + payload.folder() + " page=" + payload.page() + " knownVersion=" + payload.knownVersion());
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
AntmailDebug.log("state request rejected: computer missing, out of reach, unauthenticated, or owned by another user");
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
data.drainAddress(player.server, player.serverLevel(), payload.pos());
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (address == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", "", "unconfigured", "");
return;
}
AntmailMailbox mailbox = data.mailboxOrCreate(address);
if (payload.knownVersion() == data.mailboxVersion(address)) {
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "unchanged", "");
return;
}
int folder = payload.folder() < AntmailStateRequestPayload.INBOX || payload.folder() > AntmailStateRequestPayload.DRAFTS
? AntmailStateRequestPayload.INBOX : payload.folder();
int page = Math.max(0, Math.min(payload.page(), AntmailValidation.MAX_MAILBOX_MESSAGES / AntmailMailbox.PAGE_SIZE));
try {
String encoded = AntmailWire.encodeTag(mailbox.toPageTag(folder, page));
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "", encoded);
} catch (RuntimeException exception) {
AntmailDebug.error("Failed to encode state for " + address.fullAddress() + " at " + payload.pos(), exception);
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), "", "state_encode_failed", "");
}
}
public static void handle(ServerPlayer player, AntmailMessageRequestPayload payload) {
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (address == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
return;
}
try {
UUID id = UUID.fromString(payload.messageId());
AntmailMailbox mailbox = data.mailboxOrCreate(address);
AntmailMessage message = mailbox.findInbox(id);
if (message == null) message = mailbox.findSent(id);
if (message == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "not_found", "");
return;
}
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), payload.messageId(), "message_detail", AntmailWire.encodeTag(message.toTag()));
} catch (IllegalArgumentException exception) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "invalid_id", "");
}
}
public static void handle(ServerPlayer player, AntmailSendPayload payload) {
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress sender = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (sender == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unconfigured", "");
return;
}
try {
AntmailAddress recipient = AntmailAddress.parse(payload.recipient());
List<AntmailAttachment> attachments = AntmailWire.decodeAttachments(payload.attachments());
AntmailMessage message = AntmailMessage.create(sender, recipient, payload.subject(), payload.body(), player.serverLevel().getGameTime(), attachments);
AntmailDeliveryResult delivery = data.deliver(player.server, message);
result(player, payload.pos(), delivery.status().ordinal(), sender.fullAddress(), message.id().toString(), delivery.detail(), AntmailWire.encodeTag(data.mailboxOrCreate(sender).toTag(false)));
} catch (RuntimeException exception) {
result(player, payload.pos(), AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal(), "", "", "invalid_message", "");
}
}
public static void handle(ServerPlayer player, AntmailReadPayload payload) {
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (address == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
return;
}
try {
UUID id = UUID.fromString(payload.messageId());
AntmailMailbox mailbox = data.mailboxOrCreate(address);
boolean changed = payload.read() ? mailbox.markRead(id) : markUnread(mailbox, id);
if (changed) data.touchMailbox(address);
result(player, payload.pos(), changed ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), changed ? "" : "not_found", AntmailWire.encodeTag(mailbox.toTag(false)));
} catch (IllegalArgumentException exception) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "invalid_id", "");
}
}
public static void handle(ServerPlayer player, AntmailDeletePayload payload) {
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (address == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
return;
}
try {
UUID id = UUID.fromString(payload.messageId());
AntmailMailbox mailbox = data.mailboxOrCreate(address);
boolean changed = payload.sent() ? mailbox.removeSent(id) : mailbox.removeInbox(id);
if (changed) data.touchMailbox(address);
result(player, payload.pos(), changed ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), changed ? "" : "not_found", AntmailWire.encodeTag(mailbox.toTag(false)));
} catch (IllegalArgumentException exception) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "invalid_id", "");
}
}
public static void handle(ServerPlayer player, AntmailDraftPayload payload) {
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (address == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", "", "unconfigured", "");
return;
}
AntmailMailbox mailbox = data.mailboxOrCreate(address);
try {
if (payload.action() == AntmailDraftPayload.SAVE) {
AntmailDraft draft = AntmailDraft.fromTag(AntmailWire.decodeTag(payload.data()));
AntmailDraft stored = new AntmailDraft(draft.id(), address, draft.recipient(), draft.subject(), draft.body(), draft.attachments());
if (!mailbox.saveDraft(stored)) throw new IllegalArgumentException("draft_limit");
} else if (payload.action() == AntmailDraftPayload.DELETE) {
if (!mailbox.removeDraft(UUID.fromString(payload.data()))) throw new IllegalArgumentException("not_found");
} else {
throw new IllegalArgumentException("invalid_action");
}
data.touchMailbox(address);
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "draft_saved", AntmailWire.encodeTag(mailbox.toTag(false)));
} catch (RuntimeException exception) {
result(player, payload.pos(), AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal(), address.fullAddress(), "", "draft_failed", AntmailWire.encodeTag(mailbox.toTag(false)));
}
}
public static void handle(ServerPlayer player, AntmailRetryPayload payload) {
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
if (computer == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
return;
}
AntmailServerData data = AntmailServerData.access(player.server);
AntmailAddress sender = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
if (sender == null) {
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
return;
}
try {
AntmailDeliveryResult delivery = data.retry(player.server, sender, UUID.fromString(payload.messageId()));
AntmailMailbox mailbox = data.mailboxOrCreate(sender);
result(player, payload.pos(), delivery == null ? AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal() : delivery.status().ordinal(), sender.fullAddress(), payload.messageId(), delivery == null ? "not_found" : delivery.detail(), AntmailWire.encodeTag(mailbox.toTag(false)));
} catch (IllegalArgumentException exception) {
result(player, payload.pos(), AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal(), sender.fullAddress(), payload.messageId(), "invalid_id", "");
}
}
private static ComputerBlockEntity authorizedComputer(ServerPlayer player, BlockPos pos) {
ComputerBlockEntity computer = computer(player, pos);
return computer != null && computer.canUseFileSystem(player) ? computer : null;
}
private static boolean markUnread(AntmailMailbox mailbox, UUID id) {
for (com.craisinlord.antarchy.content.antmail.AntmailMessage message : mailbox.inbox()) {
if (message.id().equals(id)) {
message.markUnread();
return true;
}
}
return false;
}
private static ComputerBlockEntity computer(ServerPlayer player, BlockPos pos) {
if (!player.serverLevel().hasChunkAt(pos) || player.distanceToSqr(pos.getX() + .5D, pos.getY() + .5D, pos.getZ() + .5D) > 64D) return null;
BlockEntity blockEntity = player.serverLevel().getBlockEntity(pos);
return blockEntity instanceof ComputerBlockEntity computer ? computer : null;
}
private static void result(ServerPlayer player, BlockPos pos, int status, String address, String messageId, String detail, String data) {
long version = 0L;
try {
if (!address.isBlank()) version = AntmailServerData.access(player.server).mailboxVersion(AntmailAddress.parse(address));
} catch (IllegalArgumentException ignored) {
}
AntmailDebug.log("sending result player=" + player.getGameProfile().getName() + " pos=" + pos + " status=" + status
+ " detail=" + detail + " addressPresent=" + !address.isBlank() + " dataChars=" + data.length() + " version=" + version);
if (!resultSenderConfigured) {
AntmailDebug.error("Result sender is not configured; dropping response at " + pos, new IllegalStateException("AntmailServerHandler.setResultSender was not called"));
return;
}
resultSender.accept(player, new AntmailResultPayload(pos, status, address, messageId, detail, data, version));
}
}
@@ -1,15 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailSetupPayload(BlockPos pos, String username) implements CustomPacketPayload {
public static final Type<AntmailSetupPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_setup"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailSetupPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSetupPayload::pos, ByteBufCodecs.stringUtf8(32), AntmailSetupPayload::username, AntmailSetupPayload::new);
@Override public Type<AntmailSetupPayload> type() { return TYPE; }
}
@@ -1,23 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AntmailStateRequestPayload(BlockPos pos, int folder, int page, long knownVersion) implements CustomPacketPayload {
public static final int INBOX = 0;
public static final int SENT = 1;
public static final int DRAFTS = 2;
public static final Type<AntmailStateRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_state_request"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailStateRequestPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, AntmailStateRequestPayload::pos,
ByteBufCodecs.VAR_INT, AntmailStateRequestPayload::folder,
ByteBufCodecs.VAR_INT, AntmailStateRequestPayload::page,
ByteBufCodecs.VAR_LONG, AntmailStateRequestPayload::knownVersion,
AntmailStateRequestPayload::new);
@Override public Type<AntmailStateRequestPayload> type() { return TYPE; }
}
@@ -1,455 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
import com.craisinlord.antarchy.content.computer.terminal.TerminalCommandService;
import com.craisinlord.antarchy.content.computer.terminal.TerminalFileSystem;
import com.craisinlord.antarchy.content.computer.terminal.TerminalResult;
import com.craisinlord.antarchy.config.AntarchySettings;
import com.craisinlord.antarchy.content.antmail.AntmailWire;
import com.craisinlord.antarchy.content.computer.blockle.BlockleAnswers;
import com.craisinlord.antarchy.content.computer.blockle.BlockleDictionary;
import com.craisinlord.antarchy.content.computer.blockle.BlockleGame;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.core.registries.Registries;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.block.entity.BlockEntity;
import java.util.function.BiConsumer;
public final class ComputerAccessHandler {
private static BiConsumer<ServerPlayer, ComputerAccessResultPayload> resultSender = (player, result) -> {
};
private ComputerAccessHandler() {
}
public static void setResultSender(BiConsumer<ServerPlayer, ComputerAccessResultPayload> sender) {
resultSender = sender;
}
public static void handle(ServerPlayer player, ComputerAccessPayload payload) {
if (payload.value().length() > 65536 || player.level().isClientSide || !player.serverLevel().hasChunkAt(payload.pos()) ||
player.distanceToSqr(payload.pos().getX() + 0.5D, payload.pos().getY() + 0.5D, payload.pos().getZ() + 0.5D) > 64.0D) {
return;
}
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
if (!(blockEntity instanceof ComputerBlockEntity computer)) {
send(player, payload, ComputerAccessResultPayload.INVALID);
return;
}
switch (payload.action()) {
case ComputerAccessPayload.OPEN -> open(player, computer, payload);
case ComputerAccessPayload.SETUP -> setup(player, computer, payload);
case ComputerAccessPayload.LOGIN -> login(player, computer, payload);
case ComputerAccessPayload.LOGOUT -> {
computer.logout();
send(player, payload, ComputerAccessResultPayload.READY);
}
case ComputerAccessPayload.CLOSE -> {
computer.releaseUser(player);
send(player, payload, computer.isAuthenticated() ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.READY);
}
case ComputerAccessPayload.CHANGE_PASSWORD -> {
boolean changed = computer.setPassword(player, payload.value());
send(player, payload, changed ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID_PASSWORD);
}
case ComputerAccessPayload.EJECT -> eject(player, computer, payload);
case ComputerAccessPayload.FILE_LIST -> fileList(player, computer, payload);
case ComputerAccessPayload.FILE_OPEN -> fileOpen(player, computer, payload);
case ComputerAccessPayload.FILE_CREATE -> fileCreate(player, computer, payload);
case ComputerAccessPayload.FILE_SAVE -> fileSave(player, computer, payload);
case ComputerAccessPayload.FILE_DELETE -> fileDelete(player, computer, payload);
case ComputerAccessPayload.FILE_MOVE -> fileMove(player, computer, payload);
case ComputerAccessPayload.TERMINAL_COMMAND -> terminalCommand(player, computer, payload);
case ComputerAccessPayload.DESKTOP_STATE -> desktopState(player, computer, payload);
case ComputerAccessPayload.DESKTOP_WALLPAPER -> desktopWallpaper(player, computer, payload);
case ComputerAccessPayload.BASILISK_STATE -> basiliskState(player, computer, payload);
case ComputerAccessPayload.ANTMAN_STATE -> antmanState(player, computer, payload);
case ComputerAccessPayload.BLOCKLE_STATE -> blockleState(player, computer, payload, false);
case ComputerAccessPayload.BLOCKLE_GUESS -> blockleState(player, computer, payload, true);
case ComputerAccessPayload.LOCATE_STRUCTURE -> locateStructure(player, computer, payload);
default -> send(player, payload, ComputerAccessResultPayload.INVALID);
}
}
private static void open(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (computer.hasActiveUser() && !computer.isAuthenticatedBy(player)) {
send(player, payload, ComputerAccessResultPayload.BUSY);
sendArchive(player, computer, payload);
return;
}
if (!computer.hasPassword()) {
computer.claimUser(player);
send(player, payload, ComputerAccessResultPayload.SETUP_REQUIRED);
} else if (computer.isAuthenticated()) {
computer.claimUser(player);
send(player, payload, ComputerAccessResultPayload.SUCCESS);
} else {
send(player, payload, ComputerAccessResultPayload.READY);
}
sendArchive(player, computer, payload);
}
private static void sendArchive(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), ComputerAccessResultPayload.SUCCESS,
computer.hasPassword(), computer.isAuthenticated(), ComputerAccessPayload.ARCHIVE_STATE + "\0\0"
+ com.craisinlord.antarchy.content.guide.ComputerGuideData.encodeNetworkSnapshot()));
}
private static void setup(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.hasPassword() && computer.initializePassword(player, payload.value())) {
send(player, payload, ComputerAccessResultPayload.SUCCESS);
} else {
send(player, payload, computer.hasActiveUser() ? ComputerAccessResultPayload.BUSY : ComputerAccessResultPayload.INVALID_PASSWORD);
}
}
private static void login(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (computer.hasActiveUser() && !computer.isAuthenticatedBy(player)) {
send(player, payload, ComputerAccessResultPayload.BUSY);
} else if (computer.authenticate(player, payload.value())) {
com.craisinlord.antarchy.content.antmail.AntmailServerData data = com.craisinlord.antarchy.content.antmail.AntmailServerData.access(player.server);
net.minecraft.resources.ResourceLocation dimension = player.serverLevel().dimension().location();
com.craisinlord.antarchy.content.antmail.AntmailAddress address = data.addressAt(dimension, computer.getBlockPos());
if (address != null) data.setLastUsedAddress(player, address);
send(player, payload, ComputerAccessResultPayload.SUCCESS);
} else {
send(player, payload, ComputerAccessResultPayload.INVALID_PASSWORD);
}
}
private static void eject(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
try {
ResourceLocation diskId = ResourceLocation.parse(payload.value());
boolean ejected = computer.ejectOne(player, diskId);
send(player, payload, ejected ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID);
} catch (Exception ignored) {
send(player, payload, ComputerAccessResultPayload.INVALID);
}
}
private static void fileList(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
StringBuilder data = new StringBuilder();
for (ComputerFileSystem.ComputerFile file : computer.fileSystem().list()) {
if (!data.isEmpty()) data.append('\n');
data.append(file.type().name()).append('\t').append(file.path()).append('\t').append(file.contents().length());
}
sendFile(player, payload, true, data.toString(), "");
}
private static void fileOpen(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
ComputerFileSystem.ComputerFile file = computer.fileSystem().get(payload.value());
if (file == null) {
sendFile(player, payload, false, "", "not_found");
} else if (file.type() != ComputerFileSystem.ComputerFile.Type.TEXT && file.type() != ComputerFileSystem.ComputerFile.Type.IMAGE) {
sendFile(player, payload, false, "", "not_file");
} else {
sendFile(player, payload, true, file.path() + "\0" + file.contents(), "");
}
}
private static void fileCreate(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
String[] request = splitFileRequest(payload.value());
if (request == null) {
sendFile(player, payload, false, "", "invalid_request");
return;
}
ComputerFileSystem.Result result = computer.fileSystem().createTextFile(request[0], request[1]);
if (result.successful()) computer.setChanged();
sendFile(player, payload, result.successful(), request[0], result.error());
}
private static void fileSave(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
String[] request = splitFileRequest(payload.value());
if (request == null) {
sendFile(player, payload, false, "", "invalid_request");
return;
}
ComputerFileSystem.Result result = computer.fileSystem().writeTextFile(request[0], request[1]);
if (result.successful()) computer.setChanged();
sendFile(player, payload, result.successful(), request[0], result.error());
}
private static void fileDelete(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
ComputerFileSystem.Result result = computer.fileSystem().delete(payload.value());
if (result.successful()) computer.setChanged();
sendFile(player, payload, result.successful(), payload.value(), result.error());
}
private static void fileMove(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
String[] request = splitFileRequest(payload.value());
if (request == null) {
sendFile(player, payload, false, "", "invalid_request");
return;
}
ComputerFileSystem.Result result = computer.fileSystem().move(request[0], request[1]);
if (result.successful()) computer.setChanged();
sendFile(player, payload, result.successful(), request[1], result.error());
}
private static void terminalCommand(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
String[] request = splitFileRequest(payload.value());
if (request == null) {
sendTerminal(player, payload, TerminalResult.error("/", "ERROR: INVALID REQUEST"));
return;
}
TerminalResult result = new TerminalCommandService().execute(new ComputerTerminalFileSystem(computer.fileSystem()), request[0], request[1]);
if (result.status() != TerminalResult.Status.SUCCESS || !result.lines().isEmpty() || result.clearOutput() || result.openPath() != null) computer.setChanged();
sendTerminal(player, payload, result);
}
private static void sendTerminal(ServerPlayer player, ComputerAccessPayload payload, TerminalResult result) {
net.minecraft.nbt.CompoundTag tag = new net.minecraft.nbt.CompoundTag();
tag.putString("Directory", result.workingDirectory());
tag.putBoolean("Clear", result.clearOutput());
tag.putString("Open", result.openPath() == null ? "" : result.openPath());
net.minecraft.nbt.ListTag lines = new net.minecraft.nbt.ListTag();
for (String line : result.lines()) lines.add(net.minecraft.nbt.StringTag.valueOf(line));
tag.put("Lines", lines);
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), result.status() == TerminalResult.Status.ERROR ? ComputerAccessResultPayload.INVALID : ComputerAccessResultPayload.SUCCESS,
true, true, payload.action() + "\0" + AntmailWire.encodeTag(tag)));
}
private static final class ComputerTerminalFileSystem implements TerminalFileSystem {
private final ComputerFileSystem fileSystem;
private ComputerTerminalFileSystem(ComputerFileSystem fileSystem) { this.fileSystem = fileSystem; }
@Override public java.util.Optional<Entry> find(String path, String workingDirectory) {
ComputerFileSystem.ComputerFile file = fileSystem.get(resolve(path, workingDirectory));
return file == null ? java.util.Optional.empty() : java.util.Optional.of(new Entry(file.path(), file.path(), file.type() == ComputerFileSystem.ComputerFile.Type.DIRECTORY ? EntryType.DIRECTORY : EntryType.FILE));
}
@Override public java.util.List<Entry> list(String path, String workingDirectory) {
String directory = resolve(path, workingDirectory);
return fileSystem.list().stream().filter(file -> {
String parent = file.path().lastIndexOf('/') <= 0 ? "/" : file.path().substring(0, file.path().lastIndexOf('/'));
return parent.equals(directory) && !file.path().equals(directory);
}).map(file -> new Entry(file.path().substring(file.path().lastIndexOf('/') + 1), file.path(), file.type() == ComputerFileSystem.ComputerFile.Type.DIRECTORY ? EntryType.DIRECTORY : EntryType.FILE)).toList();
}
@Override public boolean createDirectory(String path, String workingDirectory) { return fileSystem.createDirectory(resolve(path, workingDirectory)).successful(); }
@Override public boolean createFile(String path, String workingDirectory) { return fileSystem.createTextFile(resolve(path, workingDirectory), "").successful(); }
@Override public java.util.Optional<String> readText(String path, String workingDirectory) { ComputerFileSystem.ComputerFile file = fileSystem.get(resolve(path, workingDirectory)); return file != null && file.type() == ComputerFileSystem.ComputerFile.Type.TEXT ? java.util.Optional.of(file.contents()) : java.util.Optional.empty(); }
@Override public boolean writeText(String path, String workingDirectory, String contents) { return fileSystem.createOrWriteTextFile(resolve(path, workingDirectory), contents).successful(); }
@Override public boolean deleteFile(String path, String workingDirectory) { return fileSystem.delete(resolve(path, workingDirectory)).successful(); }
@Override public boolean deleteEmptyDirectory(String path, String workingDirectory) { return fileSystem.delete(resolve(path, workingDirectory)).successful(); }
@Override public boolean move(String source, String destination, String workingDirectory) { return fileSystem.move(resolve(source, workingDirectory), resolve(destination, workingDirectory)).successful(); }
@Override public String normalize(String path, String workingDirectory) { return resolve(path, workingDirectory); }
private String resolve(String path, String workingDirectory) {
if (path == null || path.isBlank()) return ComputerFileSystem.normalize(workingDirectory);
return ComputerFileSystem.normalize(path.startsWith("/") ? path : ComputerFileSystem.normalize(workingDirectory) + "/" + path);
}
}
private static void desktopState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendDesktop(player, payload, false, "unauthorized");
return;
}
sendDesktop(player, payload, true, AntmailWire.encodeTag(computer.desktopState().save()));
}
private static void desktopWallpaper(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendDesktop(player, payload, false, "unauthorized");
return;
}
try {
boolean changed = computer.selectWallpaper(ResourceLocation.parse(payload.value()));
sendDesktop(player, payload, changed, changed ? "" : "wallpaper_locked");
} catch (RuntimeException exception) {
sendDesktop(player, payload, false, "invalid_wallpaper");
}
}
private static void basiliskState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
if (!payload.value().isBlank()) {
try {
computer.setBasiliskScore(Integer.parseInt(payload.value()));
} catch (NumberFormatException ignored) {
sendFile(player, payload, false, "", "invalid_score");
return;
}
}
sendFile(player, payload, true, computer.basiliskScore() + "\0" + computer.basiliskHighScore(), "");
}
private static void antmanState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
if (!payload.value().isBlank()) {
try {
computer.setAntmanScore(Integer.parseInt(payload.value()));
} catch (NumberFormatException ignored) {
sendFile(player, payload, false, "", "invalid_score");
return;
}
}
sendFile(player, payload, true, computer.antmanScore() + "\0" + computer.antmanHighScore(), "");
}
private static void blockleState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload, boolean submit) {
if (!computer.canUseFileSystem(player)) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
ResourceLocation disk = ResourceLocation.fromNamespaceAndPath("antarchy", "blockle_game");
if (!AntarchySettings.unlockAllArchives() && !computer.diskIds().contains(disk)) {
sendFile(player, payload, false, "", "not_installed");
return;
}
ServerLevel overworld = player.server.getLevel(Level.OVERWORLD);
if (overworld == null) {
sendFile(player, payload, false, "", "overworld_unavailable");
return;
}
long day = overworld.getDayTime() / 24000L;
computer.resetBlockle(day);
BlockleAnswers.Answer answer = BlockleAnswers.answerForDay(day);
if (answer == null) {
sendFile(player, payload, false, "", "answers_unavailable");
return;
}
if (submit) {
String guess = payload.value() == null ? "" : payload.value().toLowerCase(java.util.Locale.ROOT);
if (guess.length() != 5 || !guess.chars().allMatch(value -> value >= 'a' && value <= 'z')) {
sendFile(player, payload, false, "", "invalid_word");
return;
}
if (!BlockleDictionary.contains(guess)) {
sendFile(player, payload, false, "", "invalid_word");
return;
}
if (computer.blockleGuesses().size() >= 6 || computer.blockleGuesses().stream().anyMatch(value -> value.equals(answer.word()))) {
sendFile(player, payload, false, "", "game_over");
return;
}
computer.addBlockleGuess(guess);
}
sendFile(player, payload, true, encodeBlockle(day, answer, computer.blockleGuesses()), "");
}
private static void locateStructure(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
ResourceLocation entryId;
try {
entryId = ResourceLocation.parse(payload.value());
} catch (RuntimeException exception) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
var entry = com.craisinlord.antarchy.content.guide.ComputerGuideData.entry(entryId);
boolean unlocked = com.craisinlord.antarchy.content.guide.ComputerGuideData.entriesFor(computer.diskIds()).stream()
.anyMatch(candidate -> candidate.id().equals(entryId));
if (!computer.canUseFileSystem(player) || !unlocked || entry == null || entry.structureId().isBlank()
|| entry.structureTagId().isBlank() || entry.dimensionId().isBlank() || entry.searchRadius() <= 0) {
sendFile(player, payload, false, "", "unauthorized");
return;
}
ResourceLocation dimensionId;
ResourceLocation structureTagId;
try {
dimensionId = ResourceLocation.parse(entry.dimensionId());
structureTagId = ResourceLocation.parse(entry.structureTagId());
} catch (RuntimeException exception) {
sendFile(player, payload, false, "", "dimension_unavailable");
return;
}
ServerLevel targetLevel = player.server.getLevel(net.minecraft.resources.ResourceKey.create(Registries.DIMENSION, dimensionId));
if (targetLevel == null) {
sendFile(player, payload, false, "", "dimension_unavailable");
return;
}
TagKey<Structure> structures = TagKey.create(Registries.STRUCTURE, structureTagId);
net.minecraft.core.BlockPos origin = computer.getBlockPos();
var nearest = targetLevel.findNearestMapStructure(structures, origin, entry.searchRadius(), false);
if (nearest == null) {
sendFile(player, payload, false, "", "not_found");
return;
}
sendFile(player, payload, true, nearest.getX() + ", " + nearest.getY() + ", " + nearest.getZ(), "");
}
private static String encodeBlockle(long day, BlockleAnswers.Answer answer, java.util.List<String> guesses) {
StringBuilder result = new StringBuilder(Long.toString(day));
boolean solved = false;
for (String guess : guesses) {
String colors = BlockleGame.evaluate(answer.word(), guess);
if (guess.equals(answer.word())) solved = true;
result.append('|').append(guess).append(',').append(colors);
}
if (solved || guesses.size() >= 6) {
result.append('|').append(solved ? "SOLVED" : "FAILED").append('|').append(answer.word()).append('|').append(answer.itemId());
} else {
result.append('|').append("PLAYING");
}
return result.toString();
}
private static void sendDesktop(ServerPlayer player, ComputerAccessPayload payload, boolean success, String data) {
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
boolean hasPassword = blockEntity instanceof ComputerBlockEntity computer && computer.hasPassword();
boolean authenticated = blockEntity instanceof ComputerBlockEntity computer && computer.isAuthenticated();
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), success ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID,
hasPassword, authenticated, payload.action() + "\0\0" + data));
}
private static String[] splitFileRequest(String value) {
if (value == null) return null;
int separator = value.indexOf('\0');
if (separator < 0) return null;
return new String[]{value.substring(0, separator), value.substring(separator + 1)};
}
private static void send(ServerPlayer player, ComputerAccessPayload payload, int result) {
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
boolean hasPassword = blockEntity instanceof ComputerBlockEntity computer && computer.hasPassword();
boolean authenticated = blockEntity instanceof ComputerBlockEntity computer && computer.isAuthenticated();
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), result, hasPassword, authenticated));
}
private static void sendFile(ServerPlayer player, ComputerAccessPayload payload, boolean success, String data, String error) {
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
boolean hasPassword = blockEntity instanceof ComputerBlockEntity computer && computer.hasPassword();
boolean authenticated = blockEntity instanceof ComputerBlockEntity computer && computer.isAuthenticated();
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), success ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID,
hasPassword, authenticated, payload.action() + "\0" + error + "\0" + data));
}
}
@@ -1,50 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record ComputerAccessPayload(BlockPos pos, int action, String value) implements CustomPacketPayload {
public static final int OPEN = 0;
public static final int SETUP = 1;
public static final int LOGIN = 2;
public static final int LOGOUT = 3;
public static final int CHANGE_PASSWORD = 4;
public static final int EJECT = 5;
public static final int CLOSE = 6;
public static final int FILE_LIST = 7;
public static final int FILE_OPEN = 8;
public static final int FILE_CREATE = 9;
public static final int FILE_SAVE = 10;
public static final int DESKTOP_STATE = 11;
public static final int DESKTOP_WALLPAPER = 12;
public static final int FILE_DELETE = 13;
public static final int FILE_MOVE = 14;
public static final int TERMINAL_COMMAND = 15;
public static final int BASILISK_STATE = 16;
public static final int ANTMAN_STATE = 17;
public static final int BLOCKLE_STATE = 18;
public static final int BLOCKLE_GUESS = 19;
public static final int ARCHIVE_STATE = 20;
public static final int LOCATE_STRUCTURE = 21;
public static final Type<ComputerAccessPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access"));
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerAccessPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, ComputerAccessPayload::pos,
ByteBufCodecs.VAR_INT, ComputerAccessPayload::action,
ByteBufCodecs.stringUtf8(65536), ComputerAccessPayload::value,
ComputerAccessPayload::new
);
public ComputerAccessPayload(BlockPos pos, int action) {
this(pos, action, "");
}
@Override
public Type<ComputerAccessPayload> type() {
return TYPE;
}
}
@@ -1,36 +0,0 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record ComputerAccessResultPayload(BlockPos pos, int result, boolean hasPassword, boolean authenticated, String data) implements CustomPacketPayload {
public static final int SETUP_REQUIRED = 0;
public static final int READY = 1;
public static final int SUCCESS = 2;
public static final int INVALID_PASSWORD = 3;
public static final int BUSY = 4;
public static final int INVALID = 5;
public static final Type<ComputerAccessResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access_result"));
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerAccessResultPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, ComputerAccessResultPayload::pos,
ByteBufCodecs.VAR_INT, ComputerAccessResultPayload::result,
ByteBufCodecs.BOOL, ComputerAccessResultPayload::hasPassword,
ByteBufCodecs.BOOL, ComputerAccessResultPayload::authenticated,
ByteBufCodecs.stringUtf8(65536), ComputerAccessResultPayload::data,
ComputerAccessResultPayload::new
);
public ComputerAccessResultPayload(BlockPos pos, int result, boolean hasPassword, boolean authenticated) {
this(pos, result, hasPassword, authenticated, "");
}
@Override
public Type<ComputerAccessResultPayload> type() {
return TYPE;
}
}
@@ -1,110 +0,0 @@
package com.craisinlord.antarchy.content.network;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import java.util.function.Consumer;
public final class ComputerNetworking {
private static Consumer<ComputerAccessPayload> sender = payload -> {
};
private ComputerNetworking() {
}
public static void setSender(Consumer<ComputerAccessPayload> sender) {
ComputerNetworking.sender = sender;
}
public static void open(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.OPEN));
}
public static void setup(BlockPos pos, String password) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.SETUP, password));
}
public static void login(BlockPos pos, String password) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.LOGIN, password));
}
public static void logout(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.LOGOUT));
}
public static void close(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.CLOSE));
}
public static void changePassword(BlockPos pos, String password) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.CHANGE_PASSWORD, password));
}
public static void eject(BlockPos pos, ResourceLocation diskId) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.EJECT, diskId.toString()));
}
public static void listFiles(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_LIST));
}
public static void openFile(BlockPos pos, String path) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_OPEN, path));
}
public static void createFile(BlockPos pos, String path, String contents) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_CREATE, path + "\0" + contents));
}
public static void saveFile(BlockPos pos, String path, String contents) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_SAVE, path + "\0" + contents));
}
public static void deleteFile(BlockPos pos, String path) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_DELETE, path));
}
public static void moveFile(BlockPos pos, String source, String destination) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_MOVE, source + "\0" + destination));
}
public static void terminalCommand(BlockPos pos, String directory, String command) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.TERMINAL_COMMAND, directory + "\0" + command));
}
public static void requestDesktopState(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.DESKTOP_STATE));
}
public static void selectWallpaper(BlockPos pos, ResourceLocation id) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.DESKTOP_WALLPAPER, id.toString()));
}
public static void requestBasiliskState(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.BASILISK_STATE));
}
public static void saveBasiliskScore(BlockPos pos, int score) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.BASILISK_STATE, Integer.toString(Math.max(0, score))));
}
public static void requestAntmanState(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.ANTMAN_STATE));
}
public static void saveAntmanScore(BlockPos pos, int score) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.ANTMAN_STATE, Integer.toString(Math.max(0, score))));
}
public static void requestBlockleState(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.BLOCKLE_STATE));
}
public static void submitBlockleGuess(BlockPos pos, String guess) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.BLOCKLE_GUESS, guess));
}
public static void locateStructure(BlockPos pos, ResourceLocation entryId) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.LOCATE_STRUCTURE, entryId.toString()));
}
}
@@ -0,0 +1,19 @@
package com.craisinlord.antarchy.content.network;
import com.craisinlord.antarchy.Antarchy;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record RoyalGuardianSwordModeCyclePayload() implements CustomPacketPayload {
public static final Type<RoyalGuardianSwordModeCyclePayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "royal_guardian_sword_mode_cycle"));
public static final StreamCodec<ByteBuf, RoyalGuardianSwordModeCyclePayload> STREAM_CODEC =
StreamCodec.unit(new RoyalGuardianSwordModeCyclePayload());
@Override
public Type<RoyalGuardianSwordModeCyclePayload> type() {
return TYPE;
}
}
@@ -36,6 +36,7 @@ public class TimeDilationFieldEntity extends Entity {
private int missingAnchorTicks;
private boolean visual = true;
private boolean chronosphere;
private boolean queenChronosphere;
public TimeDilationFieldEntity(EntityType<? extends TimeDilationFieldEntity> entityType, Level level) {
super(entityType, level);
@@ -79,19 +80,25 @@ public class TimeDilationFieldEntity extends Entity {
this.chronosphere = true;
}
public void configureQueenChronosphere(QueenEntity owner) {
this.ownerId = owner.getUUID();
this.attachTo(owner);
this.queenChronosphere = true;
}
public boolean isChronosphere() {
return this.chronosphere;
}
public boolean affects(Entity entity) {
if (entity instanceof RoyalBlackHoleEntity || this.isOwnedBy(entity)
if (!this.queenChronosphere && entity instanceof RoyalBlackHoleEntity || this.isOwnedBy(entity)
|| this.anchorId != null && this.anchorId.equals(entity.getUUID())) {
return false;
}
if (this.chronosphere && !(entity instanceof LivingEntity || entity instanceof Projectile || entity instanceof ItemEntity)) {
return false;
}
if (this.ownerId != null && this.level() instanceof ServerLevel level
if (!this.queenChronosphere && this.ownerId != null && this.level() instanceof ServerLevel level
&& level.getEntity(this.ownerId) instanceof QueenEntity) {
return !entity.getType().is(AntarchyTags.Entities.QUEEN_DOES_NOT_ATTACK);
}
@@ -202,6 +209,7 @@ public class TimeDilationFieldEntity extends Entity {
this.anchorId = tag.hasUUID("AnchorUuid") ? tag.getUUID("AnchorUuid") : null;
this.visual = !tag.contains("Visual") || tag.getBoolean("Visual");
this.chronosphere = tag.getBoolean("Chronosphere");
this.queenChronosphere = tag.getBoolean("QueenChronosphere");
}
@Override
@@ -218,5 +226,6 @@ public class TimeDilationFieldEntity extends Entity {
}
tag.putBoolean("Visual", this.visual);
tag.putBoolean("Chronosphere", this.chronosphere);
tag.putBoolean("QueenChronosphere", this.queenChronosphere);
}
}
@@ -1,7 +1,6 @@
package com.craisinlord.antarchy.content.worldgen;
import com.craisinlord.antarchy.Antarchy;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antarchy.mixins.PoiTypesAccessor;
import com.craisinlord.integrated_api.mixins.structures.StructurePoolAccessor;
import com.google.gson.Gson;
@@ -59,7 +58,9 @@ public final class VillagePoolAdditions {
ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, poiId));
Map<BlockState, Holder<net.minecraft.world.entity.ai.village.poi.PoiType>> stateMap =
PoiTypesAccessor.antarchy$getTypeByState();
for (BlockState state : AntarchyObjects.COMPUTER.get().getStateDefinition().getPossibleStates()) {
net.minecraft.world.level.block.Block computer = BuiltInRegistries.BLOCK.get(
ResourceLocation.fromNamespaceAndPath("antos", "computer"));
for (BlockState state : computer.getStateDefinition().getPossibleStates()) {
stateMap.put(state, poi);
}
}
@@ -144,9 +144,8 @@ public final class KingsTreeGridFeature extends Feature<NoneFeatureConfiguration
}
double minimumY = baseY + 24.0D;
double maximumY = baseY + 304.0D;
// Start near the lower trunk so players on the ground can reliably engage the King.
// The patrol bounds remain unchanged, allowing him to retreat upward through the tree.
double spawnY = minimumY + 48.0D;
// Introduce the King high in the canopy, where his tree patrol is most visible.
double spawnY = minimumY + (maximumY - minimumY) * 0.65D;
Vec3 spawn = findSafeKingSpawn(serverLevel, king, centerX, centerZ, spawnY);
king.moveTo(spawn.x, spawn.y, spawn.z,
(float) (Math.atan2(centerZ + 0.5D - spawn.z, centerX + 0.5D - spawn.x)
@@ -1,22 +0,0 @@
package com.craisinlord.antarchy.mixins;
import com.craisinlord.antarchy.content.antmail.AntmailEventData;
import net.minecraft.advancements.AdvancementHolder;
import net.minecraft.server.PlayerAdvancements;
import net.minecraft.server.level.ServerPlayer;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerAdvancements.class)
public abstract class PlayerAdvancementsMixin {
@Shadow @Final private ServerPlayer player;
@Inject(method = "award", at = @At("RETURN"))
private void antarchy$onAward(AdvancementHolder advancement, String criterion, CallbackInfoReturnable<Boolean> callback) {
if (callback.getReturnValueZ() && player.getAdvancements().getOrStartProgress(advancement).isDone()) AntmailEventData.onAdvancement(player, advancement);
}
}
@@ -1,6 +1,5 @@
package com.craisinlord.antarchy.mixins;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antarchy.Antarchy;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
@@ -23,12 +22,14 @@ public abstract class PoiTypesMixin {
private static void antarchy$computerPoi(BlockState state,
CallbackInfoReturnable<Optional<Holder<PoiType>>> callback) {
try {
if (!state.is(AntarchyObjects.COMPUTER.get())) {
boolean computer = state.is(BuiltInRegistries.BLOCK.get(ResourceLocation.fromNamespaceAndPath("antos", "computer")));
boolean antTrap = state.is(BuiltInRegistries.BLOCK.get(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "ant_trap")));
if (!computer && !antTrap) {
return;
}
ResourceKey<PoiType> key = ResourceKey.create(
Registries.POINT_OF_INTEREST_TYPE,
ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer"));
ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, computer ? "computer" : "pest_control"));
BuiltInRegistries.POINT_OF_INTEREST_TYPE.getHolder(key)
.ifPresent(holder -> callback.setReturnValue(Optional.of(holder)));
} catch (IllegalStateException ignored) {
@@ -6,18 +6,23 @@ import net.minecraft.client.model.EntityModel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LivingEntityRenderer.class)
public abstract class TemporalContractionLivingRendererMixin<T extends LivingEntity, M extends EntityModel<T>> {
@Invoker("addLayer")
protected abstract boolean antarchy$addLayer(RenderLayer<T, M> layer);
@Inject(method = "<init>", at = @At("RETURN"))
private void antarchy$addTemporalContractionLayer(EntityRendererProvider.Context context, M model,
float shadowRadius, CallbackInfo callbackInfo) {
LivingEntityRenderer<T, M> renderer = (LivingEntityRenderer<T, M>) (Object) this;
renderer.addLayer(new TemporalContractionAfterimageLayer<>(renderer));
this.antarchy$addLayer(new TemporalContractionAfterimageLayer<>(renderer));
}
}
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff. Show more