diff --git a/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java b/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java index b43cc09e..aa74e715 100644 --- a/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java +++ b/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java @@ -76,11 +76,9 @@ public class OctopusBombEntity extends Monster implements GeoEntity { private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); - // Squid-style wander private float wanderDx, wanderDy, wanderDz; private int wanderRetargetTicks; - // Combat private int attackCooldown; private int actionTicks; private int lastAttackState; @@ -89,7 +87,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { private float orbitDirection = 1.0F; private int inkSprayCooldown; - // Kraken spawn flag private boolean spawnedByKraken; // Explosive impact (fired from the R.P.O. Launcher) @@ -366,10 +363,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { this.hasImpulse = true; } - // ------------------------------------------------------------------------- - // Squid-style wander - // ------------------------------------------------------------------------- - private void tickWanderMovement() { if (--this.wanderRetargetTicks <= 0) { this.wanderRetargetTicks = 40 + this.random.nextInt(40); @@ -388,10 +381,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { this.setDeltaMovement(this.getDeltaMovement().add(this.wanderDx, dy, this.wanderDz)); } - // ------------------------------------------------------------------------- - // Ink spray - // ------------------------------------------------------------------------- - private boolean hasHitExplosiveTarget() { AABB box = this.getBoundingBox(); return !this.level().getEntitiesOfClass(LivingEntity.class, box, @@ -447,10 +436,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { serverLevel.sendParticles(OCTOPUS_BOMB_INK_PARTICLE, cx, cy, cz, 30, 3.5D, 1.8D, 3.5D, 0.02D); } - // ------------------------------------------------------------------------- - // Combat - // ------------------------------------------------------------------------- - private void tickPursuitMovement(LivingEntity target) { if (this.strafeRetargetTicks-- <= 0) { this.strafeRetargetTicks = 10 + this.random.nextInt(12); diff --git a/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java b/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java index eabd0260..20480b3e 100644 --- a/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java +++ b/common-1.20.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java @@ -157,7 +157,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem, com.craisinlord return; } - // Toreterror jump tracking if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) { player.fallDistance = 0.0F; // prevent fall damage during the crash-down boolean launched = readInt(stack, TORETERROR_LAUNCHED_TAG) == 1; @@ -178,7 +177,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem, com.craisinlord } } - // Tick client shake counter if (level.isClientSide && clientShakeTicks > 0) { clientShakeTicks--; } diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/Antarchy.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/Antarchy.java index ea6644dd..e62cdde1 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/Antarchy.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/Antarchy.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger; public class Antarchy { public static final String MODID = "antarchy"; + public static final String MOD_VERSION = "2.0.0"; public static final Logger LOGGER = LogManager.getLogger(); public static void init() { diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/AntarchySoundEvents.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/AntarchySoundEvents.java index 73f7bc1f..2a90521c 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/AntarchySoundEvents.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/AntarchySoundEvents.java @@ -197,6 +197,7 @@ public final class AntarchySoundEvents { public static Supplier QUEEN_ROAR = (Supplier) UNBOUND; public static Supplier KING_BEAM_SHOOT = (Supplier) UNBOUND; public static Supplier KING_FIREBALL_SHOOT = (Supplier) UNBOUND; + public static Supplier KING_ICE_SPIKES = (Supplier) UNBOUND; public static Supplier KING_DECREE_CAST = (Supplier) UNBOUND; public static Supplier QUEEN_BEAM_SHOOT = (Supplier) UNBOUND; public static Supplier QUEEN_BEAM_START = (Supplier) UNBOUND; @@ -409,6 +410,7 @@ public final class AntarchySoundEvents { Supplier queenRoar, Supplier kingBeamShoot, Supplier kingFireballShoot, + Supplier kingIceSpikes, Supplier kingDecreeCast, Supplier queenBeamShoot, Supplier queenBeamStart, @@ -617,6 +619,7 @@ public final class AntarchySoundEvents { QUEEN_ROAR = queenRoar; KING_BEAM_SHOOT = kingBeamShoot; KING_FIREBALL_SHOOT = kingFireballShoot; + KING_ICE_SPIKES = kingIceSpikes; KING_DECREE_CAST = kingDecreeCast; QUEEN_BEAM_SHOOT = queenBeamShoot; QUEEN_BEAM_START = queenBeamStart; diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAddress.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAddress.java new file mode 100644 index 00000000..26d6a820 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAddress.java @@ -0,0 +1,76 @@ +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); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAttachment.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAttachment.java new file mode 100644 index 00000000..e6efc7a9 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAttachment.java @@ -0,0 +1,102 @@ +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; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAttachmentFiles.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAttachmentFiles.java new file mode 100644 index 00000000..05e2a268 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailAttachmentFiles.java @@ -0,0 +1,93 @@ +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 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 selectableTextFiles(Collection files) { + if (files == null) return List.of(); + List result = new ArrayList<>(); + for (ComputerFileSystem.ComputerFile file : files) { + if (fromComputerFile(file).isPresent()) result.add(file); + } + return List.copyOf(result); + } + + public static List selectablePaintFiles(Collection files) { + if (files == null) return List.of(); + List 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); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailDeliveryResult.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailDeliveryResult.java new file mode 100644 index 00000000..7cba81c4 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailDeliveryResult.java @@ -0,0 +1,28 @@ +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); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailDraft.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailDraft.java new file mode 100644 index 00000000..4fe498ab --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailDraft.java @@ -0,0 +1,43 @@ +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 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 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); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMailbox.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMailbox.java new file mode 100644 index 00000000..0b1450e4 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMailbox.java @@ -0,0 +1,93 @@ +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 { + private final AntmailAddress address; + private final List inbox = new ArrayList<>(); + private final List sent = new ArrayList<>(); + private final List drafts = new ArrayList<>(); + + public AntmailMailbox(AntmailAddress address) { + this.address = address; + } + + public AntmailAddress address() { return address; } + public List inbox() { return List.copyOf(inbox); } + public List sent() { return List.copyOf(sent); } + public List drafts() { return List.copyOf(drafts); } + public int unreadCount() { return (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 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() { + CompoundTag tag = new CompoundTag(); + tag.putString("Address", address.fullAddress()); + tag.put("Inbox", messagesToTag(inbox)); + tag.put("Sent", messagesToTag(sent)); + ListTag draftTags = new ListTag(); + for (AntmailDraft draft : drafts) draftTags.add(draft.toTag()); + tag.put("Drafts", draftTags); + return tag; + } + + public static AntmailMailbox fromTag(CompoundTag tag) { + AntmailMailbox mailbox = new AntmailMailbox(AntmailAddress.parse(tag.getString("Address"))); + 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 messages) { + ListTag tags = new ListTag(); + for (AntmailMessage message : messages) tags.add(message.toTag()); + return tags; + } + + private static void readMessages(ListTag tags, List 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 messages, UUID id) { + return messages.stream().anyMatch(message -> message.id().equals(id)); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMailboxStore.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMailboxStore.java new file mode 100644 index 00000000..5fe98546 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMailboxStore.java @@ -0,0 +1,54 @@ +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 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 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; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMessage.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMessage.java new file mode 100644 index 00000000..43d375f5 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailMessage.java @@ -0,0 +1,80 @@ +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 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 attachments; + private boolean read; + + public AntmailMessage(UUID id, AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List 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 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 attachments() { return attachments; } + public boolean read() { return read; } + public void markRead() { read = true; } + public void markUnread() { read = false; } + + public CompoundTag toTag() { + 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); + 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 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))); + return 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)); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailServerData.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailServerData.java new file mode 100644 index 00000000..e0ce6938 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailServerData.java @@ -0,0 +1,174 @@ +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.world.level.saveddata.SavedData; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class AntmailServerData extends SavedData { + private static final String ID = "antarchy_antmail"; + private final Map endpoints = new LinkedHashMap<>(); + private final AntmailMailboxStore mailboxes = new AntmailMailboxStore(); + private final List pending = new ArrayList<>(); + + 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) { + } + } + return loaded; + } + + @Override + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + ListTag endpointTags = new ListTag(); + for (Map.Entry 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); + 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 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 AntmailMailbox mailbox(AntmailAddress address) { + return mailboxes.get(address); + } + + public synchronized AntmailMailbox mailboxOrCreate(AntmailAddress address) { + AntmailMailbox mailbox = mailboxes.getOrCreate(address); + setDirty(); + return mailbox; + } + + public synchronized AntmailDeliveryResult deliver(MinecraftServer server, AntmailMessage message) { + if (endpoint(message.recipient()) == null) return AntmailDeliveryResult.failed(AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND, message, "address_not_found"); + AntmailMailbox destination = mailboxes.getOrCreate(message.recipient()); + if (destination.inbox().size() >= AntmailValidation.MAX_MAILBOX_MESSAGES) return AntmailDeliveryResult.failed(AntmailDeliveryResult.Status.MAILBOX_FULL, message, "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()))) { + pending.add(new PendingMessage(message.recipient(), message)); + mailboxes.getOrCreate(message.sender()).addSent(message); + setDirty(); + return AntmailDeliveryResult.queued(message); + } + destination.addIncoming(message); + mailboxes.getOrCreate(message.sender()).addSent(message); + setDirty(); + return AntmailDeliveryResult.delivered(message); + } + + public synchronized void drain(MinecraftServer server, AntmailAddress address) { + AntmailMailbox mailbox = mailboxes.getOrCreate(address); + pending.removeIf(pendingMessage -> { + if (!pendingMessage.recipient().equals(address) || mailbox.inbox().size() >= AntmailValidation.MAX_MAILBOX_MESSAGES) return false; + mailbox.addIncoming(pendingMessage.message()); + return true; + }); + setDirty(); + } + + 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) { + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailValidation.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailValidation.java new file mode 100644 index 00000000..a9676e10 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailValidation.java @@ -0,0 +1,67 @@ +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 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 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); + } + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailWire.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailWire.java new file mode 100644 index 00000000..858c8857 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/antmail/AntmailWire.java @@ -0,0 +1,55 @@ +package com.craisinlord.antarchy.content.antmail; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.TagParser; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +public final class AntmailWire { + private AntmailWire() { + } + + public static String encodeAttachments(List 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 = Base64.getEncoder().encodeToString(root.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)); + if (encoded.length() > AntmailValidation.MAX_ATTACHMENT_WIRE_CHARS) throw new IllegalArgumentException("Attachment payload is too large"); + return encoded; + } + + public static String encodeTag(CompoundTag tag) { + return Base64.getEncoder().encodeToString(tag.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + + public static CompoundTag decodeTag(String encoded) { + try { + return TagParser.parseTag(new String(Base64.getDecoder().decode(encoded), java.nio.charset.StandardCharsets.UTF_8)); + } catch (Exception exception) { + throw new IllegalArgumentException("Invalid Antmail data", exception); + } + } + + public static List 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 { + String snbt = new String(Base64.getDecoder().decode(encoded), java.nio.charset.StandardCharsets.UTF_8); + CompoundTag root = TagParser.parseTag(snbt); + ListTag list = root.getList("Attachments", 10); + List 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); + } + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/ComputerBlock.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/ComputerBlock.java index 6a8ca442..40f34539 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/ComputerBlock.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/ComputerBlock.java @@ -5,6 +5,7 @@ import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity; 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; @@ -13,7 +14,9 @@ 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; @@ -21,6 +24,8 @@ 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; @@ -28,11 +33,12 @@ import net.minecraft.world.phys.shapes.VoxelShape; public final class ComputerBlock extends BaseEntityBlock { public static final MapCodec 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)); + registerDefaultState(stateDefinition.any().setValue(ACTIVE, false).setValue(FACING, Direction.NORTH)); } @Override @@ -42,7 +48,22 @@ public final class ComputerBlock extends BaseEntityBlock { @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(ACTIVE); + 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 @@ -55,6 +76,25 @@ public final class ComputerBlock extends BaseEntityBlock { 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) { @@ -92,12 +132,6 @@ public final class ComputerBlock extends BaseEntityBlock { protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) { - if (player.isShiftKeyDown()) { - if (computer.ejectOne(player)) { - return net.minecraft.world.InteractionResult.SUCCESS; - } - return net.minecraft.world.InteractionResult.PASS; - } computer.activate(); if (level.isClientSide) { AntarchyClientHooks.openComputer(pos); diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/entity/ComputerBlockEntity.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/entity/ComputerBlockEntity.java index 4f330314..f4dc81f8 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/entity/ComputerBlockEntity.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/block/entity/ComputerBlockEntity.java @@ -2,6 +2,8 @@ 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 net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; @@ -10,12 +12,12 @@ 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 net.minecraft.world.phys.Vec3; import java.util.ArrayList; import java.util.LinkedHashSet; @@ -31,15 +33,27 @@ 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 final Set diskIds = new LinkedHashSet<>(); + 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 long AUTHENTICATION_TIMEOUT = 6000L; + private final Set 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; public ComputerBlockEntity(BlockPos pos, BlockState state, Supplier> type) { @@ -60,6 +74,11 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn 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) { @@ -67,12 +86,12 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn return false; } ResourceLocation diskId = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get()); - if (diskId == null || this.diskIds.contains(diskId)) { + if (diskId == null || this.physicalDiskIds.contains(diskId)) { stack.shrink(1); activate(); return true; } - this.diskIds.add(diskId); + this.physicalDiskIds.add(diskId); stack.shrink(1); activate(); setChanged(); @@ -94,12 +113,7 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn } public boolean isActive() { - return level != null && (level.hasNeighborSignal(worldPosition) || level.getGameTime() < activeUntil || hasNearbyPlayer()); - } - - private boolean hasNearbyPlayer() { - Vec3 center = Vec3.atCenterOf(worldPosition); - return level.players().stream().anyMatch(player -> player.distanceToSqr(center) <= 25.0D); + return level != null && (level.hasNeighborSignal(worldPosition) || isAuthenticated()); } @Override @@ -116,15 +130,23 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn } public List diskIds() { - return List.copyOf(diskIds); + List ids = new ArrayList<>(physicalDiskIds.size() + 1); + ids.add(INTRODUCTION_DISK); + ids.addAll(physicalDiskIds); + return List.copyOf(ids); } public boolean ejectOne(Player player) { - if (level == null || level.isClientSide || diskIds.isEmpty()) { + 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; } - ResourceLocation diskId = diskIds.iterator().next(); - diskIds.remove(diskId); ItemStack disk = diskStack(diskId); if (!player.addItem(disk)) { player.drop(disk, false); @@ -138,10 +160,158 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn if (level.isClientSide) { return; } - for (ResourceLocation diskId : diskIds) { + for (ResourceLocation diskId : physicalDiskIds) { Containers.dropItemStack(level, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, diskStack(diskId)); } - diskIds.clear(); + 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 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) { @@ -153,32 +323,45 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.loadAdditional(tag, registries); - diskIds.clear(); + physicalDiskIds.clear(); ListTag disks = tag.getList(DISKS_TAG, 8); for (int index = 0; index < disks.size(); index++) { try { - diskIds.add(ResourceLocation.parse(disks.getString(index))); + 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)); + 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 : diskIds) { + 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()); } @Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { CompoundTag tag = new CompoundTag(); - saveAdditional(tag, registries); + tag.putBoolean("HasPassword", hasPassword()); + tag.putBoolean(AUTHENTICATED_TAG, isAuthenticated()); return tag; } diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/AntmailClientState.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/AntmailClientState.java new file mode 100644 index 00000000..2a94b7e8 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/AntmailClientState.java @@ -0,0 +1,22 @@ +package com.craisinlord.antarchy.content.client; + +import com.craisinlord.antarchy.content.network.AntmailResultPayload; +import net.minecraft.core.BlockPos; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class AntmailClientState { + private static final Map RESULTS = new ConcurrentHashMap<>(); + + private AntmailClientState() { + } + + public static void update(AntmailResultPayload payload) { + RESULTS.put(payload.pos(), payload); + } + + public static AntmailResultPayload get(BlockPos pos) { + return RESULTS.get(pos); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/ComputerAccessClientState.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/ComputerAccessClientState.java new file mode 100644 index 00000000..1c640fdb --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/ComputerAccessClientState.java @@ -0,0 +1,23 @@ +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 RESULTS = new ConcurrentHashMap<>(); + + private ComputerAccessClientState() { + } + + public static void update(ComputerAccessResultPayload result) { + RESULTS.put(result.pos(), result); + ComputerFileSystemClientState.update(result); + } + + public static ComputerAccessResultPayload get(BlockPos pos) { + return RESULTS.get(pos); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/ComputerFileSystemClientState.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/ComputerFileSystemClientState.java new file mode 100644 index 00000000..73e184ef --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/ComputerFileSystemClientState.java @@ -0,0 +1,60 @@ +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 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; + } + State state = STATES.computeIfAbsent(payload.pos(), ignored -> new State()); + state.success = payload.result() == ComputerAccessResultPayload.SUCCESS; + state.error = envelope[1]; + if (action == ComputerAccessPayload.FILE_LIST && state.success) { + state.files = new ArrayList<>(); + if (!envelope[2].isEmpty()) state.files.addAll(List.of(envelope[2].split("\\n"))); + } else if (action == ComputerAccessPayload.FILE_OPEN && state.success) { + String[] file = envelope[2].split("\u0000", 2); + if (file.length == 2) { + state.openedPath = file[0]; + state.openedContents = file[1]; + } + } + } + + public static State get(BlockPos pos) { + return STATES.computeIfAbsent(pos, ignored -> new State()); + } + + public static final class State { + private List files = List.of(); + private String openedPath = ""; + private String openedContents = ""; + private String error = ""; + private boolean success; + + public List 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; } + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmGame.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmGame.java new file mode 100644 index 00000000..19b9059b --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmGame.java @@ -0,0 +1,168 @@ +package com.craisinlord.antarchy.content.client.game; + +import java.util.ArrayList; +import java.util.List; + +public final class AntFarmGame { + public enum Phase { READY, PLAYING, WON, LOST } + public enum Direction { + UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0), NONE(0, 0); + + private final int dx; + private final int dy; + + Direction(int dx, int dy) { + this.dx = dx; + this.dy = dy; + } + } + + private static final String[] MAP = { + "###################", + "#.................#", + "#.###.#####.#####.#", + "#.................#", + "#.#####.###.#####.#", + "#.................#", + "#.#####.###.#####.#", + "#.................#", + "#.#####.###.#####.#", + "#.................#", + "###################" + }; + private static final int TICKS_PER_TILE = 4; + private static final int PLAYER_START_X = 1; + private static final int PLAYER_START_Y = 1; + private static final int ENEMY_START_X = 17; + private static final int ENEMY_START_Y = 9; + + private final boolean[][] pellets = new boolean[MAP.length][MAP[0].length()]; + private Phase phase = Phase.READY; + private Direction playerDirection = Direction.NONE; + private Direction requestedDirection = Direction.NONE; + private Direction enemyDirection = Direction.LEFT; + private int playerX; + private int playerY; + private int enemyX; + private int enemyY; + private int movementTicks; + private int score; + private int remainingFood; + + public AntFarmGame() { + reset(); + } + + public void reset() { + phase = Phase.READY; + playerDirection = Direction.NONE; + requestedDirection = Direction.NONE; + enemyDirection = Direction.LEFT; + playerX = PLAYER_START_X; + playerY = PLAYER_START_Y; + enemyX = ENEMY_START_X; + enemyY = ENEMY_START_Y; + movementTicks = 0; + score = 0; + remainingFood = 0; + for (int y = 0; y < MAP.length; y++) { + for (int x = 0; x < MAP[y].length(); x++) { + pellets[y][x] = MAP[y].charAt(x) == '.'; + if (pellets[y][x]) remainingFood++; + } + } + } + + public void start() { + if (phase == Phase.READY) phase = Phase.PLAYING; + } + + public void tick() { + if (phase != Phase.PLAYING) return; + movementTicks++; + if (movementTicks < TICKS_PER_TILE) return; + movementTicks = 0; + movePlayer(); + if (phase != Phase.PLAYING) return; + moveEnemy(); + if (playerX == enemyX && playerY == enemyY) phase = Phase.LOST; + } + + public void setRequestedDirection(Direction direction) { + if (direction != null && direction != Direction.NONE) requestedDirection = direction; + } + + public Phase phase() { return phase; } + public int score() { return score; } + public int remainingFood() { return remainingFood; } + public int width() { return MAP[0].length(); } + public int height() { return MAP.length; } + public int playerX() { return playerX; } + public int playerY() { return playerY; } + public int enemyX() { return enemyX; } + public int enemyY() { return enemyY; } + public boolean hasPellet(int x, int y) { return pellets[y][x]; } + public boolean isWall(int x, int y) { return MAP[y].charAt(x) == '#'; } + + private void movePlayer() { + if (canMove(playerX, playerY, requestedDirection)) playerDirection = requestedDirection; + if (canMove(playerX, playerY, playerDirection)) { + playerX += playerDirection.dx; + playerY += playerDirection.dy; + } + if (pellets[playerY][playerX]) { + pellets[playerY][playerX] = false; + remainingFood--; + score += 10; + if (remainingFood == 0) phase = Phase.WON; + } + } + + private void moveEnemy() { + List directions = legalDirections(enemyX, enemyY); + if (directions.isEmpty()) return; + Direction reverse = opposite(enemyDirection); + if (directions.size() > 1) directions.remove(reverse); + Direction best = directions.get(0); + int bestDistance = distanceAfter(best, playerX, playerY, enemyX, enemyY); + for (Direction direction : directions) { + int distance = distanceAfter(direction, playerX, playerY, enemyX, enemyY); + if (distance < bestDistance) { + best = direction; + bestDistance = distance; + } + } + enemyDirection = best; + enemyX += best.dx; + enemyY += best.dy; + } + + private int distanceAfter(Direction direction, int targetX, int targetY, int originX, int originY) { + return Math.abs(originX + direction.dx - targetX) + Math.abs(originY + direction.dy - targetY); + } + + private List legalDirections(int x, int y) { + List result = new ArrayList<>(4); + for (Direction direction : new Direction[]{Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT}) { + if (canMove(x, y, direction)) result.add(direction); + } + return result; + } + + private boolean canMove(int x, int y, Direction direction) { + if (direction == Direction.NONE) return false; + int nextX = x + direction.dx; + int nextY = y + direction.dy; + return nextY >= 0 && nextY < MAP.length && nextX >= 0 && nextX < MAP[nextY].length() && !isWall(nextX, nextY); + } + + private Direction opposite(Direction direction) { + return switch (direction) { + case UP -> Direction.DOWN; + case DOWN -> Direction.UP; + case LEFT -> Direction.RIGHT; + case RIGHT -> Direction.LEFT; + case NONE -> Direction.NONE; + }; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmInput.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmInput.java new file mode 100644 index 00000000..1e3f47eb --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmInput.java @@ -0,0 +1,22 @@ +package com.craisinlord.antarchy.content.client.game; + +import org.lwjgl.glfw.GLFW; + +public final class AntFarmInput { + private AntFarmInput() { + } + + public static AntFarmGame.Direction directionForKey(int keyCode) { + return switch (keyCode) { + case GLFW.GLFW_KEY_W, GLFW.GLFW_KEY_UP -> AntFarmGame.Direction.UP; + case GLFW.GLFW_KEY_S, GLFW.GLFW_KEY_DOWN -> AntFarmGame.Direction.DOWN; + case GLFW.GLFW_KEY_A, GLFW.GLFW_KEY_LEFT -> AntFarmGame.Direction.LEFT; + case GLFW.GLFW_KEY_D, GLFW.GLFW_KEY_RIGHT -> AntFarmGame.Direction.RIGHT; + default -> AntFarmGame.Direction.NONE; + }; + } + + public static boolean isStartOrRetry(int keyCode) { + return keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmProgram.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmProgram.java new file mode 100644 index 00000000..1a42f278 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmProgram.java @@ -0,0 +1,53 @@ +package com.craisinlord.antarchy.content.client.game; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; + +public final class AntFarmProgram { + private final AntFarmGame game = new AntFarmGame(); + private boolean gameInstalled; + + public AntFarmProgram(boolean gameInstalled) { + this.gameInstalled = gameInstalled; + } + + public void setGameInstalled(boolean gameInstalled) { + this.gameInstalled = gameInstalled; + if (!gameInstalled) game.reset(); + } + + public boolean isGameInstalled() { return gameInstalled; } + public AntFarmGame game() { return game; } + + public void tick() { + if (gameInstalled) game.tick(); + } + + public boolean keyPressed(int keyCode) { + if (!gameInstalled) return false; + AntFarmGame.Direction direction = AntFarmInput.directionForKey(keyCode); + if (direction != AntFarmGame.Direction.NONE) { + if (game.phase() == AntFarmGame.Phase.READY) game.start(); + game.setRequestedDirection(direction); + return true; + } + if (AntFarmInput.isStartOrRetry(keyCode)) { + if (game.phase() == AntFarmGame.Phase.READY) game.start(); + else if (game.phase() == AntFarmGame.Phase.WON || game.phase() == AntFarmGame.Phase.LOST) { + game.reset(); + game.start(); + } + return true; + } + return false; + } + + public void render(GuiGraphics graphics, Font font, int x, int y, int width, int height) { + if (!gameInstalled) { + graphics.fill(x, y, x + width, y + height, 0xFF020602); + graphics.drawString(font, "ANT FARM NOT INSTALLED", x + 12, y + height / 2, 0xFF65FF65, false); + return; + } + AntFarmRenderer.render(graphics, font, game, x, y, width, height); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmRenderer.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmRenderer.java new file mode 100644 index 00000000..b38542f1 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/game/AntFarmRenderer.java @@ -0,0 +1,65 @@ +package com.craisinlord.antarchy.content.client.game; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +public final class AntFarmRenderer { + private static final int BLACK = 0xFF020602; + private static final int DARK_GREEN = 0xFF123012; + private static final int GREEN = 0xFF65FF65; + private static final int PALE_GREEN = 0xFFB8FFB8; + + private AntFarmRenderer() { + } + + public static void render(GuiGraphics graphics, Font font, AntFarmGame game, int x, int y, int width, int height) { + graphics.fill(x, y, x + width, y + height, BLACK); + int hudHeight = 18; + int boardX = x + 5; + int boardY = y + hudHeight + 4; + int boardWidth = width - 10; + int boardHeight = height - hudHeight - 9; + int tileSize = Math.max(1, Math.min(boardWidth / game.width(), boardHeight / game.height())); + int mazeWidth = tileSize * game.width(); + int mazeHeight = tileSize * game.height(); + int mazeX = boardX + Math.max(0, (boardWidth - mazeWidth) / 2); + int mazeY = boardY + Math.max(0, (boardHeight - mazeHeight) / 2); + graphics.drawString(font, Component.literal("SCORE " + String.format("%06d", game.score())), x + 6, y + 4, GREEN, false); + graphics.drawString(font, Component.literal("FOOD " + String.format("%03d", game.remainingFood())), x + width - 76, y + 4, PALE_GREEN, false); + for (int gridY = 0; gridY < game.height(); gridY++) { + for (int gridX = 0; gridX < game.width(); gridX++) { + int cellX = mazeX + gridX * tileSize; + int cellY = mazeY + gridY * tileSize; + if (game.isWall(gridX, gridY)) { + graphics.fill(cellX, cellY, cellX + tileSize, cellY + tileSize, DARK_GREEN); + } else if (game.hasPellet(gridX, gridY)) { + int pelletSize = Math.max(1, tileSize / 4); + int offset = (tileSize - pelletSize) / 2; + graphics.fill(cellX + offset, cellY + offset, cellX + offset + pelletSize, cellY + offset + pelletSize, PALE_GREEN); + } + } + } + drawAnt(graphics, mazeX + game.playerX() * tileSize, mazeY + game.playerY() * tileSize, tileSize, GREEN); + drawAnt(graphics, mazeX + game.enemyX() * tileSize, mazeY + game.enemyY() * tileSize, tileSize, PALE_GREEN); + if (game.phase() != AntFarmGame.Phase.PLAYING) { + String message = switch (game.phase()) { + case READY -> "ANT FARM // PRESS ENTER TO START"; + case WON -> "COLONY FED // ENTER TO PLAY AGAIN"; + case LOST -> "COLONY COMPROMISED // ENTER TO RETRY"; + case PLAYING -> ""; + }; + int messageWidth = font.width(message); + graphics.fill(x + 8, y + height / 2 - 12, x + width - 8, y + height / 2 + 12, BLACK); + graphics.drawString(font, Component.literal(message), x + Math.max(8, (width - messageWidth) / 2), y + height / 2 - 4, GREEN, false); + } + } + + private static void drawAnt(GuiGraphics graphics, int x, int y, int tileSize, int color) { + int inset = Math.max(1, tileSize / 4); + int center = x + tileSize / 2; + graphics.fill(center - inset, y + inset, center + inset + 1, y + tileSize - inset, color); + graphics.fill(x + inset, y + tileSize / 2 - inset, x + tileSize - inset, y + tileSize / 2 + inset + 1, color); + if (tileSize >= 5) graphics.fill(center - inset, y + inset, center + inset + 1, y + inset + 1, BLACK); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/renderer/ComputerRenderer.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/renderer/ComputerRenderer.java index d1ffa3b0..24dd5208 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/renderer/ComputerRenderer.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/renderer/ComputerRenderer.java @@ -2,10 +2,16 @@ 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 { 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); + } } diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/screen/ComputerScreen.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/screen/ComputerScreen.java index d411944d..0b627eb5 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/screen/ComputerScreen.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/client/screen/ComputerScreen.java @@ -1,150 +1,1054 @@ package com.craisinlord.antarchy.content.client.screen; -import com.craisinlord.antarchy.content.AntarchyObjects; +import com.craisinlord.antarchy.Antarchy; import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity; +import com.craisinlord.antarchy.content.client.ComputerAccessClientState; +import com.craisinlord.antarchy.content.client.ComputerFileSystemClientState; +import com.craisinlord.antarchy.content.client.AntmailClientState; +import com.craisinlord.antarchy.content.antmail.AntmailAddress; +import com.craisinlord.antarchy.content.antmail.AntmailMailbox; +import com.craisinlord.antarchy.content.antmail.AntmailMessage; +import com.craisinlord.antarchy.content.antmail.AntmailWire; +import com.craisinlord.antarchy.content.antmail.AntmailAttachment; import com.craisinlord.antarchy.content.guide.ComputerGuideData; +import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload; +import com.craisinlord.antarchy.content.network.AntmailResultPayload; +import com.craisinlord.antarchy.content.network.ComputerNetworking; +import com.craisinlord.antarchy.content.network.AntmailNetworking; +import com.craisinlord.antarchy.content.computer.paint.AntPaintCanvas; +import com.craisinlord.antarchy.content.computer.paint.AntPaintHistory; +import com.craisinlord.antarchy.content.computer.paint.AntPaintTool; +import com.craisinlord.antarchy.content.computer.paint.AntPaintFile; +import com.craisinlord.antarchy.content.computer.paint.AntPaintFileCodec; +import com.craisinlord.antarchy.content.computer.ComputerFileSystem; +import com.craisinlord.antarchy.content.computer.ComputerDesktopState; +import com.craisinlord.antarchy.content.client.game.AntFarmProgram; +import com.craisinlord.antarchy.content.computer.terminal.TerminalCommandParser; +import com.craisinlord.antarchy.content.computer.terminal.TerminalResult; +import org.lwjgl.glfw.GLFW; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; -import net.minecraft.util.FormattedCharSequence; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Base64; +import java.util.UUID; public final class ComputerScreen extends Screen { - private static final int WIDTH = 320; - private static final int HEIGHT = 210; - private static final String[] CATEGORIES = {"general", "elythia", "thoraxis", "cavaryn"}; - private static final int[] COLORS = {0xFF65FF65, 0xFF65FF65, 0xFFFF5A5A, 0xFFFFA044}; - private final ResourceLocation positionDimension; + private static final int WIDTH = 440; + private static final int HEIGHT = 286; + private static final int GREEN = 0xFF65FF65; + private static final int PALE_GREEN = 0xFFB8FFB8; + private static final int DARK_GREEN = 0xFF102010; + private static final int BLACK = 0xFF030603; + private static final String[] BOOT_MESSAGES = {"ANTS MARCHING...", "COMPUTER COMPUTING...", "HCFS BREWING...", "WAKING THE QUEEN..."}; + private static final long SESSION_TIMEOUT = 300_000L; + private static final Map SESSIONS = new LinkedHashMap<>(); + private static final String[] ICONS = {"ARCHIVE", "FILES", "SETTINGS", "TERMINAL", "TEXT", "PAINT", "ANTMAIL", "GAMES", "TRASH"}; + private static final Map TITLES = Map.of("ARCHIVE", "ANTARCHIVE", "FILES", "FILE EXPLORER", "SETTINGS", "SYSTEM SETTINGS", "TERMINAL", "ANTINTOSH TERMINAL", "TEXT", "ANTTEXT EDITOR", "PAINT", "ANTPAINT", "ANTMAIL", "ANTMAIL", "GAMES", "INSTALLED GAMES", "TRASH", "RECYCLE BIN"); private final net.minecraft.core.BlockPos position; - private int categoryIndex; - private int selectedIndex; - private List visibleEntries = List.of(); + private final String sessionKey; + private final Session session; + private boolean loggedIn; + private String password = ""; + private String confirmation = ""; + private boolean confirmingPassword; + private String loginMessage = ""; + private int loginMessageTicks; + private ComputerAccessResultPayload accessResult; + private int observedResult = -1; + private Window dragging; + private String draggedDisk; + private ResourceLocation selectedDisk; + private int dragX; + private int dragY; + private Window activeWindow; + private final AntFarmProgram antFarm = new AntFarmProgram(false); public ComputerScreen(net.minecraft.core.BlockPos position) { super(Component.translatable("screen.antarchy.computer")); this.position = position; - this.positionDimension = Minecraft.getInstance().level == null ? null : Minecraft.getInstance().level.dimension().location(); - } - - @Override - protected void init() { - refreshEntries(); - int left = (this.width - WIDTH) / 2; - int top = (this.height - HEIGHT) / 2; - for (int index = 0; index < CATEGORIES.length; index++) { - int buttonIndex = index; - addRenderableWidget(Button.builder(Component.translatable("guide.antarchy.category." + CATEGORIES[index]), button -> { - categoryIndex = buttonIndex; - selectedIndex = 0; - refreshEntries(); - }).bounds(left + 10 + index * 75, top + 10, 70, 20).build()); - } - } - - private void refreshEntries() { - if (Minecraft.getInstance().level != null && Minecraft.getInstance().level.getBlockEntity(position) instanceof ComputerBlockEntity computer) { - visibleEntries = ComputerGuideData.entriesFor(computer.diskIds()).stream() - .filter(entry -> entry.category().equals(CATEGORIES[categoryIndex])) - .toList(); - } else { - visibleEntries = List.of(); - } - if (selectedIndex >= visibleEntries.size()) { - selectedIndex = Math.max(0, visibleEntries.size() - 1); + String dimension = Minecraft.getInstance().level == null ? "" : Minecraft.getInstance().level.dimension().location().toString(); + this.sessionKey = dimension + ":" + position.asLong(); + this.session = SESSIONS.computeIfAbsent(sessionKey, ignored -> new Session()); + if (this.session.bootTicks < 0) this.session.bootTicks = 80; + ComputerNetworking.open(position); + AntmailNetworking.requestState(position); + this.accessResult = ComputerAccessClientState.get(position); + this.loggedIn = accessResult != null && accessResult.authenticated() && session.lastUse + SESSION_TIMEOUT > System.currentTimeMillis(); + if (!loggedIn) { + session.authenticated = false; + session.windows.clear(); } } @Override public void tick() { - refreshEntries(); + if (session.bootTicks > 0) session.bootTicks--; + if (loginMessageTicks > 0) loginMessageTicks--; + ComputerAccessResultPayload currentResult = ComputerAccessClientState.get(position); + if (currentResult != null) { + accessResult = currentResult; + if (currentResult.result() != observedResult) { + observedResult = currentResult.result(); + if (currentResult.result() == ComputerAccessResultPayload.SUCCESS) { + loginMessage = "ACCESS GRANTED // WELCOME"; + loginMessageTicks = 40; + } else if (currentResult.result() == ComputerAccessResultPayload.INVALID_PASSWORD) { + loginMessage = "ACCESS DENIED // PASSWORD REJECTED"; + loginMessageTicks = 100; + } else if (currentResult.result() == ComputerAccessResultPayload.BUSY) { + loginMessage = "TERMINAL BUSY // USER PRESENT"; + loginMessageTicks = 100; + } + } + } + if (!loggedIn && accessResult != null && accessResult.authenticated()) { + loggedIn = true; + session.authenticated = true; + session.lastUse = System.currentTimeMillis(); + } + if (loggedIn && (session.lastUse + SESSION_TIMEOUT <= System.currentTimeMillis() || accessResult == null || !accessResult.authenticated())) { + loggedIn = false; + session.authenticated = false; + session.windows.clear(); + } + if (loggedIn && !session.desktopRequested) { + ComputerNetworking.requestDesktopState(position); + session.desktopRequested = true; + } + if (currentResult != null && currentResult.data().startsWith("11\u0000")) { + String[] desktop = currentResult.data().split("\u0000", 3); + if (desktop.length == 3 && !desktop[2].isBlank()) { + try { + var state = new ComputerDesktopState(); + state.load(AntmailWire.decodeTag(desktop[2])); + session.wallpaperId = state.selectedWallpaper() == null ? ComputerDesktopState.DEFAULT_WALLPAPER.toString() : state.selectedWallpaper().toString(); + session.wallpapers = state.unlockedWallpaperIds().stream().map(ResourceLocation::toString).toList(); + } catch (RuntimeException ignored) { + } + } + } + antFarm.setGameInstalled(physicalDisks().stream().anyMatch(id -> id.getPath().equals("ant_farm"))); + if (activeWindow != null && activeWindow.type.equals("GAMES") && activeWindow.gameOpen) antFarm.tick(); } @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { - super.render(graphics, mouseX, mouseY, partialTick); - int left = (this.width - WIDTH) / 2; - int top = (this.height - HEIGHT) / 2; - graphics.fill(0, 0, this.width, this.height, 0x99000000); - graphics.fill(left, top, left + WIDTH, top + HEIGHT, 0xFF071007); - graphics.fill(left + 4, top + 4, left + WIDTH - 4, top + HEIGHT - 4, 0xFF102010); - int accent = COLORS[categoryIndex]; - graphics.fill(left + 8, top + 34, left + WIDTH - 8, top + 36, accent); - graphics.drawString(this.font, Component.translatable("screen.antarchy.computer"), left + 12, top + 45, accent, false); - graphics.drawString(this.font, Component.literal(positionDimension == null ? "ARCHIVE OFFLINE" : "ARCHIVE ONLINE"), left + WIDTH - 105, top + 45, 0xFF65FF65, false); - graphics.fill(left + 10, top + 60, left + 112, top + HEIGHT - 12, 0xFF050805); - for (int index = 0; index < visibleEntries.size() && index < 6; index++) { - ComputerGuideData.Entry entry = visibleEntries.get(index); - int y = top + 68 + index * 20; - int color = index == selectedIndex ? accent : 0xFF65FF65; - if (index == selectedIndex) { - graphics.fill(left + 12, y - 2, left + 110, y + 14, 0xFF173817); - } - graphics.drawString(this.font, Component.literal("FILE_" + (index + 1)), left + 16, y, color, false); - if (isMouseOver(left + 12, y - 2, 98, 16, mouseX, mouseY)) { - graphics.renderTooltip(this.font, Component.translatable(entry.titleKey()), mouseX, mouseY); - } + this.renderBackground(graphics, mouseX, mouseY, partialTick); + float scale = uiScale(); + graphics.pose().pushPose(); + graphics.pose().translate(width / 2.0F, height / 2.0F, 0.0F); + graphics.pose().scale(scale, scale, 1.0F); + int left = -WIDTH / 2; + int top = -HEIGHT / 2; + int localMouseX = (int) ((mouseX - width / 2.0F) / scale); + int localMouseY = (int) ((mouseY - height / 2.0F) / scale); + graphics.fill(left - 5, top - 5, left + WIDTH + 5, top + HEIGHT + 5, GREEN); + graphics.fill(left, top, left + WIDTH, top + HEIGHT, BLACK); + graphics.fill(left + 5, top + 5, left + WIDTH - 5, top + HEIGHT - 5, DARK_GREEN); + if (session.bootTicks > 0) renderBoot(graphics, left, top); else if (loggedIn) renderDesktop(graphics, left, top, localMouseX, localMouseY); else renderLogin(graphics, left, top); + graphics.pose().popPose(); + } + + private float uiScale() { + return Math.min(1.0F, Math.min((width - 24.0F) / WIDTH, (height - 24.0F) / HEIGHT)); + } + + private void renderBoot(GuiGraphics g, int l, int t) { + int progress = Math.min(100, (80 - session.bootTicks) * 100 / 80); + int message = Math.min(BOOT_MESSAGES.length - 1, progress * BOOT_MESSAGES.length / 101); + g.drawString(font, Component.literal("ANTINTOSH OS"), l + 28, t + 78, GREEN, false); + g.drawString(font, Component.literal("INITIALIZING SYSTEM..."), l + 28, t + 105, PALE_GREEN, false); + g.fill(l + 28, t + 132, l + WIDTH - 28, t + 148, BLACK); + box(g, l + 28, t + 132, l + WIDTH - 28, t + 148, GREEN); + g.fill(l + 32, t + 136, l + 32 + (WIDTH - 64) * progress / 100, t + 144, GREEN); + g.drawString(font, Component.literal(progress + "%"), l + 28, t + 164, GREEN, false); + g.drawString(font, Component.literal(BOOT_MESSAGES[message]), l + 28, t + 190, PALE_GREEN, false); + } + + private void renderLogin(GuiGraphics g, int l, int t) { + g.drawString(font, Component.literal("ANTINTOSH OS"), l + 28, t + 25, GREEN, false); + g.drawString(font, Component.literal("BOOT SEQUENCE COMPLETE"), l + 28, t + 43, PALE_GREEN, false); + g.fill(l + 28, t + 59, l + WIDTH - 28, t + 61, GREEN); + ComputerBlockEntity computer = computer(); + boolean setup = (accessResult != null && !accessResult.hasPassword()) || (computer != null && !computer.hasPassword()); + int formX = setup ? l + 110 : l + 242; + int formRight = setup ? l + 330 : l + WIDTH - 28; + g.drawString(font, Component.literal(setup ? "INITIALIZE COMPUTER" : "LOGIN REQUIRED"), formX, t + 105, GREEN, false); + g.drawString(font, Component.literal(setup ? (confirmingPassword ? "CONFIRM PASSWORD" : "CREATE PASSWORD") : "PASSWORD"), formX, t + 132, PALE_GREEN, false); + g.fill(formX, t + 150, formRight, t + 173, BLACK); + box(g, formX, t + 150, formRight, t + 173, GREEN); + g.drawString(font, Component.literal("*".repeat((confirmingPassword ? confirmation : password).length())), formX + 6, t + 157, GREEN, false); + g.drawString(font, Component.literal(setup ? (confirmingPassword ? "[ ENTER ] CONFIRM" : "[ ENTER ] CONTINUE") : "[ ENTER ] LOGIN"), formX, t + 187, GREEN, false); + g.drawString(font, Component.literal("[ ESC ] POWER DOWN"), formX, t + 205, PALE_GREEN, false); + if (loginMessageTicks > 0) wrap(g, loginMessage, formX, t + 232, setup ? 220 : 165, PALE_GREEN); + } + + private void renderDesktop(GuiGraphics g, int l, int t, int mouseX, int mouseY) { + session.lastUse = System.currentTimeMillis(); + if (session.wallpaperId.endsWith("grid_ant")) { + for (int py = t + 36; py < t + HEIGHT - 8; py += 18) for (int px = l + 8; px < l + WIDTH - 8; px += 36) g.fill(px, py, px + 2, py + 2, 0xFF173817); } - if (visibleEntries.isEmpty()) { - graphics.drawString(this.font, Component.literal("NO DATA FOUND"), left + 22, top + 86, accent, false); - graphics.drawString(this.font, Component.literal("INSERT FLOPPY DISKS"), left + 16, top + 104, 0xFF65FF65, false); - } else { - renderEntry(graphics, visibleEntries.get(selectedIndex), left + 124, top + 62, accent); + g.fill(l + 9, t + 9, l + WIDTH - 9, t + 30, BLACK); + g.drawString(font, Component.literal("ANTINTOSH OS // v" + Antarchy.MOD_VERSION), l + 18, t + 15, GREEN, false); + long dayTime = Minecraft.getInstance().level == null ? 0L : Minecraft.getInstance().level.getDayTime(); + long dayTicks = Math.floorMod(dayTime, 24000L); + int hours = (int) ((dayTicks / 1000L + 6L) % 24L); + int minutes = (int) (dayTicks % 1000L * 60L / 1000L); + g.drawString(font, Component.literal(String.format("%02d:%02d", hours, minutes)), l + WIDTH - 48, t + 15, PALE_GREEN, false); + g.fill(l + 9, t + 31, l + WIDTH - 9, t + 33, GREEN); + for (int i = 0; i < ICONS.length; i++) { + int x = l + 24 + i % 4 * 94; + int y = t + 48 + i / 4 * 76; + boolean hover = inside(x - 6, y - 6, 76, 57, mouseX, mouseY); + if (hover) g.fill(x - 7, y - 7, x + 69, y + 51, 0xFF173817); + icon(g, ICONS[i], x + 25, y + 2); + g.drawString(font, Component.literal(ICONS[i]), x, y + 35, GREEN, false); } - for (Renderable renderable : this.renderables) { - renderable.render(graphics, mouseX, mouseY, partialTick); + List disks = physicalDisks(); + for (int i = 0; i < disks.size() && i < 3; i++) { + int x = l + 24 + i % 4 * 94; + int y = t + 200 + i / 4 * 54; + icon(g, "DISK", x + 25, y + 2); + String name = disks.get(i).getPath(); + g.drawString(font, Component.literal(name.length() > 12 ? name.substring(0, 12) : name), x, y + 35, GREEN, false); + } + for (Window window : session.windows) if (!window.minimized) renderWindow(g, window, l, t, mouseX, mouseY); + } + + private void icon(GuiGraphics g, String type, int x, int y) { + g.fill(x - 10, y, x + 10, y + 18, BLACK); + g.fill(x - 8, y + 2, x + 8, y + 16, GREEN); + if (type.equals("SETTINGS")) { + g.fill(x - 12, y + 7, x + 12, y + 11, BLACK); + g.fill(x - 2, y - 2, x + 2, y + 20, BLACK); + } else if (type.equals("TRASH")) { + g.fill(x - 12, y - 3, x + 12, y, GREEN); + g.fill(x - 7, y + 4, x + 7, y + 18, BLACK); + } else if (type.equals("TERMINAL")) { + g.fill(x - 5, y + 7, x - 1, y + 11, BLACK); + g.fill(x - 1, y + 11, x + 6, y + 14, BLACK); } } - private void renderEntry(GuiGraphics graphics, ComputerGuideData.Entry entry, int x, int y, int accent) { - graphics.drawString(this.font, Component.translatable(entry.titleKey()), x, y, accent, false); - if (!entry.subtitleKey().isEmpty()) { - graphics.drawString(this.font, Component.translatable(entry.subtitleKey()), x, y + 16, 0xFF99FF99, false); + private void renderWindow(GuiGraphics g, Window window, int l, int t, int mouseX, int mouseY) { + int x = l + window.x; + int y = t + window.y; + int w = window.maximized ? WIDTH - 28 : window.width; + int h = window.maximized ? HEIGHT - 48 : window.height; + window.renderWidth = w; + window.renderHeight = h; + g.fill(x - 2, y - 2, x + w + 2, y + h + 2, GREEN); + g.fill(x, y, x + w, y + h, BLACK); + g.fill(x, y, x + w, y + 19, 0xFF173817); + g.drawString(font, Component.literal(window.title), x + 7, y + 6, GREEN, false); + g.drawString(font, Component.literal("_"), x + w - 39, y + 5, PALE_GREEN, false); + g.drawString(font, Component.literal(window.maximized ? "❐" : "□"), x + w - 27, y + 5, PALE_GREEN, false); + g.drawString(font, Component.literal("X"), x + w - 13, y + 6, GREEN, false); + int cx = x + 8; + int cy = y + 28; + if (window.type.equals("ARCHIVE")) renderArchive(g, cx, cy, w - 16, h - 34); + else if (window.type.equals("FILES")) renderFileExplorer(g, cx, cy, w, h); + else if (window.type.equals("SETTINGS")) renderSettings(g, cx, cy, h - 34); + else if (window.type.equals("TERMINAL")) renderTerminal(g, cx, cy, h); + else if (window.type.equals("TEXT")) renderTextEditor(g, cx, cy, w, h); + else if (window.type.equals("PAINT")) renderPaint(g, cx, cy, w, h); + else if (window.type.equals("ANTMAIL")) renderAntmail(g, cx, cy, h); + else if (window.type.equals("GAMES")) renderGames(g, cx, cy); + else renderTrash(g, cx, cy); + } + + private void renderArchive(GuiGraphics g, int x, int y, int w, int h) { + List entries = List.of(); + if (Minecraft.getInstance().level != null && Minecraft.getInstance().level.getBlockEntity(position) instanceof ComputerBlockEntity computer) entries = ComputerGuideData.entriesFor(computer.diskIds()); + + ComputerGuideData.Entry selected = null; + if (session.archiveEntryId != null) { + selected = entries.stream().filter(entry -> entry.id().equals(session.archiveEntryId)).findFirst().orElse(null); + if (selected == null) session.archiveEntryId = null; } + + if (selected != null) { + g.drawString(font, Component.literal("< BACK // RECOVERED FILES"), x, y, GREEN, false); + g.fill(x, y + 16, x + w, y + 18, GREEN); + g.drawString(font, Component.translatable(selected.titleKey()), x, y + 27, GREEN, false); + if (!selected.subtitleKey().isBlank()) wrap(g, Component.translatable(selected.subtitleKey()), x, y + 43, w, PALE_GREEN); + int line = y + (selected.subtitleKey().isBlank() ? 43 : 59); + g.drawString(font, Component.literal(selected.type().toUpperCase() + " // " + selected.category().toUpperCase()), x, line, PALE_GREEN, false); + line += 16; + for (String descriptionKey : selected.descriptionKeys()) { + line = wrap(g, Component.translatable(descriptionKey), x, line, w, GREEN) + 3; + if (line > y + h - 36) break; + } + if (!selected.itemId().isBlank() && line < y + h - 24) { + line = wrap(g, "ITEM // " + selected.itemId(), x, line, w, PALE_GREEN) + 2; + } + if (!selected.structureId().isBlank() && line < y + h - 24) { + line = wrap(g, "STRUCTURE // " + selected.structureId(), x, line, w, PALE_GREEN) + 2; + } + if (!selected.dimensionId().isBlank() && line < y + h - 24) { + wrap(g, "DIMENSION // " + selected.dimensionId(), x, line, w, PALE_GREEN); + } + g.drawString(font, Component.literal("[ ESC ] BACK"), x, y + h - 12, PALE_GREEN, false); + return; + } + + g.drawString(font, Component.literal("RECOVERED FILES // " + entries.size()), x, y, GREEN, false); + if (entries.isEmpty()) { + g.drawString(font, Component.literal("NO ARCHIVE DATA"), x, y + 28, PALE_GREEN, false); + return; + } + int row = y + 18; + int visible = Math.max(1, (h - 34) / 25); + for (int i = 0; i < entries.size() && i < visible; i++) { + ComputerGuideData.Entry entry = entries.get(i); + int rowTop = row + i * 25; + g.drawString(font, Component.literal(String.format("FILE_%02d", i + 1)), x + 5, rowTop, PALE_GREEN, false); + String title = trimToWidth(Component.translatable(entry.titleKey()).getString(), w - 64); + g.drawString(font, Component.literal(title), x + 57, rowTop, GREEN, false); + g.drawString(font, Component.literal(entry.type().toUpperCase() + " // " + entry.category().toUpperCase()), x + 57, rowTop + 11, PALE_GREEN, false); + } + if (entries.size() > visible) g.drawString(font, Component.literal("SCROLLING NOT AVAILABLE // FIRST FILES SHOWN"), x, y + h - 12, PALE_GREEN, false); + else g.drawString(font, Component.literal("[ CLICK FILE TO OPEN ]"), x, y + h - 12, PALE_GREEN, false); + } + + private void renderFileExplorer(GuiGraphics g, int x, int y, int w, int h) { + var files = ComputerFileSystemClientState.get(position).files(); + g.drawString(font, Component.literal("FILE EXPLORER // " + session.fileExplorerDirectory), x, y, GREEN, false); + if (!session.fileExplorerDirectory.equals("/")) g.drawString(font, Component.literal("[ UP ]"), x + w - 40, y, GREEN, false); + g.drawString(font, Component.literal("TYPE PATH SIZE"), x, y + 18, PALE_GREEN, false); int line = y + 34; - for (String descriptionKey : entry.descriptionKeys()) { - for (FormattedCharSequence wrapped : this.font.split(Component.translatable(descriptionKey), 174)) { - graphics.drawString(this.font, wrapped, x, line, 0xFFB8FFB8, false); - line += 11; - if (line > y + 125) { - return; + for (String file : files) { + String[] fields = file.split("\\t", 3); + if (fields.length < 3 || !isDirectChild(fields[1], session.fileExplorerDirectory) || line > y + h - 20) continue; + g.drawString(font, Component.literal(trimToWidth(fields[0], 36)), x, line, PALE_GREEN, false); + g.drawString(font, Component.literal(trimToWidth(fields[1], 125)), x + 42, line, GREEN, false); + g.drawString(font, Component.literal(fields[2]), x + w - 28, line, PALE_GREEN, false); + line += 14; + } + if (line == y + 34) g.drawString(font, Component.literal("NO FILES // OPEN TERMINAL TO CREATE ONE"), x, y + 38, PALE_GREEN, false); + } + + private boolean isDirectChild(String path, String directory) { + if (path.equals(directory)) return false; + String prefix = directory.equals("/") ? "/" : directory + "/"; + if (!path.startsWith(prefix)) return false; + return path.indexOf('/', prefix.length()) < 0; + } + + private String parentDirectory(String path) { + if (path.equals("/")) return "/"; + int slash = path.lastIndexOf('/'); + return slash <= 0 ? "/" : path.substring(0, slash); + } + + private String trimToWidth(String value, int width) { + if (font.width(value) <= width) return value; + String ellipsis = "..."; + int end = value.length(); + while (end > 0 && font.width(value.substring(0, end) + ellipsis) > width) end--; + return value.substring(0, end) + ellipsis; + } + + private void renderSettings(GuiGraphics g, int x, int y, int h) { + g.drawString(font, Component.literal("SYSTEM STATUS"), x, y, GREEN, false); + g.drawString(font, Component.literal("CPU " + (System.currentTimeMillis() / 100 % 87 + 12) + "%"), x, y + 18, PALE_GREEN, false); + g.drawString(font, Component.literal("MEM " + (System.currentTimeMillis() / 250 % 42 + 31) + "%"), x, y + 32, PALE_GREEN, false); + g.drawString(font, Component.literal("WALLPAPER " + session.wallpaperId.substring(session.wallpaperId.indexOf(':') + 1).toUpperCase()), x, y + 46, PALE_GREEN, false); + g.drawString(font, Component.literal("[ CLICK TO CYCLE ]"), x, y + 60, GREEN, false); + g.drawString(font, Component.literal("PASSWORD CHANGE IN FULL BUILD"), x, y + 70, GREEN, false); + g.drawString(font, Component.literal("PHYSICAL DISKS"), x, y + 91, GREEN, false); + List disks = physicalDisks(); + if (disks.isEmpty()) { + g.drawString(font, Component.literal("NONE INSTALLED"), x, y + 108, PALE_GREEN, false); + } else { + for (int i = 0; i < disks.size() && i < 3; i++) { + ResourceLocation disk = disks.get(i); + boolean active = disk.equals(selectedDisk); + if (active) g.fill(x - 3, y + 105 + i * 17, x + 205, y + 120 + i * 17, 0xFF173817); + g.drawString(font, Component.literal(disk.toString()), x, y + 108 + i * 17, active ? GREEN : PALE_GREEN, false); + } + g.drawString(font, Component.literal("[ EJECT SELECTED ]"), x, y + 162, selectedDisk == null ? 0xFF4A754A : GREEN, false); + } + g.drawString(font, Component.literal("LOG OUT"), x, y + h - 17, GREEN, false); + } + + private void renderTerminal(GuiGraphics g, int x, int y, int h) { + var fileState = ComputerFileSystemClientState.get(position); + g.drawString(font, Component.literal("ANTINTOSH TERMINAL [READY]"), x, y, GREEN, false); + int line = y + 17; + for (String output : session.terminalOutput) { + line = wrap(g, output, x, line, 208, PALE_GREEN) + 1; + if (line > y + h - 32) break; + } + if (!fileState.error().isEmpty()) g.drawString(font, Component.literal("RESPONSE: " + fileState.error()), x, y + 16, PALE_GREEN, false); + g.drawString(font, Component.literal(session.terminalDirectory + "> " + session.terminalInput + "_"), x, y + h - 48, GREEN, false); + } + + private void renderPaint(GuiGraphics g, int x, int y, int w, int h) { + var fileState = ComputerFileSystemClientState.get(position); + if (!session.paintDirty && fileState.openedPath().endsWith(".antpaint") && !fileState.openedContents().isEmpty()) { + try { + AntPaintFile file = AntPaintFileCodec.decode(Base64.getDecoder().decode(fileState.openedContents())); + session.paintName = file.filename(); + session.paintCanvas = file.canvas().copy(); + } catch (IllegalArgumentException ignored) { + } + } + g.drawString(font, Component.literal("ANTPAINT // " + session.paintName + (session.paintDirty ? " *" : "")), x, y, GREEN, false); + g.drawString(font, Component.literal("NEW PENCIL ERASER FILL CLEAR UNDO REDO SAVE"), x, y + 14, PALE_GREEN, false); + int size = Math.min((w - 16) / AntPaintCanvas.WIDTH, (h - 54) / AntPaintCanvas.HEIGHT); + int cx = x + (w - size * AntPaintCanvas.WIDTH) / 2; + int cy = y + 30; + g.fill(cx - 1, cy - 1, cx + size * AntPaintCanvas.WIDTH + 1, cy + size * AntPaintCanvas.HEIGHT + 1, BLACK); + g.fill(cx, cy, cx + size * AntPaintCanvas.WIDTH, cy + size * AntPaintCanvas.HEIGHT, GREEN); + for (int py = 0; py < AntPaintCanvas.HEIGHT; py++) for (int px = 0; px < AntPaintCanvas.WIDTH; px++) if (!session.paintCanvas.get(px, py)) g.fill(cx + px * size, cy + py * size, cx + (px + 1) * size, cy + (py + 1) * size, BLACK); + g.drawString(font, Component.literal("TOOL " + session.paintTool.name() + " " + (session.paintDirty ? "MODIFIED" : "SAVED")), x, y + h - 16, PALE_GREEN, false); + } + + private void renderTextEditor(GuiGraphics g, int x, int y, int w, int h) { + var fileState = ComputerFileSystemClientState.get(position); + if (!fileState.openedPath().isEmpty() && !session.textDirty) { + session.textPath = fileState.openedPath(); + session.textContent = fileState.openedContents(); + } + g.drawString(font, Component.literal("ANTTEXT // " + session.textPath + (session.textDirty ? " *" : "")), x, y, GREEN, false); + g.drawString(font, Component.literal("NEW OPEN WRITE SAVE"), x, y + 18, PALE_GREEN, false); + g.fill(x + 4, y + 36, x + w - 4, y + h - 4, BLACK); + if (session.textListing) { + g.drawString(font, Component.literal("SELECT FILE"), x + 10, y + 46, GREEN, false); + int line = y + 62; + for (String file : fileState.files()) { + String[] fields = file.split("\\t", 3); + if (fields.length < 2 || line > y + h - 28) continue; + g.drawString(font, Component.literal(fields[1]), x + 10, line, PALE_GREEN, false); + line += 14; + } + if (fileState.files().isEmpty()) g.drawString(font, Component.literal("NO TEXT FILES"), x + 10, y + 62, PALE_GREEN, false); + } else { + wrap(g, session.textContent.isEmpty() ? "TYPE HERE..." : session.textContent, x + 10, y + 46, w - 28, GREEN); + } + g.drawString(font, Component.literal("[ CTRL+S ] SAVE [ ESC ] CLOSE"), x, y + h - 50, PALE_GREEN, false); + } + + private void renderAntmail(GuiGraphics g, int x, int y, int h) { + var result = AntmailClientState.get(position); + g.drawString(font, Component.literal("ANTMAIL // COMPUTER ADDRESS"), x, y, GREEN, false); + boolean unconfigured = result == null || result.address().isBlank(); + if (unconfigured) { + g.drawString(font, Component.literal("NO ADDRESS CONFIGURED"), x, y + 20, PALE_GREEN, false); + g.drawString(font, Component.literal("USERNAME // 3-16 CHARACTERS"), x, y + 40, GREEN, false); + g.fill(x, y + 56, x + 206, y + 79, BLACK); + box(g, x, y + 56, x + 206, y + 79, GREEN); + g.drawString(font, Component.literal(session.antmailUsername + "@ANTMAIL.COM"), x + 6, y + 63, GREEN, false); + g.drawString(font, Component.literal("[ ENTER ] REGISTER ADDRESS"), x, y + 88, GREEN, false); + } else { + g.drawString(font, Component.literal(result.address()), x, y + 20, PALE_GREEN, false); + g.drawString(font, Component.literal("[ INBOX ] [ SENT ] [ COMPOSE ]"), x, y + 40, GREEN, false); + if (session.antmailMode.equals("compose")) { + g.drawString(font, Component.literal("TO"), x, y + 62, GREEN, false); + g.drawString(font, Component.literal(session.antmailRecipient), x + 28, y + 62, PALE_GREEN, false); + g.drawString(font, Component.literal("SUBJECT"), x, y + 78, GREEN, false); + g.drawString(font, Component.literal(session.antmailSubject), x + 52, y + 78, PALE_GREEN, false); + g.drawString(font, Component.literal("BODY"), x, y + 94, GREEN, false); + wrap(g, session.antmailBody.isEmpty() ? "TYPE MESSAGE..." : session.antmailBody, x, y + 110, 208, PALE_GREEN); + g.drawString(font, Component.literal("[ " + (session.antmailAttachText ? "X" : " ") + " ] TEXT FILE [ " + (session.antmailAttachPaint ? "X" : " ") + " ] PAINTING"), x, y + 142, PALE_GREEN, false); + g.drawString(font, Component.literal("[ ENTER ] SEND MESSAGE"), x, y + h - 30, GREEN, false); + } else if (session.antmailMode.equals("message")) { + AntmailMessage message = selectedAntmailMessage(result); + if (message != null) { + g.drawString(font, Component.literal("< BACK // INBOX"), x, y + 40, GREEN, false); + g.drawString(font, Component.literal(message.sender().fullAddress()), x, y + 62, PALE_GREEN, false); + g.drawString(font, Component.literal(message.subject()), x, y + 78, GREEN, false); + wrap(g, message.body(), x, y + 98, 208, PALE_GREEN); + } + } else { + g.drawString(font, Component.literal("INBOX // " + mailboxMessages(result, false).size()), x, y + 62, GREEN, false); + int line = y + 80; + for (AntmailMessage message : mailboxMessages(result, false)) { + if (line > y + h - 38) break; + g.drawString(font, Component.literal((message.read() ? " " : "* ") + trimToWidth(message.subject(), 28)), x, line, message.read() ? PALE_GREEN : GREEN, false); + line += 14; } } - line += 5; } - if (entry.type().equals("item") && !entry.itemId().isEmpty()) { - try { - ItemStack stack = new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(ResourceLocation.parse(entry.itemId()))); - graphics.renderItem(stack, x + 130, y + 122); - } catch (Exception ignored) { - } + if (!session.antmailStatus.isEmpty()) g.drawString(font, Component.literal(session.antmailStatus), x, y + h - 30, PALE_GREEN, false); + g.drawString(font, Component.literal(result == null ? "WAITING FOR SERVER" : result.status() == 0 ? "DELIVERY READY" : "SERVER RESPONSE: " + result.detail()), x, y + h - 16, GREEN, false); + } + + private AntmailMailbox mailbox(AntmailResultPayload result) { + if (result == null || result.data().isBlank()) return null; + try { + return AntmailMailbox.fromTag(AntmailWire.decodeTag(result.data())); + } catch (RuntimeException ignored) { + return null; } } + private List mailboxMessages(AntmailResultPayload result, boolean sent) { + AntmailMailbox mailbox = mailbox(result); + if (mailbox == null) return List.of(); + return sent ? mailbox.sent() : mailbox.inbox(); + } + + private AntmailMessage selectedAntmailMessage(AntmailResultPayload result) { + List messages = mailboxMessages(result, session.antmailSent); + return session.antmailMessageIndex >= 0 && session.antmailMessageIndex < messages.size() ? messages.get(session.antmailMessageIndex) : null; + } + + private void renderGames(GuiGraphics g, int x, int y) { + g.drawString(font, Component.literal("INSTALLED GAMES"), x, y, GREEN, false); + if (activeWindow != null && activeWindow.gameOpen) { antFarm.render(g, font, x, y + 16, 214, 155); return; } + g.drawString(font, Component.literal(antFarm.isGameInstalled() ? "ANT FARM [ ENTER ]" : "ANT FARM [NOT INSTALLED]"), x, y + 22, PALE_GREEN, false); + g.drawString(font, Component.literal("GLIMMER MATCH"), x, y + 38, PALE_GREEN, false); + g.drawString(font, Component.literal("HFCS FACTORY"), x, y + 54, PALE_GREEN, false); + } + + private void renderTrash(GuiGraphics g, int x, int y) { + g.drawString(font, Component.literal("DROP A DISK HERE TO EJECT"), x, y, GREEN, false); + g.drawString(font, Component.literal("EJECTED FILES RETURN TO PLAYER"), x, y + 22, PALE_GREEN, false); + } + + private int wrap(GuiGraphics g, String text, int x, int y, int width, int color) { return wrap(g, Component.literal(text), x, y, width, color); } + + private int wrap(GuiGraphics g, Component text, int x, int y, int width, int color) { + for (var line : font.split(text, width)) { g.drawString(font, line, x, y, color, false); y += 11; } + return y; + } + + private void box(GuiGraphics g, int x1, int y1, int x2, int y2, int color) { + g.fill(x1, y1, x2, y1 + 2, color); + g.fill(x1, y2 - 2, x2, y2, color); + g.fill(x1, y1, x1 + 2, y2, color); + g.fill(x2 - 2, y1, x2, y2, color); + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - int left = (this.width - WIDTH) / 2; - int top = (this.height - HEIGHT) / 2; - for (int index = 0; index < visibleEntries.size() && index < 6; index++) { - if (isMouseOver(left + 12, top + 66 + index * 20, 98, 16, mouseX, mouseY)) { - selectedIndex = index; + float scale = uiScale(); + mouseX = (mouseX - width / 2.0F) / scale; + mouseY = (mouseY - height / 2.0F) / scale; + int l = -WIDTH / 2; + int t = -HEIGHT / 2; + if (!loggedIn) { + ComputerBlockEntity computer = computer(); + boolean setup = (accessResult != null && !accessResult.hasPassword()) || (computer != null && !computer.hasPassword()); + int formX = setup ? l + 110 : l + 242; + if (inside(formX, t + 181, setup ? 150 : 100, 24, mouseX, mouseY)) tryLogin(); + return true; + } + session.lastUse = System.currentTimeMillis(); + for (int i = session.windows.size() - 1; i >= 0; i--) { + Window window = session.windows.get(i); + if (window.minimized) continue; + int x = l + window.x; + int y = t + window.y; + int w = window.maximized ? WIDTH - 28 : window.width; + int h = window.maximized ? HEIGHT - 48 : window.height; + if (inside(x, y + 20, w, h - 20, mouseX, mouseY)) { + activeWindow = window; + if (window.type.equals("ARCHIVE")) { + List entries = ComputerGuideData.entriesFor(computer() == null ? List.of() : computer().diskIds()); + int archiveX = x + 8; + int archiveY = y + 28; + if (session.archiveEntryId != null) { + if (inside(archiveX, archiveY, w - 16, 20, mouseX, mouseY)) session.archiveEntryId = null; + } else { + int visible = Math.max(1, (h - 34) / 25); + int index = ((int) mouseY - (archiveY + 16)) / 25; + if (index >= 0 && index < entries.size() && index < visible && inside(archiveX, archiveY + 16 + index * 25, w - 16, 23, mouseX, mouseY)) session.archiveEntryId = entries.get(index).id(); + } + return true; + } + if (window.type.equals("FILES")) { + var files = ComputerFileSystemClientState.get(position).files(); + if (!session.fileExplorerDirectory.equals("/") && mouseY < y + 36 && mouseX > x + w - 48) { + session.fileExplorerDirectory = parentDirectory(session.fileExplorerDirectory); + return true; + } + int row = 0; + for (String file : files) { + String[] fields = file.split("\\t", 3); + if (fields.length < 3 || !isDirectChild(fields[1], session.fileExplorerDirectory)) continue; + if (row++ != ((int) mouseY - (y + 62)) / 14) continue; + if (fields[0].equals("DIRECTORY")) session.fileExplorerDirectory = fields[1]; + else if (fields[0].equals("TEXT")) { + ComputerNetworking.openFile(position, fields[1]); + open(fields[1].endsWith(".antpaint") ? "PAINT" : "TEXT"); + } + break; + } + return true; + } + if (window.type.equals("PAINT")) { + int contentX = x + 8; + int contentY = y + 48; + if (inside(contentX, contentY, w - 16, h - 54, mouseX, mouseY)) paintAt(mouseX, mouseY, contentX, contentY - 18, w - 16, h - 54, button == 1); + else if (mouseY < y + 66) { + if (mouseX > contentX + 190) savePaint(); + else paintTool((int) mouseX - contentX); + } + return true; + } + if (window.type.equals("TEXT")) { + if (mouseY < y + 66) { + int action = ((int) mouseX - (x + 8)) / 42; + if (action == 0) newText(); else if (action == 1) { session.textListing = true; ComputerNetworking.listFiles(position); } else if (action == 2) writeText(); else if (action == 3) saveText(); + } else if (session.textListing) { + var files = ComputerFileSystemClientState.get(position).files(); + int index = ((int) mouseY - (y + 62)) / 14; + if (index >= 0 && index < files.size()) { + String[] fields = files.get(index).split("\\t", 3); + if (fields.length >= 2) { + session.textListing = false; + ComputerNetworking.openFile(position, fields[1]); + } + } + } else session.textFocused = true; + return true; + } + if (window.type.equals("ANTMAIL")) { + var result = AntmailClientState.get(position); + if (result == null || result.address().isBlank()) { + if (inside(x + 8, y + 84, 214, 25, mouseX, mouseY)) { + session.antmailFocused = true; + return true; + } + if (inside(x + 8, y + 112, 214, 24, mouseX, mouseY)) { + setupAntmail(); + return true; + } + } else if (mouseY < y + 72) { + if (mouseX < x + 70) { session.antmailMode = "inbox"; session.antmailSent = false; session.antmailMessageIndex = -1; } + else if (mouseX < x + 125) { session.antmailMode = "sent"; session.antmailSent = true; session.antmailMessageIndex = -1; } + else { session.antmailMode = "compose"; session.antmailFocused = true; session.antmailField = 1; } + } else if (session.antmailMode.equals("inbox") || session.antmailMode.equals("sent")) { + int index = ((int) mouseY - (y + 108)) / 14; + List messages = mailboxMessages(result, session.antmailSent); + if (index >= 0 && index < messages.size()) { + session.antmailMessageIndex = index; + session.antmailMode = "message"; + AntmailNetworking.markRead(position, messages.get(index).id()); + } + } else if (session.antmailMode.equals("compose")) { + if (mouseY >= y + 160 && mouseY < y + 184) { + session.antmailAttachText = mouseX < x + 112; + session.antmailAttachPaint = mouseX >= x + 112; + return true; + } + if (mouseY < y + 94) { + session.antmailField = mouseY < y + 78 ? 1 : 2; + session.antmailFocused = true; + } else if (mouseY < y + 150) { + session.antmailField = 3; + session.antmailFocused = true; + } + } + return true; + } + if (window.type.equals("GAMES") && mouseY > y + 35 && !window.gameOpen && antFarm.isGameInstalled()) { window.gameOpen = true; activeWindow = window; return true; } + if (window.type.equals("TERMINAL")) { session.terminalFocused = true; return true; } + } + if (window.type.equals("SETTINGS") && inside(x + 8, y + 105, 205, 51, mouseX, mouseY)) { + List disks = physicalDisks(); + int index = ((int) mouseY - (y + 105)) / 17; + if (index >= 0 && index < disks.size() && index < 3) { + selectedDisk = disks.get(index); + return true; + } + } + if (window.type.equals("SETTINGS") && inside(x + 8, y + 70, 205, 25, mouseX, mouseY)) { + if (session.wallpapers.size() > 1) { + int current = session.wallpapers.indexOf(session.wallpaperId); + String next = session.wallpapers.get((current + 1 + session.wallpapers.size()) % session.wallpapers.size()); + session.wallpaperId = next; + ComputerNetworking.selectWallpaper(position, ResourceLocation.parse(next)); + } + return true; + } + if (window.type.equals("SETTINGS") && inside(x + 8, y + 155, 205, 25, mouseX, mouseY)) { + ejectSelected(); + return true; + } + if (inside(x, y + 20, w, h - 20, mouseX, mouseY)) return true; + if (inside(x, y, w, 20, mouseX, mouseY)) { + if (inside(x + w - 18, y, 18, 20, mouseX, mouseY)) session.windows.remove(i); + else if (inside(x + w - 48, y, 16, 20, mouseX, mouseY)) window.maximized = !window.maximized; + else if (inside(x + w - 63, y, 16, 20, mouseX, mouseY)) window.minimized = true; + else { dragging = window; dragX = (int) mouseX - x; dragY = (int) mouseY - y; session.windows.remove(i); session.windows.add(window); } return true; } } - return super.mouseClicked(mouseX, mouseY, button); - } - - private static boolean isMouseOver(int x, int y, int width, int height, double mouseX, double mouseY) { - return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + for (int i = 0; i < ICONS.length; i++) { + int x = l + 24 + i % 4 * 94; + int y = t + 48 + i / 4 * 76; + if (inside(x - 6, y - 6, 76, 57, mouseX, mouseY)) { open(ICONS[i]); return true; } + } + List disks = physicalDisks(); + for (int i = 0; i < disks.size() && i < 3; i++) { + int x = l + 24 + i % 4 * 94; + int y = t + 200 + i / 4 * 54; + if (inside(x - 6, y - 6, 76, 48, mouseX, mouseY)) { + draggedDisk = disks.get(i).toString(); + selectedDisk = disks.get(i); + return true; + } + } + return true; } @Override - public boolean isPauseScreen() { - return false; + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + float scale = uiScale(); + mouseX = (mouseX - width / 2.0F) / scale; + mouseY = (mouseY - height / 2.0F) / scale; + if (dragging != null) { + int l = -WIDTH / 2; + int t = -HEIGHT / 2; + dragging.x = Math.max(12, Math.min(WIDTH - dragging.renderWidth - 12, (int) mouseX - l - this.dragX)); + dragging.y = Math.max(34, Math.min(HEIGHT - dragging.renderHeight - 12, (int) mouseY - t - this.dragY)); + } + if (activeWindow != null && activeWindow.type.equals("PAINT") && button != 0) { + int l = -WIDTH / 2; + int t = -HEIGHT / 2; + int x = l + activeWindow.x + 8; + int y = t + activeWindow.y + 48; + paintAt(mouseX, mouseY, x, y - 18, activeWindow.renderWidth - 16, activeWindow.renderHeight - 54, button == 1); + } + return true; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + float scale = uiScale(); + mouseX = (mouseX - width / 2.0F) / scale; + mouseY = (mouseY - height / 2.0F) / scale; + if (draggedDisk != null) { + int l = -WIDTH / 2; + int t = -HEIGHT / 2; + int trashX = l + 24 + (6 % 4) * 94; + int trashY = t + 48 + (6 / 4) * 76; + if (inside(trashX - 6, trashY - 6, 76, 57, mouseX, mouseY)) ejectSelected(); + draggedDisk = null; + } + dragging = null; + return true; + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + if (!loggedIn && codePoint >= 32) { + if (confirmingPassword) { + if (confirmation.length() < 32) confirmation += Character.toUpperCase(codePoint); + } else if (password.length() < 32) { + password += Character.toUpperCase(codePoint); + } + } + if (loggedIn && activeWindow != null) { + if (activeWindow.type.equals("TEXT") && session.textFocused && codePoint >= 32) { session.textContent += codePoint; session.textDirty = true; return true; } + if (activeWindow.type.equals("TERMINAL") && session.terminalFocused && codePoint >= 32) { session.terminalInput += codePoint; return true; } + if (activeWindow.type.equals("ANTMAIL") && session.antmailFocused && codePoint >= 32) { + if (session.antmailMode.equals("compose")) { + if (session.antmailField == 1 && session.antmailRecipient.length() < 64) session.antmailRecipient += codePoint; + else if (session.antmailField == 2 && session.antmailSubject.length() < 64) session.antmailSubject += codePoint; + else if (session.antmailField == 3 && session.antmailBody.length() < 16384) session.antmailBody += codePoint; + } else if (Character.isLetterOrDigit(codePoint) || codePoint == '_') session.antmailUsername += Character.toLowerCase(codePoint); + return true; + } + } + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!loggedIn) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { onClose(); return true; } + if (keyCode == 259) { + if (confirmingPassword && !confirmation.isEmpty()) confirmation = confirmation.substring(0, confirmation.length() - 1); + else if (!password.isEmpty()) password = password.substring(0, password.length() - 1); + } + else if (keyCode == 257 || keyCode == 335) tryLogin(); + return true; + } + if (activeWindow != null && activeWindow.type.equals("GAMES") && activeWindow.gameOpen) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { activeWindow.gameOpen = false; return true; } + if (antFarm.keyPressed(keyCode)) return true; + } + if (activeWindow != null && activeWindow.type.equals("TERMINAL") && session.terminalFocused) { + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { if (!session.terminalInput.isEmpty()) session.terminalInput = session.terminalInput.substring(0, session.terminalInput.length() - 1); return true; } + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { executeTerminal(); return true; } + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { session.terminalFocused = false; return true; } + } + + if (activeWindow != null && activeWindow.type.equals("ANTMAIL") && session.antmailFocused) { + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { + if (session.antmailMode.equals("compose")) { + if (session.antmailField == 1 && !session.antmailRecipient.isEmpty()) session.antmailRecipient = session.antmailRecipient.substring(0, session.antmailRecipient.length() - 1); + else if (session.antmailField == 2 && !session.antmailSubject.isEmpty()) session.antmailSubject = session.antmailSubject.substring(0, session.antmailSubject.length() - 1); + else if (session.antmailField == 3 && !session.antmailBody.isEmpty()) session.antmailBody = session.antmailBody.substring(0, session.antmailBody.length() - 1); + } else if (!session.antmailUsername.isEmpty()) session.antmailUsername = session.antmailUsername.substring(0, session.antmailUsername.length() - 1); + return true; + } + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + if (session.antmailMode.equals("compose")) sendAntmail(); else setupAntmail(); + return true; + } + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { session.antmailFocused = false; return true; } + } + if (activeWindow != null && activeWindow.type.equals("TEXT") && session.textFocused) { + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { if (!session.textContent.isEmpty()) { session.textContent = session.textContent.substring(0, session.textContent.length() - 1); session.textDirty = true; } return true; } + if (keyCode == GLFW.GLFW_KEY_ENTER) { session.textContent += "\n"; session.textDirty = true; return true; } + if (keyCode == GLFW.GLFW_KEY_S && hasControl(modifiers)) { saveText(); return true; } + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { session.textFocused = false; return true; } + } + if (activeWindow != null && activeWindow.type.equals("ARCHIVE") && session.archiveEntryId != null && keyCode == GLFW.GLFW_KEY_ESCAPE) { + session.archiveEntryId = null; + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + private void tryLogin() { + if (password.isBlank()) return; + if (accessResult != null && !accessResult.hasPassword()) { + if (!confirmingPassword) { + confirmingPassword = true; + confirmation = ""; + loginMessage = "RETYPE PASSWORD // CONFIRMATION REQUIRED"; + loginMessageTicks = 100; + return; + } + if (!password.equals(confirmation)) { + password = ""; + confirmation = ""; + confirmingPassword = false; + loginMessage = "SETUP ERROR // PASSWORDS DO NOT MATCH"; + loginMessageTicks = 100; + return; + } + ComputerNetworking.setup(position, password); + loginMessage = "INITIALIZING ACCESS // WAITING"; + } else { + ComputerNetworking.login(position, password); + loginMessage = "AUTHENTICATING // WAITING"; + } + loginMessageTicks = 40; + password = ""; + confirmation = ""; + confirmingPassword = false; + } + + private void executeTerminal() { + String input = session.terminalInput.trim(); + if (input.isEmpty()) return; + session.terminalOutput.add(session.terminalDirectory + "> " + input); + TerminalCommandParser.ParseResult parsed = TerminalCommandParser.parse(input); + if (!parsed.isSuccess()) session.terminalOutput.add(parsed.error()); + else { + String command = parsed.command().name(); + List args = parsed.command().arguments(); + if (command.equals("pwd")) session.terminalOutput.add(session.terminalDirectory); + else if (command.equals("clear")) session.terminalOutput.clear(); + else if (command.equals("ls")) ComputerNetworking.listFiles(position); + else if (command.equals("open") && args.size() == 1) ComputerNetworking.openFile(position, args.get(0)); + else if (command.equals("touch") && args.size() == 1) ComputerNetworking.createFile(position, args.get(0), ""); + else if (command.equals("write") && args.size() >= 1) ComputerNetworking.saveFile(position, args.get(0), args.size() > 1 ? args.get(1) : ""); + else if (command.equals("cd") && args.size() == 1) { + String path = args.get(0).startsWith("/") ? args.get(0) : session.terminalDirectory + "/" + args.get(0); + session.terminalDirectory = ComputerFileSystem.normalize(path); + } + else if (command.equals("help")) session.terminalOutput.add("HELP CLEAR PWD LS CD MKDIR TOUCH CAT WRITE RM RMDIR MV OPEN"); + else session.terminalOutput.add("COMMAND NOT RECOGNIZED"); + } + while (session.terminalOutput.size() > 40) session.terminalOutput.remove(0); + session.terminalInput = ""; + } + + private void setupAntmail() { + if (!AntmailAddress.isValidUsername(session.antmailUsername)) { + session.antmailStatus = "SETUP ERROR // USE 3-16 LOWERCASE LETTERS, NUMBERS, OR _"; + session.antmailFocused = true; + return; + } + AntmailNetworking.setup(position, session.antmailUsername); + session.antmailStatus = "REGISTERING ADDRESS // WAITING"; + session.antmailFocused = false; + } + + private void sendAntmail() { + if (!AntmailAddress.isValidAddress(session.antmailRecipient)) { + session.antmailStatus = "SEND ERROR // INVALID RECIPIENT"; + return; + } + if (session.antmailSubject.isBlank() || session.antmailBody.isBlank()) { + session.antmailStatus = "SEND ERROR // SUBJECT AND BODY REQUIRED"; + return; + } + List attachments = new ArrayList<>(); + if (session.antmailAttachText && !session.textContent.isEmpty()) attachments.add(new AntmailAttachment.TextFile(session.textPath.substring(session.textPath.lastIndexOf('/') + 1), session.textContent)); + if (session.antmailAttachPaint) { + byte[] pixels = new byte[AntPaintCanvas.WIDTH * AntPaintCanvas.HEIGHT]; + for (int py = 0; py < AntPaintCanvas.HEIGHT; py++) for (int px = 0; px < AntPaintCanvas.WIDTH; px++) pixels[py * AntPaintCanvas.WIDTH + px] = (byte) (session.paintCanvas.get(px, py) ? 1 : 0); + attachments.add(new AntmailAttachment.PaintImage(session.paintName, AntPaintCanvas.WIDTH, AntPaintCanvas.HEIGHT, pixels)); + } + AntmailNetworking.send(position, session.antmailRecipient, session.antmailSubject, session.antmailBody, attachments); + session.antmailStatus = "TRANSMITTING MESSAGE // WAITING"; + session.antmailMode = "inbox"; + session.antmailFocused = false; + session.antmailRecipient = ""; + session.antmailSubject = ""; + session.antmailBody = ""; + session.antmailAttachText = false; + session.antmailAttachPaint = false; + } + + private void newText() { + session.textPath = "/documents/untitled.txt"; + session.textContent = ""; + session.textDirty = true; + session.textListing = false; + session.textFocused = true; + } + + private void writeText() { + ComputerNetworking.createFile(position, session.textPath, session.textContent); + session.textDirty = false; + } + + private void saveText() { + ComputerNetworking.saveFile(position, session.textPath, session.textContent); + session.textDirty = false; + } + + private void savePaint() { + String path = "/pictures/" + session.paintName; + if (!path.endsWith(".antpaint")) path += ".antpaint"; + session.paintName = path.substring(path.lastIndexOf('/') + 1); + AntPaintFile file = new AntPaintFile(UUID.randomUUID().toString(), session.paintName, session.paintCanvas.copy(), 0L, 0L); + String encoded = Base64.getEncoder().encodeToString(AntPaintFileCodec.encode(file)); + String savedPath = path; + boolean exists = ComputerFileSystemClientState.get(position).files().stream().anyMatch(entry -> entry.contains("\t" + savedPath + "\t")); + if (exists) ComputerNetworking.saveFile(position, savedPath, encoded); else ComputerNetworking.createFile(position, savedPath, encoded); + session.paintDirty = false; + } + + private void paintTool(int offset) { + session.paintTool = offset < 52 ? AntPaintTool.PENCIL : offset < 108 ? AntPaintTool.ERASER : AntPaintTool.FILL; + } + + private void paintAt(double mouseX, double mouseY, int x, int y, int w, int h, boolean draw) { + int size = Math.min(w / AntPaintCanvas.WIDTH, h / AntPaintCanvas.HEIGHT); + if (size <= 0) return; + int canvasX = x + (w - size * AntPaintCanvas.WIDTH) / 2; + int px = (int) ((mouseX - canvasX) / size); + int py = (int) ((mouseY - y) / size); + if (px < 0 || py < 0 || px >= AntPaintCanvas.WIDTH || py >= AntPaintCanvas.HEIGHT) return; + session.paintHistory.record(session.paintCanvas); + if (session.paintTool == AntPaintTool.PENCIL) session.paintCanvas.erase(px, py); + else if (session.paintTool == AntPaintTool.ERASER) session.paintCanvas.draw(px, py); + else session.paintCanvas.floodFill(px, py, session.paintTool == AntPaintTool.ERASER); + session.paintDirty = true; + } + + private boolean hasControl(int modifiers) { + return (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; + } + + private ComputerBlockEntity computer() { + if (Minecraft.getInstance().level == null) return null; + return Minecraft.getInstance().level.getBlockEntity(position) instanceof ComputerBlockEntity computer ? computer : null; + } + + private List physicalDisks() { + ComputerBlockEntity computer = computer(); + if (computer == null) return List.of(); + return computer.diskIds().stream() + .filter(id -> !id.equals(ResourceLocation.fromNamespaceAndPath("antarchy", "introduction"))) + .toList(); + } + + private void ejectSelected() { + if (selectedDisk == null) return; + ComputerNetworking.eject(position, selectedDisk); + loginMessage = "EJECTING DISK // WAITING"; + loginMessageTicks = 100; + selectedDisk = null; + } + + private void open(String type) { + for (Window window : session.windows) if (window.type.equals(type)) { window.minimized = false; session.windows.remove(window); session.windows.add(window); return; } + session.windows.add(new Window(type, TITLES.get(type), 112 + session.windows.size() * 12, 52 + session.windows.size() * 10)); + if (type.equals("ANTMAIL")) AntmailNetworking.requestState(position); + } + + private static boolean inside(int x, int y, int w, int h, double mx, double my) { return mx >= x && mx < x + w && my >= y && my < y + h; } + + @Override + public boolean isPauseScreen() { return false; } + + @Override + public void onClose() { + ComputerNetworking.close(position); + super.onClose(); + } + + private static final class Session { + private boolean authenticated; + private long lastUse; + private final List windows = new ArrayList<>(); + private final List terminalOutput = new ArrayList<>(List.of("ANTINTOSH TERMINAL [READY]", "TYPE HELP FOR COMMANDS")); + private String terminalInput = ""; + private String terminalDirectory = "/"; + private boolean terminalFocused; + private String textPath = "/documents/untitled.txt"; + private String textContent = ""; + private boolean textDirty; + private boolean textFocused; + private boolean textListing; + private ResourceLocation archiveEntryId; + private AntPaintCanvas paintCanvas = new AntPaintCanvas(); + private final AntPaintHistory paintHistory = new AntPaintHistory(); + private AntPaintTool paintTool = AntPaintTool.PENCIL; + private String paintName = "UNTITLED.ANTPAINT"; + private boolean paintDirty; + private String antmailUsername = ""; + private String antmailStatus = ""; + private boolean antmailFocused; + private String antmailMode = "inbox"; + private String antmailRecipient = ""; + private String antmailSubject = ""; + private String antmailBody = ""; + private int antmailField; + private boolean antmailSent; + private int antmailMessageIndex = -1; + private boolean antmailAttachText; + private boolean antmailAttachPaint; + private String fileExplorerDirectory = "/"; + private String wallpaperId = ComputerDesktopState.DEFAULT_WALLPAPER.toString(); + private List wallpapers = List.of(ComputerDesktopState.DEFAULT_WALLPAPER.toString()); + private boolean desktopRequested; + private int bootTicks = -1; + } + + private static final class Window { + private final String type; + private final String title; + private final int width = 230; + private final int height = 210; + private int x; + private int y; + private int renderWidth = width; + private int renderHeight = height; + private boolean minimized; + private boolean maximized; + private boolean gameOpen; + + private Window(String type, String title, int x, int y) { this.type = type; this.title = title; this.x = x; this.y = y; } } } diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/ComputerDesktopState.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/ComputerDesktopState.java new file mode 100644 index 00000000..4356263a --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/ComputerDesktopState.java @@ -0,0 +1,196 @@ +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 unlockedWallpaperIds = new LinkedHashSet<>(); + private final Set installedProgramIds = new LinkedHashSet<>(); + private final Map iconPositions = new LinkedHashMap<>(); + + public ResourceLocation selectedWallpaper() { + return selectedWallpaper; + } + + public Set unlockedWallpaperIds() { + return Collections.unmodifiableSet(new LinkedHashSet<>(unlockedWallpaperIds)); + } + + public Set installedProgramIds() { + return Collections.unmodifiableSet(new LinkedHashSet<>(installedProgramIds)); + } + + public Map 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 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(); + selectedWallpaper = parseId(tag.getString(SELECTED_WALLPAPER_TAG)); + loadIds(tag.getList(UNLOCKED_WALLPAPERS_TAG, 8), unlockedWallpaperIds, MAX_UNLOCKED_WALLPAPERS); + 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 ids) { + ListTag list = new ListTag(); + for (ResourceLocation id : ids) { + list.add(StringTag.valueOf(id.toString())); + } + return list; + } + + private static void loadIds(ListTag list, Set 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) { + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/ComputerFileSystem.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/ComputerFileSystem.java new file mode 100644 index 00000000..28683555 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/ComputerFileSystem.java @@ -0,0 +1,174 @@ +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 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 list() { + return List.copyOf(nodes.values()); + } + + public List listTextFiles() { + return nodes.values().stream().filter(file -> file.type() == ComputerFile.Type.TEXT).toList(); + } + + 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) return Result.failure("not_text"); + 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 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) 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)) { + 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, 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 new ComputerFile(path, Type.TEXT, contents); } + public enum Type { DIRECTORY, TEXT, IMAGE } + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintCanvas.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintCanvas.java new file mode 100644 index 00000000..1e1ff637 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintCanvas.java @@ -0,0 +1,114 @@ +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 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; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFile.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFile.java new file mode 100644 index 00000000..3c528337 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFile.java @@ -0,0 +1,26 @@ +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); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFileCodec.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFileCodec.java new file mode 100644 index 00000000..960c1acf --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFileCodec.java @@ -0,0 +1,86 @@ +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); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFilename.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFilename.java new file mode 100644 index 00000000..3beefbcb --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintFilename.java @@ -0,0 +1,38 @@ +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; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintHistory.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintHistory.java new file mode 100644 index 00000000..9e7b0b58 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintHistory.java @@ -0,0 +1,47 @@ +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 undo = new ArrayDeque<>(); + private final ArrayDeque 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(); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintRequests.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintRequests.java new file mode 100644 index 00000000..a2830775 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintRequests.java @@ -0,0 +1,35 @@ +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"); + } + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintResults.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintResults.java new file mode 100644 index 00000000..d5ca9707 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintResults.java @@ -0,0 +1,40 @@ +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 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) { + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintTool.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintTool.java new file mode 100644 index 00000000..68c6088e --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/paint/AntPaintTool.java @@ -0,0 +1,7 @@ +package com.craisinlord.antarchy.content.computer.paint; + +public enum AntPaintTool { + PENCIL, + ERASER, + FILL +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommand.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommand.java new file mode 100644 index 00000000..b4a79272 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommand.java @@ -0,0 +1,11 @@ +package com.craisinlord.antarchy.content.computer.terminal; + +import java.util.List; + +public record TerminalCommand(String name, List arguments, String rawInput) { + public TerminalCommand { + name = name.toLowerCase(java.util.Locale.ROOT); + arguments = List.copyOf(arguments); + rawInput = rawInput == null ? "" : rawInput; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommandParser.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommandParser.java new file mode 100644 index 00000000..330f82fa --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommandParser.java @@ -0,0 +1,79 @@ +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 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); + } + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommandService.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommandService.java new file mode 100644 index 00000000..1fbe556b --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalCommandService.java @@ -0,0 +1,207 @@ +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 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 // CREATE DIRECTORY"); + case "touch" -> TerminalResult.success(workingDirectory, "TOUCH // CREATE EMPTY TEXT FILE"); + case "cat" -> TerminalResult.success(workingDirectory, "CAT // PRINT TEXT FILE"); + case "write" -> TerminalResult.success(workingDirectory, "WRITE [TEXT] // REPLACE TEXT FILE CONTENTS"); + case "rm" -> TerminalResult.success(workingDirectory, "RM // DELETE TEXT FILE"); + case "rmdir" -> TerminalResult.success(workingDirectory, "RMDIR // DELETE EMPTY DIRECTORY"); + case "mv" -> TerminalResult.success(workingDirectory, "MV // MOVE OR RENAME"); + case "open" -> TerminalResult.success(workingDirectory, "OPEN // 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 entry = find(fileSystem, path, workingDirectory); + if (entry.isEmpty()) { + return missing(workingDirectory, path); + } + if (!entry.get().isDirectory()) { + return TerminalResult.success(workingDirectory, entry.get().name()); + } + List 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 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 "); + } + 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 "); + } + 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 "); + } + Optional 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 [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 "); + } + 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 "); + } + 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 "); + } + 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 "); + } + Optional 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 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 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"); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalFileSystem.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalFileSystem.java new file mode 100644 index 00000000..c1ff3701 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalFileSystem.java @@ -0,0 +1,39 @@ +package com.craisinlord.antarchy.content.computer.terminal; + +import java.util.List; +import java.util.Optional; + +public interface TerminalFileSystem { + String ROOT_PATH = "/"; + + Optional find(String path, String workingDirectory); + + List list(String path, String workingDirectory); + + boolean createDirectory(String path, String workingDirectory); + + boolean createFile(String path, String workingDirectory); + + Optional 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 + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalResult.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalResult.java new file mode 100644 index 00000000..02304226 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/computer/terminal/TerminalResult.java @@ -0,0 +1,33 @@ +package com.craisinlord.antarchy.content.computer.terminal; + +import java.util.List; + +public record TerminalResult(Status status, List 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 + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java index 3e85e15f..94a7a9eb 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/OctopusBombEntity.java @@ -75,11 +75,9 @@ public class OctopusBombEntity extends Monster implements GeoEntity { private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); - // Squid-style wander private float wanderDx, wanderDy, wanderDz; private int wanderRetargetTicks; - // Combat private int attackCooldown; private int actionTicks; private int lastAttackState; @@ -88,7 +86,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { private float orbitDirection = 1.0F; private int inkSprayCooldown; - // Kraken spawn flag private boolean spawnedByKraken; // Explosive impact (fired from the R.P.O. Launcher) @@ -364,10 +361,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { this.hasImpulse = true; } - // ------------------------------------------------------------------------- - // Squid-style wander - // ------------------------------------------------------------------------- - private void tickWanderMovement() { if (--this.wanderRetargetTicks <= 0) { this.wanderRetargetTicks = 40 + this.random.nextInt(40); @@ -386,10 +379,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { this.setDeltaMovement(this.getDeltaMovement().add(this.wanderDx, dy, this.wanderDz)); } - // ------------------------------------------------------------------------- - // Ink spray - // ------------------------------------------------------------------------- - private boolean hasHitExplosiveTarget() { AABB box = this.getBoundingBox(); return !this.level().getEntitiesOfClass(LivingEntity.class, box, @@ -445,10 +434,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity { serverLevel.sendParticles(OCTOPUS_BOMB_INK_PARTICLE, cx, cy, cz, 30, 3.5D, 1.8D, 3.5D, 0.02D); } - // ------------------------------------------------------------------------- - // Combat - // ------------------------------------------------------------------------- - private void tickPursuitMovement(LivingEntity target) { if (this.strafeRetargetTicks-- <= 0) { this.strafeRetargetTicks = 10 + this.random.nextInt(12); diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/ant/BaseAntEntity.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/ant/BaseAntEntity.java index d4e01dd8..c760ab23 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/ant/BaseAntEntity.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/ant/BaseAntEntity.java @@ -95,6 +95,7 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity { private static final int NEST_SEARCH_INTERVAL_TICKS = 80; private static final int FOOD_NEST_SEARCH_INTERVAL_TICKS = 20; private static final int CHEST_SEARCH_INTERVAL_TICKS = 20; + private static final int CHEST_VALIDATION_INTERVAL_TICKS = 10; private static final int SHARED_CHEST_MEMORY_TICKS = 120; private static final double FOOD_CHEST_SEARCH_RADIUS = 20.0D; private static final int GROUND_FOOD_CACHE_INTERVAL_TICKS = 20; @@ -136,6 +137,10 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity { private BlockPos targetFoodChestPos; private int nextFoodChestSearchTick; @Nullable + private BlockPos cachedFoodChestValidationPos; + private boolean cachedFoodChestValidationResult; + private int nextFoodChestValidationTick; + @Nullable private BlockPos recentFoodChestPos; private int recentFoodChestExpireTick; @Nullable @@ -749,17 +754,36 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity { } } + if (bestPos != null) { + this.cachedFoodChestValidationPos = bestPos; + this.cachedFoodChestValidationResult = true; + this.nextFoodChestValidationTick = this.tickCount + CHEST_VALIDATION_INTERVAL_TICKS; + } return bestPos; } private boolean isValidFoodChest(BlockPos chestPos) { + if (this.cachedFoodChestValidationPos != null + && this.cachedFoodChestValidationPos.equals(chestPos) + && this.tickCount < this.nextFoodChestValidationTick) { + return this.cachedFoodChestValidationResult; + } + BlockEntity blockEntity = this.level().getBlockEntity(chestPos); - return blockEntity instanceof ChestBlockEntity chestBlockEntity && this.findStealableFoodSlot(chestBlockEntity) >= 0; + boolean valid = blockEntity instanceof ChestBlockEntity chestBlockEntity + && this.findStealableFoodSlot(chestBlockEntity) >= 0; + this.cachedFoodChestValidationPos = chestPos.immutable(); + this.cachedFoodChestValidationResult = valid; + this.nextFoodChestValidationTick = this.tickCount + CHEST_VALIDATION_INTERVAL_TICKS; + return valid; } private void clearFoodChestTarget() { this.targetFoodChestPos = null; this.nextFoodChestSearchTick = 0; + this.cachedFoodChestValidationPos = null; + this.cachedFoodChestValidationResult = false; + this.nextFoodChestValidationTick = 0; } private boolean stealFoodFromChest(BlockPos chestPos) { @@ -1390,13 +1414,16 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity { return null; } - if (leader.targetFoodChestPos != null && this.isValidFoodChest(leader.targetFoodChestPos)) { + if (leader.targetFoodChestPos != null + && leader.cachedFoodChestValidationPos != null + && leader.cachedFoodChestValidationPos.equals(leader.targetFoodChestPos) + && leader.cachedFoodChestValidationResult + && leader.tickCount < leader.nextFoodChestValidationTick) { return leader.targetFoodChestPos; } if (leader.recentFoodChestPos != null - && leader.tickCount <= leader.recentFoodChestExpireTick - && this.isValidFoodChest(leader.recentFoodChestPos)) { + && leader.tickCount <= leader.recentFoodChestExpireTick) { return leader.recentFoodChestPos; } diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/royal/KingEntity.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/royal/KingEntity.java index a5504795..850f90c2 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/royal/KingEntity.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/entity/royal/KingEntity.java @@ -994,7 +994,7 @@ public class KingEntity extends RoyalBossEntity { } @Override public void onActive(int elapsedTicks) { KingEntity.this.raiseIceSpikes(level, target); - KingEntity.this.playRoyalSound(AntarchySoundEvents.KING_BEAM_SHOOT.get(), 0.8F); + KingEntity.this.playRoyalSound(AntarchySoundEvents.KING_ICE_SPIKES.get(), 0.8F); } })) return; this.iceSpikeCooldownTicks = AntarchySettings.kingIceSpikeCooldownTicks(); diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/guide/ComputerGuideData.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/guide/ComputerGuideData.java index 3b5ba093..666fd006 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/guide/ComputerGuideData.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/guide/ComputerGuideData.java @@ -18,6 +18,7 @@ import java.util.Map; public final class ComputerGuideData extends SimplePreparableReloadListener { private static final ComputerGuideData INSTANCE = new ComputerGuideData(); + private static final ResourceLocation INTRODUCTION = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction"); private static volatile Map entries = Map.of(); private static volatile Map disks = Map.of(); @@ -39,6 +40,10 @@ public final class ComputerGuideData extends SimplePreparableReloadListener entriesFor(List diskIds) { Map result = new LinkedHashMap<>(); for (ResourceLocation diskId : diskIds) { + if (INTRODUCTION.equals(diskId)) { + result.put(INTRODUCTION, new Entry(INTRODUCTION, "article", "general", "guide.antarchy.entry.introduction.title", "guide.antarchy.entry.introduction.subtitle", List.of("guide.antarchy.entry.introduction.description"), "", "", "")); + continue; + } Disk disk = disks.get(diskId); if (disk == null) { continue; diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java index 24de9c9d..094859a7 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/BigBerthaItem.java @@ -176,7 +176,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem { return; } - // Toreterror jump tracking if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) { player.fallDistance = 0.0F; // prevent fall damage during the crash-down boolean launched = readInt(stack, TORETERROR_LAUNCHED_TAG) == 1; @@ -197,7 +196,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem { } } - // Tick client shake counter if (level.isClientSide && clientShakeTicks > 0) { clientShakeTicks--; } diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/ComputerItem.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/ComputerItem.java index 4515573d..bbd4d438 100644 --- a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/ComputerItem.java +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/item/ComputerItem.java @@ -18,7 +18,7 @@ import java.util.function.Consumer; public final class ComputerItem extends BlockItem implements GeoItem { private static final ResourceLocation MODEL = ResourceLocation.fromNamespaceAndPath("antarchy", "geo/computer.geo.json"); - private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("antarchy", "textures/block/computer/computer.png"); + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("antarchy", "textures/item/computer.png"); private static final ResourceLocation ANIMATION = ResourceLocation.fromNamespaceAndPath("antarchy", "animations/computer.animation.json"); private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); @@ -29,7 +29,7 @@ public final class ComputerItem extends BlockItem implements GeoItem { @Override public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { - controllers.add(new AnimationController<>(this, "state", state -> state.setAndContinue(RawAnimation.begin().thenLoop("off_state")))); + controllers.add(new AnimationController<>(this, "state", state -> state.setAndContinue(RawAnimation.begin().thenLoop("on_state")))); } @Override diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailNetworking.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailNetworking.java new file mode 100644 index 00000000..83e2d6e9 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailNetworking.java @@ -0,0 +1,40 @@ +package com.craisinlord.antarchy.content.network; + +import com.craisinlord.antarchy.content.antmail.AntmailAttachment; +import com.craisinlord.antarchy.content.antmail.AntmailWire; +import net.minecraft.core.BlockPos; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public final class AntmailNetworking { + private static Consumer sender = payload -> { }; + + private AntmailNetworking() { + } + + public static void setSender(Consumer sender) { + AntmailNetworking.sender = sender; + } + + public static void setup(BlockPos pos, String username) { + sender.accept(new AntmailSetupPayload(pos, username)); + } + + public static void requestState(BlockPos pos) { + sender.accept(new AntmailStateRequestPayload(pos)); + } + + public static void send(BlockPos pos, String recipient, String subject, String body, List attachments) { + sender.accept(new AntmailSendPayload(pos, recipient, subject, body, AntmailWire.encodeAttachments(attachments))); + } + + public static void markRead(BlockPos pos, UUID messageId) { + sender.accept(new AntmailReadPayload(pos, messageId.toString(), true)); + } + + public static void markUnread(BlockPos pos, UUID messageId) { + sender.accept(new AntmailReadPayload(pos, messageId.toString(), false)); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailPayloadTypes.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailPayloadTypes.java new file mode 100644 index 00000000..9ed63a81 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailPayloadTypes.java @@ -0,0 +1,20 @@ +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> all() { + return List.of( + AntmailSetupPayload.TYPE, + AntmailStateRequestPayload.TYPE, + AntmailSendPayload.TYPE, + AntmailReadPayload.TYPE, + AntmailResultPayload.TYPE + ); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailReadPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailReadPayload.java new file mode 100644 index 00000000..017e7f6c --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailReadPayload.java @@ -0,0 +1,15 @@ +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 TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_read")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailReadPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailReadPayload::messageId, ByteBufCodecs.BOOL, AntmailReadPayload::read, AntmailReadPayload::new); + @Override public Type type() { return TYPE; } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailResultPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailResultPayload.java new file mode 100644 index 00000000..17c281a1 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailResultPayload.java @@ -0,0 +1,15 @@ +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) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_result")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailResultPayload::pos, ByteBufCodecs.VAR_INT, AntmailResultPayload::status, ByteBufCodecs.stringUtf8(64), AntmailResultPayload::address, ByteBufCodecs.stringUtf8(64), AntmailResultPayload::messageId, ByteBufCodecs.stringUtf8(128), AntmailResultPayload::detail, ByteBufCodecs.stringUtf8(65536), AntmailResultPayload::data, AntmailResultPayload::new); + @Override public Type type() { return TYPE; } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailSendPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailSendPayload.java new file mode 100644 index 00000000..a34221b2 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailSendPayload.java @@ -0,0 +1,15 @@ +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 TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_send")); + public static final StreamCodec 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 type() { return TYPE; } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailServerHandler.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailServerHandler.java new file mode 100644 index 00000000..4ce459de --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailServerHandler.java @@ -0,0 +1,127 @@ +package com.craisinlord.antarchy.content.network; + +import com.craisinlord.antarchy.content.antmail.AntmailAddress; +import com.craisinlord.antarchy.content.antmail.AntmailAttachment; +import com.craisinlord.antarchy.content.antmail.AntmailDeliveryResult; +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 resultSender = (player, payload) -> { }; + + private AntmailServerHandler() { + } + + public static void setResultSender(BiConsumer sender) { + resultSender = sender; + } + + public static void handle(ServerPlayer player, AntmailSetupPayload payload) { + ComputerBlockEntity computer = computer(player, payload.pos()); + if (computer == null || !computer.canUseFileSystem(player)) { + result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", ""); + return; + } + AntmailServerData data = AntmailServerData.access(player.server); + AntmailServerData.Registration registration = data.register(player.server, player.serverLevel(), payload.pos(), payload.username()); + result(player, payload.pos(), registration.valid() ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), registration.valid() ? registration.address().fullAddress() : "", "", registration.reason(), ""); + } + + public static void handle(ServerPlayer player, AntmailStateRequestPayload 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); + result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "", AntmailWire.encodeTag(mailbox.toTag())); + } + + 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 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(), recipient.fullAddress(), message.id().toString(), delivery.detail(), ""); + } 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.setDirty(); + result(player, payload.pos(), changed ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), changed ? "" : "not_found", ""); + } catch (IllegalArgumentException exception) { + result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.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) { + resultSender.accept(player, new AntmailResultPayload(pos, status, address, messageId, detail, data)); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailSetupPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailSetupPayload.java new file mode 100644 index 00000000..8c93d6ce --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailSetupPayload.java @@ -0,0 +1,15 @@ +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 TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_setup")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSetupPayload::pos, ByteBufCodecs.stringUtf8(32), AntmailSetupPayload::username, AntmailSetupPayload::new); + @Override public Type type() { return TYPE; } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailStateRequestPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailStateRequestPayload.java new file mode 100644 index 00000000..ae918088 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/AntmailStateRequestPayload.java @@ -0,0 +1,14 @@ +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.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; + +public record AntmailStateRequestPayload(BlockPos pos) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_state_request")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailStateRequestPayload::pos, AntmailStateRequestPayload::new); + @Override public Type type() { return TYPE; } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessHandler.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessHandler.java new file mode 100644 index 00000000..1a2c6b9b --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessHandler.java @@ -0,0 +1,212 @@ +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.antmail.AntmailWire; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntity; + +import java.util.function.BiConsumer; + +public final class ComputerAccessHandler { + private static BiConsumer resultSender = (player, result) -> { + }; + + private ComputerAccessHandler() { + } + + public static void setResultSender(BiConsumer 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.DESKTOP_STATE -> desktopState(player, computer, payload); + case ComputerAccessPayload.DESKTOP_WALLPAPER -> desktopWallpaper(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); + 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); + } + } + + 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())) { + 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) { + sendFile(player, payload, false, "", "not_text"); + } 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 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 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)); + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessPayload.java new file mode 100644 index 00000000..bcc96371 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessPayload.java @@ -0,0 +1,41 @@ +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 Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access")); + public static final StreamCodec 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 type() { + return TYPE; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessResultPayload.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessResultPayload.java new file mode 100644 index 00000000..10059c69 --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerAccessResultPayload.java @@ -0,0 +1,36 @@ +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 TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access_result")); + public static final StreamCodec 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 type() { + return TYPE; + } +} diff --git a/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerNetworking.java b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerNetworking.java new file mode 100644 index 00000000..019e24db --- /dev/null +++ b/common-1.21.1/src/main/java/com/craisinlord/antarchy/content/network/ComputerNetworking.java @@ -0,0 +1,70 @@ +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 sender = payload -> { + }; + + private ComputerNetworking() { + } + + public static void setSender(Consumer 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 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())); + } +} diff --git a/common-1.21.1/src/main/resources/assets/antarchy/blockstates/computer.json b/common-1.21.1/src/main/resources/assets/antarchy/blockstates/computer.json index d05e6bdb..0cce1c7f 100644 --- a/common-1.21.1/src/main/resources/assets/antarchy/blockstates/computer.json +++ b/common-1.21.1/src/main/resources/assets/antarchy/blockstates/computer.json @@ -1,7 +1,9 @@ { - "variants": { - "": { - "model": "antarchy:block/computer" + "multipart": [ + { + "apply": { + "model": "antarchy:block/computer" + } } - } + ] } diff --git a/common-1.21.1/src/main/resources/assets/antarchy/lang/en_us.json b/common-1.21.1/src/main/resources/assets/antarchy/lang/en_us.json index b599b4c7..3e60ab18 100644 --- a/common-1.21.1/src/main/resources/assets/antarchy/lang/en_us.json +++ b/common-1.21.1/src/main/resources/assets/antarchy/lang/en_us.json @@ -1053,6 +1053,7 @@ "subtitles.antarchy.king_death": "The King falls", "subtitles.antarchy.king_beam_shoot": "The King fires a beam", "subtitles.antarchy.king_fireball_shoot": "The King breathes fire", + "subtitles.antarchy.king_ice_spikes": "The King summons ice spikes", "subtitles.antarchy.king_decree_cast": "The King pronounces a Decree", "subtitles.antarchy.king_decree": "The King issues a Decree", "subtitles.antarchy.king_success": "The King acknowledges compliance", @@ -1444,7 +1445,7 @@ ,"block.antarchy.computer": "Computer" ,"item.antarchy.computer": "Computer" ,"item.antarchy.floppy_disk": "Floppy Disk" - ,"screen.antarchy.computer": "ANTARCHY ARCHIVE" + ,"screen.antarchy.computer": "ANTINTOSH OS" ,"guide.antarchy.category.general": "GENERAL" ,"guide.antarchy.category.elythia": "ELYTHIA" ,"guide.antarchy.category.thoraxis": "THORAXIS" @@ -1467,4 +1468,21 @@ ,"guide.antarchy.entry.high_fructose_corn_syrup.title": "High Fructose Corn Syrup" ,"guide.antarchy.entry.high_fructose_corn_syrup.subtitle": "Nutritional and operational notes" ,"guide.antarchy.entry.high_fructose_corn_syrup.description": "A sweet substance of uncertain strategic value. Doctoral enthusiasm surrounding it is statistically suspicious." + ,"screen.antarchy.computer.first_use.title": "INITIAL PASSWORD SETUP" + ,"screen.antarchy.computer.first_use.prompt": "CREATE A PASSWORD TO CONTINUE" + ,"screen.antarchy.computer.first_use.confirm": "CONFIRM PASSWORD" + ,"screen.antarchy.computer.busy": "COMPUTER IN USE" + ,"screen.antarchy.computer.eject_denied": "ACCESS DENIED: SETTINGS REQUIRED" + ,"screen.antarchy.computer.ejected": "DISK EJECTED" + ,"antintosh.antarchy.desktop.archive": "ARCHIVE" + ,"antintosh.antarchy.desktop.settings": "SETTINGS" + ,"antintosh.antarchy.desktop.terminal": "TERMINAL" + ,"antintosh.antarchy.desktop.antpaint": "ANTPAINT" + ,"antintosh.antarchy.desktop.antmail": "ANTMAIL" + ,"antintosh.antarchy.desktop.text_editor": "TEXT EDITOR" + ,"antintosh.antarchy.antmail.compose": "COMPOSE MESSAGE" + ,"antintosh.antarchy.antmail.attach_text": "ATTACH TEXT FILE" + ,"antintosh.antarchy.antmail.attach_image": "ATTACH ANTPAINT IMAGE" + ,"antintosh.antarchy.antmail.save_attachment": "SAVE ATTACHMENT" + ,"antintosh.antarchy.antmail.address": "COMPUTER ADDRESS" } diff --git a/common-1.21.1/src/main/resources/assets/antarchy/models/item/computer.json b/common-1.21.1/src/main/resources/assets/antarchy/models/item/computer.json index 744a6940..ab467129 100644 --- a/common-1.21.1/src/main/resources/assets/antarchy/models/item/computer.json +++ b/common-1.21.1/src/main/resources/assets/antarchy/models/item/computer.json @@ -1,3 +1,3 @@ { - "parent": "antarchy:block/computer" + "parent": "builtin/entity" } diff --git a/common-1.21.1/src/main/resources/assets/antarchy/sounds.json b/common-1.21.1/src/main/resources/assets/antarchy/sounds.json index 73086df5..dd7b67a2 100644 --- a/common-1.21.1/src/main/resources/assets/antarchy/sounds.json +++ b/common-1.21.1/src/main/resources/assets/antarchy/sounds.json @@ -2850,6 +2850,14 @@ } ] }, + "king_ice_spikes": { + "subtitle": "subtitles.antarchy.king_ice_spikes", + "sounds": [ + { + "name": "antarchy:mob/king/ice_spikes" + } + ] + }, "king_decree_cast": { "subtitle": "subtitles.antarchy.king_decree_cast", "sounds": [ diff --git a/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/death.ogg b/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/death.ogg index d3a7526a..fd337098 100644 Binary files a/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/death.ogg and b/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/death.ogg differ diff --git a/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/fireball_shoot.ogg b/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/fireball_shoot.ogg index 8a3cd9d3..d0899b53 100644 Binary files a/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/fireball_shoot.ogg and b/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/fireball_shoot.ogg differ diff --git a/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/ice_spikes.ogg b/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/ice_spikes.ogg new file mode 100644 index 00000000..9b027e40 Binary files /dev/null and b/common-1.21.1/src/main/resources/assets/antarchy/sounds/mob/king/ice_spikes.ogg differ diff --git a/common-1.21.1/src/main/resources/assets/antarchy/textures/item/computer.png b/common-1.21.1/src/main/resources/assets/antarchy/textures/item/computer.png new file mode 100644 index 00000000..8ebda321 Binary files /dev/null and b/common-1.21.1/src/main/resources/assets/antarchy/textures/item/computer.png differ diff --git a/common-1.21.1/src/main/resources/data/antarchy/computer/disk/chronosphere.json b/common-1.21.1/src/main/resources/data/antarchy/computer/disk/chronosphere.json index 68ebe8e2..29c1e104 100644 --- a/common-1.21.1/src/main/resources/data/antarchy/computer/disk/chronosphere.json +++ b/common-1.21.1/src/main/resources/data/antarchy/computer/disk/chronosphere.json @@ -1,5 +1,5 @@ { - "category": "general", + "category": "thoraxis", "title": "guide.antarchy.disk.chronosphere.title", "entries": ["antarchy:chronosphere"] } diff --git a/common-1.21.1/src/main/resources/data/antarchy/computer/disk/introduction.json b/common-1.21.1/src/main/resources/data/antarchy/computer/disk/introduction.json deleted file mode 100644 index b9179276..00000000 --- a/common-1.21.1/src/main/resources/data/antarchy/computer/disk/introduction.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "category": "general", - "title": "guide.antarchy.disk.introduction.title", - "entries": ["antarchy:introduction"] -} diff --git a/common-1.21.1/src/main/resources/data/antarchy/computer/entry/chronosphere.json b/common-1.21.1/src/main/resources/data/antarchy/computer/entry/chronosphere.json index 601eecfa..75fc77bd 100644 --- a/common-1.21.1/src/main/resources/data/antarchy/computer/entry/chronosphere.json +++ b/common-1.21.1/src/main/resources/data/antarchy/computer/entry/chronosphere.json @@ -1,6 +1,6 @@ { "type": "item", - "category": "general", + "category": "thoraxis", "title": "guide.antarchy.entry.chronosphere.title", "subtitle": "guide.antarchy.entry.chronosphere.subtitle", "description": ["guide.antarchy.entry.chronosphere.description"], diff --git a/common-1.21.1/src/main/resources/data/antarchy/worldgen/biome/biowart_caves.json b/common-1.21.1/src/main/resources/data/antarchy/worldgen/biome/biowart_caves.json index b8afe375..8f155f43 100644 --- a/common-1.21.1/src/main/resources/data/antarchy/worldgen/biome/biowart_caves.json +++ b/common-1.21.1/src/main/resources/data/antarchy/worldgen/biome/biowart_caves.json @@ -60,8 +60,7 @@ "antarchy:biowart_patch", "antarchy:cavaryn_creepvine", "antarchy:cavaryn_hanging_creeproots" - ], - [] + ] ], "creature_spawn_probability": 0.98, "spawners": { diff --git a/common-1.21.1/src/test/resources/antarchy/computer/fixtures/README.md b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/README.md new file mode 100644 index 00000000..2301fa00 --- /dev/null +++ b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/README.md @@ -0,0 +1,5 @@ +# Computer contract fixtures + +These fixtures describe expected computer state transitions for validation tooling. They are not runtime datapack content and are not loaded by the game. + +The repository currently has no computer-specific test harness. The fixtures cover first placement, duplicate disk consumption, protection of the built-in introduction module, authenticated ejection through Settings or Trash, rejection without authentication, and physical disk drops when the block is broken. diff --git a/common-1.21.1/src/test/resources/antarchy/computer/fixtures/built_in_introduction_protection.json b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/built_in_introduction_protection.json new file mode 100644 index 00000000..6383bd49 --- /dev/null +++ b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/built_in_introduction_protection.json @@ -0,0 +1,10 @@ +{ + "case": "built_in_introduction_protection", + "physical_disk_ids": ["antarchy:chronosphere"], + "built_in_modules": ["antarchy:introduction"], + "settings_ejection_candidates": ["antarchy:chronosphere"], + "expected_dropped_disk_ids": ["antarchy:chronosphere"], + "expected_ejectable_ids": ["antarchy:chronosphere"], + "expected_introduction_dropped": false, + "expected_introduction_ejectable": false +} diff --git a/common-1.21.1/src/test/resources/antarchy/computer/fixtures/duplicate_disk.json b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/duplicate_disk.json new file mode 100644 index 00000000..35ac5ed2 --- /dev/null +++ b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/duplicate_disk.json @@ -0,0 +1,8 @@ +{ + "case": "duplicate_disk", + "initial_physical_disk_ids": ["antarchy:chronosphere"], + "inserted_disk_id": "antarchy:chronosphere", + "expected_physical_disk_ids": ["antarchy:chronosphere"], + "expected_item_consumed": true, + "expected_archive_change": false +} diff --git a/common-1.21.1/src/test/resources/antarchy/computer/fixtures/ejection_and_drop.json b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/ejection_and_drop.json new file mode 100644 index 00000000..6d6540a6 --- /dev/null +++ b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/ejection_and_drop.json @@ -0,0 +1,7 @@ +{ + "case": "ejection_and_drop", + "physical_disk_ids": ["antarchy:chronosphere", "antarchy:toreterror_arena"], + "ejection": {"authenticated": true, "route": "settings_or_trash", "selected_disk_id": "antarchy:chronosphere", "expected_returned_item_id": "antarchy:floppy_disk", "expected_remaining_physical_disk_ids": ["antarchy:toreterror_arena"]}, + "drop_after_break": {"expected_dropped_disk_ids": ["antarchy:toreterror_arena"], "expected_introduction_dropped": false}, + "rejection": {"authenticated": false, "expected_ejection": false} +} diff --git a/common-1.21.1/src/test/resources/antarchy/computer/fixtures/newly_placed.json b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/newly_placed.json new file mode 100644 index 00000000..500d082c --- /dev/null +++ b/common-1.21.1/src/test/resources/antarchy/computer/fixtures/newly_placed.json @@ -0,0 +1,8 @@ +{ + "case": "newly_placed", + "physical_disk_ids": [], + "built_in_modules": ["antarchy:introduction"], + "password": {"state": "unset"}, + "session": {"authenticated": false, "screen_occupied": false}, + "expectations": ["introduction_visible_before_login", "first_use_password_setup_required", "introduction_not_ejectable", "introduction_not_dropped"] +} diff --git a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/AntarchyFabricContent.java b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/AntarchyFabricContent.java index 363c9a65..c9644656 100644 --- a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/AntarchyFabricContent.java +++ b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/AntarchyFabricContent.java @@ -321,6 +321,7 @@ public final class AntarchyFabricContent { AntarchyFabricSounds.QUEEN_ROAR, AntarchyFabricSounds.KING_BEAM_SHOOT, AntarchyFabricSounds.KING_FIREBALL_SHOOT, + AntarchyFabricSounds.KING_ICE_SPIKES, AntarchyFabricSounds.KING_DECREE_CAST, AntarchyFabricSounds.QUEEN_BEAM_SHOOT, AntarchyFabricSounds.QUEEN_BEAM_START, diff --git a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/client/AntarchyClothConfigScreenBuilder.java b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/client/AntarchyClothConfigScreenBuilder.java index 901b1527..0853b5c5 100644 --- a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/client/AntarchyClothConfigScreenBuilder.java +++ b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/client/AntarchyClothConfigScreenBuilder.java @@ -33,11 +33,6 @@ public final class AntarchyClothConfigScreenBuilder { ConfigBuilder builder = ConfigBuilder.create() .setParentScreen(parent) .setTitle(Component.translatable("antarchy.config.title")); - // Entry save consumers (below) already update the live AntarchySettings fields the - // instant the player edits a widget, matching how the game normally behaves. This runs - // last, once, when the player clicks "Save" — it's what actually writes those changes - // back to the three JSON files, since AntarchyConfigModuleFabric only reads+writes them - // at startup otherwise. builder.setSavingRunnable(AntarchyConfigModuleFabric::persist); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); @@ -74,11 +69,6 @@ public final class AntarchyClothConfigScreenBuilder { .setSaveConsumer(binding::setValue) .build()); } - // String-typed bindings back the ResourceKey dimension fields (see - // AntarchyConfigModuleFabric.isSupported) and are intentionally skipped here — they're - // structural dimension identity, not a tuning knob, and Cloth Config has no natural - // widget for them. Every other supported type is exactly what's already persisted to - // JSON today, so nothing shown here can drift from what the JSON files contain. } private static String displayName(ConfigSection section) { diff --git a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricClientNetworking.java b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricClientNetworking.java index 5bfa7cf5..8151b5b8 100644 --- a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricClientNetworking.java +++ b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricClientNetworking.java @@ -36,6 +36,11 @@ public final class AntarchyFabricClientNetworking { } public static void register() { + ClientPlayNetworking.registerGlobalReceiver(AntmailResultPayload.TYPE, (payload, context) -> + context.client().execute(() -> com.craisinlord.antarchy.content.client.AntmailClientState.update(payload))); + ClientPlayNetworking.registerGlobalReceiver(ComputerAccessResultPayload.TYPE, (payload, context) -> + context.client().execute(() -> com.craisinlord.antarchy.content.client.ComputerAccessClientState.update(payload))); + com.craisinlord.antarchy.content.network.ComputerNetworking.setSender(ServerPlayNetworking::send); ClientPlayNetworking.registerGlobalReceiver(GravityStatePayload.TYPE, (payload, context) -> context.client().execute(() -> AntarchyFabricNetworking.handleGravityState(context.player(), payload))); ClientPlayNetworking.registerGlobalReceiver(BloodglassStatePayload.TYPE, (payload, context) -> diff --git a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricNetworking.java b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricNetworking.java index 3b6af52d..97a3cf2f 100644 --- a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricNetworking.java +++ b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/network/AntarchyFabricNetworking.java @@ -49,6 +49,9 @@ public final class AntarchyFabricNetworking { public static void register() { registerPayloadTypes(); registerServerReceivers(); + com.craisinlord.antarchy.content.network.ComputerAccessHandler.setResultSender(ServerPlayNetworking::send); + com.craisinlord.antarchy.content.network.AntmailServerHandler.setResultSender(ServerPlayNetworking::send); + com.craisinlord.antarchy.content.network.ComputerNetworking.setSender(ServerPlayNetworking::send); } public static void bootstrapMultipartCommon() { @@ -72,6 +75,8 @@ public final class AntarchyFabricNetworking { } private static void registerPayloadTypes() { + PayloadTypeRegistry.playS2C().register(ComputerAccessResultPayload.TYPE, ComputerAccessResultPayload.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(AntmailResultPayload.TYPE, AntmailResultPayload.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(GravityStatePayload.TYPE, GravityStatePayload.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(BloodglassStatePayload.TYPE, BloodglassStatePayload.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(TigerEyeCamouflageStatePayload.TYPE, TigerEyeCamouflageStatePayload.STREAM_CODEC); @@ -104,9 +109,24 @@ public final class AntarchyFabricNetworking { PayloadTypeRegistry.playC2S().register(HerculesBeetleMountedChargePayload.TYPE, HerculesBeetleMountedChargePayload.STREAM_CODEC); PayloadTypeRegistry.playC2S().register(MultipartAttackPayload.TYPE, MultipartAttackPayload.STREAM_CODEC); PayloadTypeRegistry.playC2S().register(MultipartInteractPayload.TYPE, MultipartInteractPayload.STREAM_CODEC); + PayloadTypeRegistry.playC2S().register(ComputerAccessPayload.TYPE, ComputerAccessPayload.STREAM_CODEC); + PayloadTypeRegistry.playC2S().register(AntmailSetupPayload.TYPE, AntmailSetupPayload.STREAM_CODEC); + PayloadTypeRegistry.playC2S().register(AntmailStateRequestPayload.TYPE, AntmailStateRequestPayload.STREAM_CODEC); + PayloadTypeRegistry.playC2S().register(AntmailSendPayload.TYPE, AntmailSendPayload.STREAM_CODEC); + PayloadTypeRegistry.playC2S().register(AntmailReadPayload.TYPE, AntmailReadPayload.STREAM_CODEC); } private static void registerServerReceivers() { + ServerPlayNetworking.registerGlobalReceiver(ComputerAccessPayload.TYPE, (payload, context) -> + context.server().execute(() -> ComputerAccessHandler.handle(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(AntmailSetupPayload.TYPE, (payload, context) -> + context.server().execute(() -> AntmailServerHandler.handle(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(AntmailStateRequestPayload.TYPE, (payload, context) -> + context.server().execute(() -> AntmailServerHandler.handle(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(AntmailSendPayload.TYPE, (payload, context) -> + context.server().execute(() -> AntmailServerHandler.handle(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(AntmailReadPayload.TYPE, (payload, context) -> + context.server().execute(() -> AntmailServerHandler.handle(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(GravityGunPrimaryPayload.TYPE, (payload, context) -> context.server().execute(() -> handleGravityGunPrimary(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(PortalGunPrimaryPayload.TYPE, (payload, context) -> diff --git a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricItems.java b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricItems.java index 80d3b06c..e3062ff3 100644 --- a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricItems.java +++ b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricItems.java @@ -1548,9 +1548,6 @@ public final class AntarchyFabricItems { () -> new EyeOfTheStormItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.EPIC))); - // Temporarily disabled: keeping the code but not registering the item for now. - // public static final DeferredItem MINERS_DREAM = ITEMS.register("miners_dream", - // () -> new MinersDreamItem(new Item.Properties().stacksTo(1).rarity(Rarity.RARE))); public static final DeferredItem SQUIDZOOKA = ITEMS.register("squidzooka", diff --git a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricSounds.java b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricSounds.java index 9ce98be8..3a49ea45 100644 --- a/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricSounds.java +++ b/fabric-1.21.1/src/main/java/com/craisinlord/antarchy/fabric/registry/AntarchyFabricSounds.java @@ -542,6 +542,7 @@ public final class AntarchyFabricSounds { public static final DeferredHolder KING_DEATH = registerSoundEvent("king_death"); public static final DeferredHolder KING_BEAM_SHOOT = registerSoundEvent("king_beam_shoot"); public static final DeferredHolder KING_FIREBALL_SHOOT = registerSoundEvent("king_fireball_shoot"); + public static final DeferredHolder KING_ICE_SPIKES = registerSoundEvent("king_ice_spikes"); public static final DeferredHolder KING_DECREE_CAST = registerSoundEvent("king_decree_cast"); public static final DeferredHolder KING_DECREE = registerSoundEvent("king_decree"); public static final DeferredHolder KING_SUCCESS = registerSoundEvent("king_success"); diff --git a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/AntarchyNeoforge.java b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/AntarchyNeoforge.java index 7410a6c5..8597e4b7 100644 --- a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/AntarchyNeoforge.java +++ b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/AntarchyNeoforge.java @@ -216,6 +216,7 @@ public class AntarchyNeoforge { AntarchyNeoforgeSounds.QUEEN_ROAR, AntarchyNeoforgeSounds.KING_BEAM_SHOOT, AntarchyNeoforgeSounds.KING_FIREBALL_SHOOT, + AntarchyNeoforgeSounds.KING_ICE_SPIKES, AntarchyNeoforgeSounds.KING_DECREE_CAST, AntarchyNeoforgeSounds.QUEEN_BEAM_SHOOT, AntarchyNeoforgeSounds.QUEEN_BEAM_START, @@ -287,6 +288,12 @@ public class AntarchyNeoforge { AntarchyNeoforgeCreativeModeTabs.register(modEventBus); AntarchyNeoforgeEntityAttributes.register(modEventBus); AntarchyNeoforgePayloadHandlers.register(modEventBus); + com.craisinlord.antarchy.content.network.ComputerNetworking.setSender(payload -> + net.neoforged.neoforge.network.PacketDistributor.sendToServer(payload)); + com.craisinlord.antarchy.content.network.ComputerAccessHandler.setResultSender((player, payload) -> + net.neoforged.neoforge.network.PacketDistributor.sendToPlayer(player, payload)); + com.craisinlord.antarchy.content.network.AntmailServerHandler.setResultSender((player, payload) -> + net.neoforged.neoforge.network.PacketDistributor.sendToPlayer(player, payload)); Antarchy.init(); } diff --git a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeItems.java b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeItems.java index 05116ff3..cfbc793b 100644 --- a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeItems.java +++ b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeItems.java @@ -1020,9 +1020,6 @@ public final class AntarchyNeoforgeItems { )); public static final DeferredItem EYE_OF_THE_STORM = ITEMS.register("eye_of_the_storm", () -> new EyeOfTheStormItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.EPIC))); - // Temporarily disabled: keeping the code but not registering the item for now. - // public static final DeferredItem MINERS_DREAM = ITEMS.register("miners_dream", - // () -> new MinersDreamItem(new Item.Properties().stacksTo(1).rarity(Rarity.RARE))); public static final DeferredItem SQUIDZOOKA = ITEMS.register("squidzooka", () -> new SquidzookaItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.RARE))); public static final DeferredItem RPO_LAUNCHER = ITEMS.register("rpo_launcher", diff --git a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgePayloadHandlers.java b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgePayloadHandlers.java index a93253ee..30a52e9f 100644 --- a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgePayloadHandlers.java +++ b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgePayloadHandlers.java @@ -1,7 +1,17 @@ package com.craisinlord.antarchy.neoforge.registry; import com.craisinlord.antarchy.content.network.BloodCrystalKatanaTrailPayload; +import com.craisinlord.antarchy.content.network.ComputerAccessHandler; +import com.craisinlord.antarchy.content.network.ComputerAccessPayload; +import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload; import com.craisinlord.antarchy.content.network.ToggleTigerEyeCamouflagePayload; +import com.craisinlord.antarchy.content.network.AntmailPayloadTypes; +import com.craisinlord.antarchy.content.network.AntmailSetupPayload; +import com.craisinlord.antarchy.content.network.AntmailStateRequestPayload; +import com.craisinlord.antarchy.content.network.AntmailSendPayload; +import com.craisinlord.antarchy.content.network.AntmailReadPayload; +import com.craisinlord.antarchy.content.network.AntmailResultPayload; +import com.craisinlord.antarchy.content.network.AntmailServerHandler; import com.craisinlord.antarchy.content.item.TigerEyeArmorUtil; import com.craisinlord.antarchy.content.tigereye.TigerEyeCamouflageController; import com.craisinlord.antarchy.neoforge.network.AntarchyGravityNetworking; @@ -23,6 +33,38 @@ public class AntarchyNeoforgePayloadHandlers { static void registerPayloadHandlers(RegisterPayloadHandlersEvent event) { var registrar = event.registrar("1"); + registrar.playToServer( + ComputerAccessPayload.TYPE, + ComputerAccessPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> { + if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer player) { + ComputerAccessHandler.handle(player, payload); + } + }) + ); + registrar.playToServer(AntmailSetupPayload.TYPE, AntmailSetupPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> { + if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer player) AntmailServerHandler.handle(player, payload); + })); + registrar.playToServer(AntmailStateRequestPayload.TYPE, AntmailStateRequestPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> { + if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer player) AntmailServerHandler.handle(player, payload); + })); + registrar.playToServer(AntmailSendPayload.TYPE, AntmailSendPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> { + if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer player) AntmailServerHandler.handle(player, payload); + })); + registrar.playToServer(AntmailReadPayload.TYPE, AntmailReadPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> { + if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer player) AntmailServerHandler.handle(player, payload); + })); + registrar.playToClient(AntmailResultPayload.TYPE, AntmailResultPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> com.craisinlord.antarchy.content.client.AntmailClientState.update(payload))); + registrar.playToClient( + ComputerAccessResultPayload.TYPE, + ComputerAccessResultPayload.STREAM_CODEC, + (payload, ctx) -> ctx.enqueueWork(() -> com.craisinlord.antarchy.content.client.ComputerAccessClientState.update(payload)) + ); AntarchyGravityNetworking.register(registrar); AntarchyTimeDilationNetworking.register(registrar); BrutalflyElytraNetworking.register(registrar); diff --git a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeSounds.java b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeSounds.java index 81990f7c..9dadc728 100644 --- a/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeSounds.java +++ b/neoforge-1.21.1/src/main/java/com/craisinlord/antarchy/neoforge/registry/AntarchyNeoforgeSounds.java @@ -225,6 +225,7 @@ public final class AntarchyNeoforgeSounds { public static final DeferredHolder KING_DEATH = registerSoundEvent("king_death"); public static final DeferredHolder KING_BEAM_SHOOT = registerSoundEvent("king_beam_shoot"); public static final DeferredHolder KING_FIREBALL_SHOOT = registerSoundEvent("king_fireball_shoot"); + public static final DeferredHolder KING_ICE_SPIKES = registerSoundEvent("king_ice_spikes"); public static final DeferredHolder KING_DECREE_CAST = registerSoundEvent("king_decree_cast"); public static final DeferredHolder KING_DECREE = registerSoundEvent("king_decree"); public static final DeferredHolder KING_SUCCESS = registerSoundEvent("king_success");