a few puter fixes

This commit is contained in:
craisinlord committed 2026-09-14 19:02:18 -05:00
1 parent 0c400ef404
commit 3300fa8f2a
86 files changed
+4480 -200

No files matched your search

@@ -76,11 +76,9 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this);
// Squid-style wander
private float wanderDx, wanderDy, wanderDz; private float wanderDx, wanderDy, wanderDz;
private int wanderRetargetTicks; private int wanderRetargetTicks;
// Combat
private int attackCooldown; private int attackCooldown;
private int actionTicks; private int actionTicks;
private int lastAttackState; private int lastAttackState;
@@ -89,7 +87,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
private float orbitDirection = 1.0F; private float orbitDirection = 1.0F;
private int inkSprayCooldown; private int inkSprayCooldown;
// Kraken spawn flag
private boolean spawnedByKraken; private boolean spawnedByKraken;
// Explosive impact (fired from the R.P.O. Launcher) // Explosive impact (fired from the R.P.O. Launcher)
@@ -366,10 +363,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
this.hasImpulse = true; this.hasImpulse = true;
} }
// -------------------------------------------------------------------------
// Squid-style wander
// -------------------------------------------------------------------------
private void tickWanderMovement() { private void tickWanderMovement() {
if (--this.wanderRetargetTicks <= 0) { if (--this.wanderRetargetTicks <= 0) {
this.wanderRetargetTicks = 40 + this.random.nextInt(40); 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)); this.setDeltaMovement(this.getDeltaMovement().add(this.wanderDx, dy, this.wanderDz));
} }
// -------------------------------------------------------------------------
// Ink spray
// -------------------------------------------------------------------------
private boolean hasHitExplosiveTarget() { private boolean hasHitExplosiveTarget() {
AABB box = this.getBoundingBox(); AABB box = this.getBoundingBox();
return !this.level().getEntitiesOfClass(LivingEntity.class, box, 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); serverLevel.sendParticles(OCTOPUS_BOMB_INK_PARTICLE, cx, cy, cz, 30, 3.5D, 1.8D, 3.5D, 0.02D);
} }
// -------------------------------------------------------------------------
// Combat
// -------------------------------------------------------------------------
private void tickPursuitMovement(LivingEntity target) { private void tickPursuitMovement(LivingEntity target) {
if (this.strafeRetargetTicks-- <= 0) { if (this.strafeRetargetTicks-- <= 0) {
this.strafeRetargetTicks = 10 + this.random.nextInt(12); this.strafeRetargetTicks = 10 + this.random.nextInt(12);
@@ -157,7 +157,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem, com.craisinlord
return; return;
} }
// Toreterror jump tracking
if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) { if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) {
player.fallDistance = 0.0F; // prevent fall damage during the crash-down player.fallDistance = 0.0F; // prevent fall damage during the crash-down
boolean launched = readInt(stack, TORETERROR_LAUNCHED_TAG) == 1; 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) { if (level.isClientSide && clientShakeTicks > 0) {
clientShakeTicks--; clientShakeTicks--;
} }
@@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger;
public class Antarchy { public class Antarchy {
public static final String MODID = "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 final Logger LOGGER = LogManager.getLogger();
public static void init() { public static void init() {
@@ -197,6 +197,7 @@ public final class AntarchySoundEvents {
public static Supplier<SoundEvent> QUEEN_ROAR = (Supplier<SoundEvent>) UNBOUND; public static Supplier<SoundEvent> QUEEN_ROAR = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> KING_BEAM_SHOOT = (Supplier<SoundEvent>) UNBOUND; public static Supplier<SoundEvent> KING_BEAM_SHOOT = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> KING_FIREBALL_SHOOT = (Supplier<SoundEvent>) UNBOUND; public static Supplier<SoundEvent> KING_FIREBALL_SHOOT = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> KING_ICE_SPIKES = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> KING_DECREE_CAST = (Supplier<SoundEvent>) UNBOUND; public static Supplier<SoundEvent> KING_DECREE_CAST = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> QUEEN_BEAM_SHOOT = (Supplier<SoundEvent>) UNBOUND; public static Supplier<SoundEvent> QUEEN_BEAM_SHOOT = (Supplier<SoundEvent>) UNBOUND;
public static Supplier<SoundEvent> QUEEN_BEAM_START = (Supplier<SoundEvent>) UNBOUND; public static Supplier<SoundEvent> QUEEN_BEAM_START = (Supplier<SoundEvent>) UNBOUND;
@@ -409,6 +410,7 @@ public final class AntarchySoundEvents {
Supplier<SoundEvent> queenRoar, Supplier<SoundEvent> queenRoar,
Supplier<SoundEvent> kingBeamShoot, Supplier<SoundEvent> kingBeamShoot,
Supplier<SoundEvent> kingFireballShoot, Supplier<SoundEvent> kingFireballShoot,
Supplier<SoundEvent> kingIceSpikes,
Supplier<SoundEvent> kingDecreeCast, Supplier<SoundEvent> kingDecreeCast,
Supplier<SoundEvent> queenBeamShoot, Supplier<SoundEvent> queenBeamShoot,
Supplier<SoundEvent> queenBeamStart, Supplier<SoundEvent> queenBeamStart,
@@ -617,6 +619,7 @@ public final class AntarchySoundEvents {
QUEEN_ROAR = queenRoar; QUEEN_ROAR = queenRoar;
KING_BEAM_SHOOT = kingBeamShoot; KING_BEAM_SHOOT = kingBeamShoot;
KING_FIREBALL_SHOOT = kingFireballShoot; KING_FIREBALL_SHOOT = kingFireballShoot;
KING_ICE_SPIKES = kingIceSpikes;
KING_DECREE_CAST = kingDecreeCast; KING_DECREE_CAST = kingDecreeCast;
QUEEN_BEAM_SHOOT = queenBeamShoot; QUEEN_BEAM_SHOOT = queenBeamShoot;
QUEEN_BEAM_START = queenBeamStart; QUEEN_BEAM_START = queenBeamStart;
@@ -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);
}
}
@@ -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;
}
}
@@ -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<AntmailAttachment> fromComputerFile(ComputerFileSystem.ComputerFile file) {
if (file == null || file.type() != ComputerFileSystem.ComputerFile.Type.TEXT) return Optional.empty();
try {
return Optional.of(new AntmailAttachment.TextFile(filename(file.path()), file.contents()));
} catch (IllegalArgumentException exception) {
return Optional.empty();
}
}
public static AntmailAttachment.TextFile fromTextFile(String path, String contents) {
return new AntmailAttachment.TextFile(filename(path), contents);
}
public static AntmailAttachment.PaintImage fromPaintFile(AntPaintFile file) {
if (file == null) throw new IllegalArgumentException("Paint file cannot be null");
AntPaintCanvas canvas = file.canvas();
if (canvas == null || canvas.copyPixels().length != AntPaintCanvas.PIXEL_COUNT) {
throw new IllegalArgumentException("Invalid AntPaint canvas");
}
boolean[] source = canvas.copyPixels();
byte[] pixels = new byte[source.length];
for (int index = 0; index < source.length; index++) pixels[index] = source[index] ? (byte) 1 : (byte) 0;
return new AntmailAttachment.PaintImage(file.filename(), AntPaintCanvas.WIDTH, AntPaintCanvas.HEIGHT, pixels);
}
public static AntPaintFile toPaintFile(AntmailAttachment.PaintImage image, String fileId, long createdTime, long modifiedTime) {
if (image == null) throw new IllegalArgumentException("Paint attachment cannot be null");
if (image.width() != AntPaintCanvas.WIDTH || image.height() != AntPaintCanvas.HEIGHT) {
throw new IllegalArgumentException("Unsupported AntPaint canvas size");
}
byte[] source = image.pixels();
boolean[] pixels = new boolean[source.length];
for (int index = 0; index < source.length; index++) {
if (source[index] != 0 && source[index] != 1) throw new IllegalArgumentException("Invalid AntPaint pixel");
pixels[index] = source[index] == 1;
}
AntPaintCanvas canvas = new AntPaintCanvas();
for (int index = 0; index < pixels.length; index++) {
if (pixels[index]) canvas.set(index % AntPaintCanvas.WIDTH, index / AntPaintCanvas.WIDTH, true);
}
return new AntPaintFile(fileId, image.fileName(), canvas, createdTime, modifiedTime);
}
public static ComputerFileSystem.ComputerFile toComputerTextFile(String path, AntmailAttachment.TextFile file) {
if (file == null) throw new IllegalArgumentException("Text attachment cannot be null");
String normalized = ComputerFileSystem.normalize(path);
if (normalized.isEmpty() || normalized.equals(ComputerFileSystem.ROOT)) throw new IllegalArgumentException("Invalid text file path");
return ComputerFileSystem.ComputerFile.text(normalized, file.contents());
}
public static List<ComputerFileSystem.ComputerFile> selectableTextFiles(Collection<ComputerFileSystem.ComputerFile> files) {
if (files == null) return List.of();
List<ComputerFileSystem.ComputerFile> result = new ArrayList<>();
for (ComputerFileSystem.ComputerFile file : files) {
if (fromComputerFile(file).isPresent()) result.add(file);
}
return List.copyOf(result);
}
public static List<AntmailAttachment> selectablePaintFiles(Collection<AntPaintFile> files) {
if (files == null) return List.of();
List<AntmailAttachment> result = new ArrayList<>();
for (AntPaintFile file : files) {
try {
result.add(fromPaintFile(file));
} catch (IllegalArgumentException ignored) {
}
}
return List.copyOf(result);
}
private static String filename(String path) {
String normalized = ComputerFileSystem.normalize(path);
if (normalized.isEmpty() || normalized.equals(ComputerFileSystem.ROOT)) throw new IllegalArgumentException("Invalid file path");
int separator = normalized.lastIndexOf('/');
return normalized.substring(separator + 1);
}
}
@@ -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);
}
}
@@ -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<AntmailAttachment> attachments) {
public AntmailDraft {
id = id == null ? UUID.randomUUID() : id;
subject = subject == null ? "" : subject;
body = body == null ? "" : body;
attachments = attachments == null ? List.of() : List.copyOf(attachments);
if (subject.length() > AntmailValidation.MAX_SUBJECT_LENGTH || body.length() > AntmailValidation.MAX_BODY_LENGTH || attachments.size() > AntmailValidation.MAX_ATTACHMENTS) {
throw new IllegalArgumentException("Draft exceeds Antmail limits");
}
}
public CompoundTag toTag() {
CompoundTag tag = new CompoundTag();
tag.putString("Id", id.toString());
if (sender != null) tag.putString("From", sender.fullAddress());
if (recipient != null) tag.putString("To", recipient.fullAddress());
tag.putString("Subject", subject);
tag.putString("Body", body);
ListTag attachmentTags = new ListTag();
for (AntmailAttachment attachment : attachments) attachmentTags.add(attachment.toTag());
tag.put("Attachments", attachmentTags);
return tag;
}
public static AntmailDraft fromTag(CompoundTag tag) {
List<AntmailAttachment> attachments = new ArrayList<>();
ListTag attachmentTags = tag.getList("Attachments", 10);
for (int index = 0; index < attachmentTags.size(); index++) attachments.add(AntmailAttachment.fromTag(attachmentTags.getCompound(index)));
UUID id = UUID.fromString(tag.getString("Id"));
AntmailAddress sender = tag.contains("From") ? AntmailAddress.parse(tag.getString("From")) : null;
AntmailAddress recipient = tag.contains("To") ? AntmailAddress.parse(tag.getString("To")) : null;
return new AntmailDraft(id, sender, recipient, tag.getString("Subject"), tag.getString("Body"), attachments);
}
}
@@ -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<AntmailMessage> inbox = new ArrayList<>();
private final List<AntmailMessage> sent = new ArrayList<>();
private final List<AntmailDraft> drafts = new ArrayList<>();
public AntmailMailbox(AntmailAddress address) {
this.address = address;
}
public AntmailAddress address() { return address; }
public List<AntmailMessage> inbox() { return List.copyOf(inbox); }
public List<AntmailMessage> sent() { return List.copyOf(sent); }
public List<AntmailDraft> drafts() { return List.copyOf(drafts); }
public int unreadCount() { return (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<AntmailMessage> messages) {
ListTag tags = new ListTag();
for (AntmailMessage message : messages) tags.add(message.toTag());
return tags;
}
private static void readMessages(ListTag tags, List<AntmailMessage> target) {
for (int index = 0; index < tags.size() && target.size() < AntmailValidation.MAX_MAILBOX_MESSAGES; index++) {
try { target.add(AntmailMessage.fromTag(tags.getCompound(index))); } catch (RuntimeException ignored) { }
}
}
private static boolean contains(List<AntmailMessage> messages, UUID id) {
return messages.stream().anyMatch(message -> message.id().equals(id));
}
}
@@ -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<AntmailAddress, AntmailMailbox> mailboxes = new LinkedHashMap<>();
public AntmailMailbox getOrCreate(AntmailAddress address) {
return mailboxes.computeIfAbsent(address, AntmailMailbox::new);
}
public AntmailMailbox get(AntmailAddress address) {
return mailboxes.get(address);
}
public boolean contains(AntmailAddress address) {
return mailboxes.containsKey(address);
}
public void put(AntmailMailbox mailbox) {
mailboxes.put(mailbox.address(), mailbox);
}
public Collection<AntmailMailbox> mailboxes() {
return List.copyOf(mailboxes.values());
}
public CompoundTag toTag() {
CompoundTag tag = new CompoundTag();
ListTag mailboxTags = new ListTag();
for (AntmailMailbox mailbox : mailboxes.values()) mailboxTags.add(mailbox.toTag());
tag.put("Mailboxes", mailboxTags);
return tag;
}
public static AntmailMailboxStore fromTag(CompoundTag tag) {
AntmailMailboxStore store = new AntmailMailboxStore();
ListTag mailboxTags = tag.getList("Mailboxes", 10);
for (int index = 0; index < mailboxTags.size(); index++) {
try {
AntmailMailbox mailbox = AntmailMailbox.fromTag(mailboxTags.getCompound(index));
store.mailboxes.put(mailbox.address(), mailbox);
} catch (RuntimeException ignored) {
}
}
return store;
}
}
@@ -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<AntmailAttachment> attachments;
private boolean read;
public AntmailMessage(UUID id, AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List<AntmailAttachment> attachments, boolean read) {
AntmailValidation.Result result = AntmailValidation.validateMessage(sender, recipient, subject, body, attachments);
if (!result.valid()) {
throw new IllegalArgumentException(result.reason());
}
this.id = id == null ? UUID.randomUUID() : id;
this.sender = sender;
this.recipient = recipient;
this.subject = subject;
this.body = body;
this.createdAt = createdAt;
this.attachments = List.copyOf(attachments);
this.read = read;
}
public static AntmailMessage create(AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List<AntmailAttachment> attachments) {
return new AntmailMessage(UUID.randomUUID(), sender, recipient, subject, body, createdAt, attachments, false);
}
public UUID id() { return id; }
public AntmailAddress sender() { return sender; }
public AntmailAddress recipient() { return recipient; }
public String subject() { return subject; }
public String body() { return body; }
public long createdAt() { return createdAt; }
public List<AntmailAttachment> attachments() { return attachments; }
public boolean read() { return read; }
public 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<AntmailAttachment> attachments = new ArrayList<>();
ListTag attachmentTags = tag.getList(ATTACHMENTS_TAG, 10);
for (int index = 0; index < attachmentTags.size(); index++) attachments.add(AntmailAttachment.fromTag(attachmentTags.getCompound(index)));
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));
}
}
@@ -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<AntmailAddress, Endpoint> endpoints = new LinkedHashMap<>();
private final AntmailMailboxStore mailboxes = new AntmailMailboxStore();
private final List<PendingMessage> 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<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
CompoundTag endpoint = new CompoundTag();
endpoint.putString("Address", entry.getKey().fullAddress());
endpoint.putString("Dimension", entry.getValue().dimension());
endpoint.putLong("Position", entry.getValue().position());
endpointTags.add(endpoint);
}
tag.put("Endpoints", endpointTags);
tag.put("Mailboxes", mailboxes.toTag());
ListTag pendingTags = new ListTag();
for (PendingMessage message : pending) {
CompoundTag pendingTag = new CompoundTag();
pendingTag.putString("Recipient", message.recipient().fullAddress());
pendingTag.put("Message", message.message().toTag());
pendingTags.add(pendingTag);
}
tag.put("Pending", pendingTags);
return tag;
}
private static AntmailServerData get(MinecraftServer server) {
return server.overworld().getDataStorage().computeIfAbsent(
new SavedData.Factory<>(AntmailServerData::create, AntmailServerData::load, null), ID);
}
public static AntmailServerData access(MinecraftServer server) {
return get(server);
}
public synchronized Registration register(MinecraftServer server, ServerLevel level, BlockPos position, String username) {
final AntmailAddress address;
try {
address = AntmailAddress.ofUsername(username);
} catch (IllegalArgumentException exception) {
return Registration.invalid("invalid_username");
}
AntmailAddress previous = addressAt(level.dimension().location(), position);
Endpoint existing = endpoints.get(address);
if (existing != null && (!existing.dimension().equals(level.dimension().location().toString()) || existing.position() != position.asLong())) {
return Registration.invalid("username_taken");
}
if (previous != null && !previous.equals(address)) {
endpoints.remove(previous);
}
endpoints.put(address, new Endpoint(level.dimension().location().toString(), position.asLong()));
get(server).drain(server, address);
setDirty();
return Registration.accepted(address);
}
public synchronized AntmailAddress addressAt(ResourceLocation dimension, BlockPos position) {
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
if (entry.getValue().dimension().equals(dimension.toString()) && entry.getValue().position() == position.asLong()) return entry.getKey();
}
return null;
}
public synchronized boolean unregister(ResourceLocation dimension, BlockPos position) {
AntmailAddress address = addressAt(dimension, position);
if (address == null) return false;
endpoints.remove(address);
setDirty();
return true;
}
public synchronized Endpoint endpoint(AntmailAddress address) {
return endpoints.get(address);
}
public synchronized 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) {
}
}
@@ -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<AntmailAttachment> attachments) {
if (sender == null || recipient == null) {
return Result.invalid("missing address");
}
if (subject == null || subject.length() > MAX_SUBJECT_LENGTH) {
return Result.invalid("subject too long");
}
if (body == null || body.length() > MAX_BODY_LENGTH) {
return Result.invalid("body too long");
}
Result attachmentResult = validateAttachments(attachments);
if (!attachmentResult.valid()) return attachmentResult;
int bytes = subject.getBytes(StandardCharsets.UTF_8).length + body.getBytes(StandardCharsets.UTF_8).length;
for (AntmailAttachment attachment : attachments) bytes += attachment.byteSize();
if (bytes > MAX_MESSAGE_BYTES) {
return Result.invalid("message too large");
}
return Result.accepted();
}
public static Result validateAttachments(List<AntmailAttachment> attachments) {
if (attachments == null || attachments.size() > MAX_ATTACHMENTS) return Result.invalid("too many attachments");
int textCharacters = 0;
for (AntmailAttachment attachment : attachments) {
if (attachment == null) return Result.invalid("null attachment");
if (attachment instanceof AntmailAttachment.TextFile textFile) {
if (textFile.contents().length() > MAX_TEXT_ATTACHMENT_CHARACTERS) return Result.invalid("text attachment too large");
textCharacters += textFile.contents().length();
} else if (attachment instanceof AntmailAttachment.PaintImage paintImage) {
if (paintImage.width() != MAX_PAINT_WIDTH || paintImage.height() != MAX_PAINT_HEIGHT) return Result.invalid("unsupported paint size");
}
}
if (textCharacters > MAX_TEXT_ATTACHMENT_CHARACTERS) return Result.invalid("text attachments too large");
return Result.accepted();
}
public record Result(boolean valid, String reason) {
public static Result accepted() {
return new Result(true, "");
}
public static Result invalid(String reason) {
return new Result(false, reason);
}
}
}
@@ -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<AntmailAttachment> attachments) {
AntmailValidation.Result validation = AntmailValidation.validateAttachments(attachments);
if (!validation.valid()) throw new IllegalArgumentException(validation.reason());
CompoundTag root = new CompoundTag();
ListTag list = new ListTag();
for (AntmailAttachment attachment : attachments) list.add(attachment.toTag());
root.put("Attachments", list);
String encoded = 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<AntmailAttachment> decodeAttachments(String encoded) {
if (encoded == null || encoded.isBlank()) return List.of();
if (encoded.length() > AntmailValidation.MAX_ATTACHMENT_WIRE_CHARS) throw new IllegalArgumentException("Attachment payload is too large");
try {
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<AntmailAttachment> result = new ArrayList<>();
for (int i = 0; i < list.size(); i++) result.add(AntmailAttachment.fromTag(list.getCompound(i)));
AntmailValidation.Result validation = AntmailValidation.validateAttachments(result);
if (!validation.valid()) throw new IllegalArgumentException(validation.reason());
return List.copyOf(result);
} catch (Exception exception) {
throw new IllegalArgumentException("Invalid attachment data", exception);
}
}
}
@@ -5,6 +5,7 @@ import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import com.craisinlord.antarchy.content.client.AntarchyClientHooks; import com.craisinlord.antarchy.content.client.AntarchyClientHooks;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.ItemInteractionResult; import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.entity.player.Player; 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.Level;
import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block; 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.RenderShape;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType; 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.BlockState;
import net.minecraft.world.level.block.state.StateDefinition; 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.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.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape; 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 final class ComputerBlock extends BaseEntityBlock {
public static final MapCodec<ComputerBlock> CODEC = Block.simpleCodec(ComputerBlock::new); public static final MapCodec<ComputerBlock> CODEC = Block.simpleCodec(ComputerBlock::new);
public static final BooleanProperty ACTIVE = BooleanProperty.create("active"); 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); private static final VoxelShape SHAPE = Block.box(1.0D, 0.0D, 1.0D, 15.0D, 16.0D, 15.0D);
public ComputerBlock(BlockBehaviour.Properties properties) { public ComputerBlock(BlockBehaviour.Properties properties) {
super(properties.lightLevel(state -> state.getValue(ACTIVE) ? 7 : 0)); 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 @Override
@@ -42,7 +48,22 @@ public final class ComputerBlock extends BaseEntityBlock {
@Override @Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) { protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> 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 @Override
@@ -55,6 +76,25 @@ public final class ComputerBlock extends BaseEntityBlock {
return SHAPE; 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 @Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
if (!state.is(newState.getBlock()) && level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) { 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, protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player,
BlockHitResult hitResult) { BlockHitResult hitResult) {
if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) { 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(); computer.activate();
if (level.isClientSide) { if (level.isClientSide) {
AntarchyClientHooks.openComputer(pos); AntarchyClientHooks.openComputer(pos);
@@ -2,6 +2,8 @@ package com.craisinlord.antarchy.content.block.entity;
import com.craisinlord.antarchy.content.AntarchyObjects; import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antarchy.content.block.ComputerBlock; 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.BlockPos;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
@@ -10,12 +12,12 @@ import net.minecraft.nbt.StringTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.Containers; import net.minecraft.world.Containers;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -31,15 +33,27 @@ import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.util.GeckoLibUtil; import software.bernie.geckolib.util.GeckoLibUtil;
public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEntity { 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 DISKS_TAG = "Disks";
private static final String ACTIVE_UNTIL_TAG = "ActiveUntil"; private static final String ACTIVE_UNTIL_TAG = "ActiveUntil";
private final Set<ResourceLocation> 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<ResourceLocation> physicalDiskIds = new LinkedHashSet<>();
private final ComputerFileSystem fileSystem = new ComputerFileSystem();
private final ComputerDesktopState desktopState = new ComputerDesktopState();
private final AnimatableInstanceCache animatableCache = GeckoLibUtil.createInstanceCache(this); private final AnimatableInstanceCache animatableCache = GeckoLibUtil.createInstanceCache(this);
private static final RawAnimation ON = RawAnimation.begin().thenLoop("on_state"); private static final RawAnimation ON = RawAnimation.begin().thenLoop("on_state");
private static final RawAnimation OFF = RawAnimation.begin().thenLoop("off_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_ON = RawAnimation.begin().thenPlay("turn_on");
private static final RawAnimation TURN_OFF = RawAnimation.begin().thenPlay("turn_off"); private static final RawAnimation TURN_OFF = RawAnimation.begin().thenPlay("turn_off");
private long activeUntil; private long activeUntil;
private String password = "";
private boolean authenticated;
private long authenticatedUntil;
private java.util.UUID activeUser;
private boolean lastActive; private boolean lastActive;
public ComputerBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<ComputerBlockEntity>> type) { public ComputerBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<ComputerBlockEntity>> type) {
@@ -60,6 +74,11 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
computer.setChanged(); computer.setChanged();
level.sendBlockUpdated(pos, state, state, 3); 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) { public boolean insert(ItemStack stack, Player player) {
@@ -67,12 +86,12 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
return false; return false;
} }
ResourceLocation diskId = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get()); 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); stack.shrink(1);
activate(); activate();
return true; return true;
} }
this.diskIds.add(diskId); this.physicalDiskIds.add(diskId);
stack.shrink(1); stack.shrink(1);
activate(); activate();
setChanged(); setChanged();
@@ -94,12 +113,7 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
} }
public boolean isActive() { public boolean isActive() {
return level != null && (level.hasNeighborSignal(worldPosition) || level.getGameTime() < activeUntil || hasNearbyPlayer()); return level != null && (level.hasNeighborSignal(worldPosition) || isAuthenticated());
}
private boolean hasNearbyPlayer() {
Vec3 center = Vec3.atCenterOf(worldPosition);
return level.players().stream().anyMatch(player -> player.distanceToSqr(center) <= 25.0D);
} }
@Override @Override
@@ -116,15 +130,23 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
} }
public List<ResourceLocation> diskIds() { public List<ResourceLocation> diskIds() {
return List.copyOf(diskIds); List<ResourceLocation> ids = new ArrayList<>(physicalDiskIds.size() + 1);
ids.add(INTRODUCTION_DISK);
ids.addAll(physicalDiskIds);
return List.copyOf(ids);
} }
public boolean ejectOne(Player player) { 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; return false;
} }
ResourceLocation diskId = diskIds.iterator().next();
diskIds.remove(diskId);
ItemStack disk = diskStack(diskId); ItemStack disk = diskStack(diskId);
if (!player.addItem(disk)) { if (!player.addItem(disk)) {
player.drop(disk, false); player.drop(disk, false);
@@ -138,10 +160,158 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
if (level.isClientSide) { if (level.isClientSide) {
return; 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)); 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) { private static ItemStack diskStack(ResourceLocation diskId) {
@@ -153,32 +323,45 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
@Override @Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries); super.loadAdditional(tag, registries);
diskIds.clear(); physicalDiskIds.clear();
ListTag disks = tag.getList(DISKS_TAG, 8); ListTag disks = tag.getList(DISKS_TAG, 8);
for (int index = 0; index < disks.size(); index++) { for (int index = 0; index < disks.size(); index++) {
try { 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) { } catch (Exception ignored) {
} }
} }
activeUntil = tag.getLong(ACTIVE_UNTIL_TAG); 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 @Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries); super.saveAdditional(tag, registries);
ListTag disks = new ListTag(); ListTag disks = new ListTag();
for (ResourceLocation diskId : diskIds) { for (ResourceLocation diskId : physicalDiskIds) {
disks.add(StringTag.valueOf(diskId.toString())); disks.add(StringTag.valueOf(diskId.toString()));
} }
tag.put(DISKS_TAG, disks); tag.put(DISKS_TAG, disks);
tag.putLong(ACTIVE_UNTIL_TAG, activeUntil); tag.putLong(ACTIVE_UNTIL_TAG, activeUntil);
tag.putString(PASSWORD_TAG, password);
fileSystem.save(tag, registries);
tag.put(DESKTOP_TAG, desktopState.save());
} }
@Override @Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) { public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag(); CompoundTag tag = new CompoundTag();
saveAdditional(tag, registries); tag.putBoolean("HasPassword", hasPassword());
tag.putBoolean(AUTHENTICATED_TAG, isAuthenticated());
return tag; return tag;
} }
@@ -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<BlockPos, AntmailResultPayload> 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);
}
}
@@ -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<BlockPos, ComputerAccessResultPayload> 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);
}
}
@@ -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<BlockPos, State> STATES = new ConcurrentHashMap<>();
private ComputerFileSystemClientState() {
}
public static void update(ComputerAccessResultPayload payload) {
if (payload.data().isEmpty()) return;
String[] envelope = payload.data().split("\u0000", 3);
if (envelope.length < 3) return;
int action;
try {
action = Integer.parseInt(envelope[0]);
} catch (NumberFormatException ignored) {
return;
}
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<String> files = List.of();
private String openedPath = "";
private String openedContents = "";
private String error = "";
private boolean success;
public List<String> files() { return List.copyOf(files); }
public String openedPath() { return openedPath; }
public String openedContents() { return openedContents; }
public String error() { return error; }
public boolean success() { return success; }
}
}
@@ -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<Direction> 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<Direction> legalDirections(int x, int y) {
List<Direction> 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;
};
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -2,10 +2,16 @@ package com.craisinlord.antarchy.content.client.renderer;
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity; import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
import com.craisinlord.antarchy.content.client.model.ComputerModel; import com.craisinlord.antarchy.content.client.model.ComputerModel;
import net.minecraft.core.Direction;
import software.bernie.geckolib.renderer.GeoBlockRenderer; import software.bernie.geckolib.renderer.GeoBlockRenderer;
public final class ComputerRenderer extends GeoBlockRenderer<ComputerBlockEntity> { public final class ComputerRenderer extends GeoBlockRenderer<ComputerBlockEntity> {
public ComputerRenderer(net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context context) { public ComputerRenderer(net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context context) {
super(new ComputerModel()); super(new ComputerModel());
} }
@Override
protected Direction getFacing(ComputerBlockEntity computer) {
return computer.getBlockState().getValue(com.craisinlord.antarchy.content.block.ComputerBlock.FACING);
}
} }
@@ -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<ResourceLocation> unlockedWallpaperIds = new LinkedHashSet<>();
private final Set<ResourceLocation> installedProgramIds = new LinkedHashSet<>();
private final Map<ResourceLocation, IconPosition> iconPositions = new LinkedHashMap<>();
public ResourceLocation selectedWallpaper() {
return selectedWallpaper;
}
public Set<ResourceLocation> unlockedWallpaperIds() {
return Collections.unmodifiableSet(new LinkedHashSet<>(unlockedWallpaperIds));
}
public Set<ResourceLocation> installedProgramIds() {
return Collections.unmodifiableSet(new LinkedHashSet<>(installedProgramIds));
}
public Map<ResourceLocation, IconPosition> iconPositions() {
return Collections.unmodifiableMap(new LinkedHashMap<>(iconPositions));
}
public boolean hasWallpaper(ResourceLocation id) {
return id != null && unlockedWallpaperIds.contains(id);
}
public boolean hasProgram(ResourceLocation id) {
return id != null && installedProgramIds.contains(id);
}
public IconPosition iconPosition(ResourceLocation id) {
return iconPositions.get(id);
}
public boolean unlockWallpaper(ResourceLocation id) {
if (!validId(id) || unlockedWallpaperIds.contains(id)) {
return false;
}
if (unlockedWallpaperIds.size() >= MAX_UNLOCKED_WALLPAPERS) {
return false;
}
return unlockedWallpaperIds.add(id);
}
public boolean installProgram(ResourceLocation id) {
if (!validId(id) || installedProgramIds.size() >= MAX_INSTALLED_PROGRAMS && !installedProgramIds.contains(id)) {
return false;
}
return installedProgramIds.add(id);
}
public boolean uninstallProgram(ResourceLocation id) {
if (id == null || !installedProgramIds.remove(id)) {
return false;
}
iconPositions.remove(id);
return true;
}
public boolean selectWallpaper(ResourceLocation id) {
if (id != null && !unlockedWallpaperIds.contains(id)) {
return false;
}
if (id == null ? selectedWallpaper == null : id.equals(selectedWallpaper)) {
return false;
}
selectedWallpaper = id;
return true;
}
public boolean setIconPosition(ResourceLocation id, int x, int y) {
if (!validId(id) || x < MIN_ICON_COORDINATE || x > MAX_ICON_COORDINATE || y < MIN_ICON_COORDINATE || y > MAX_ICON_COORDINATE) {
return false;
}
IconPosition position = new IconPosition(x, y);
if (position.equals(iconPositions.get(id))) {
return false;
}
if (!iconPositions.containsKey(id) && iconPositions.size() >= MAX_ICON_POSITIONS) {
return false;
}
iconPositions.put(id, position);
return true;
}
public boolean removeIconPosition(ResourceLocation id) {
return id != null && iconPositions.remove(id) != null;
}
public void clear() {
unlockedWallpaperIds.clear();
installedProgramIds.clear();
iconPositions.clear();
unlockedWallpaperIds.add(DEFAULT_WALLPAPER);
selectedWallpaper = DEFAULT_WALLPAPER;
}
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
if (selectedWallpaper != null) {
tag.putString(SELECTED_WALLPAPER_TAG, selectedWallpaper.toString());
}
tag.put(UNLOCKED_WALLPAPERS_TAG, saveIds(unlockedWallpaperIds));
tag.put(INSTALLED_PROGRAMS_TAG, saveIds(installedProgramIds));
ListTag positions = new ListTag();
for (Map.Entry<ResourceLocation, IconPosition> entry : iconPositions.entrySet()) {
CompoundTag position = new CompoundTag();
position.putString(ID_TAG, entry.getKey().toString());
position.putInt(X_TAG, entry.getValue().x());
position.putInt(Y_TAG, entry.getValue().y());
positions.add(position);
}
tag.put(ICON_POSITIONS_TAG, positions);
return tag;
}
public void load(CompoundTag tag) {
clear();
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<ResourceLocation> ids) {
ListTag list = new ListTag();
for (ResourceLocation id : ids) {
list.add(StringTag.valueOf(id.toString()));
}
return list;
}
private static void loadIds(ListTag list, Set<ResourceLocation> destination, int limit) {
for (int index = 0; index < list.size() && destination.size() < limit; index++) {
ResourceLocation id = parseId(list.getString(index));
if (id != null) {
destination.add(id);
}
}
}
private static ResourceLocation parseId(String value) {
if (value == null || value.isBlank() || value.length() > MAX_ID_LENGTH) {
return null;
}
try {
return ResourceLocation.parse(value);
} catch (Exception ignored) {
return null;
}
}
private static boolean validId(ResourceLocation id) {
return id != null && id.toString().length() <= MAX_ID_LENGTH;
}
public record IconPosition(int x, int y) {
}
}
@@ -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<String, ComputerFile> nodes = new LinkedHashMap<>();
private int totalBytes;
public ComputerFileSystem() {
reset();
}
public void reset() {
nodes.clear();
nodes.put(ROOT, ComputerFile.directory(ROOT));
nodes.put(DOCUMENTS, ComputerFile.directory(DOCUMENTS));
nodes.put(PICTURES, ComputerFile.directory(PICTURES));
nodes.put(SYSTEM, ComputerFile.directory(SYSTEM));
totalBytes = 0;
}
public ComputerFile get(String path) {
return nodes.get(normalize(path));
}
public List<ComputerFile> list() {
return List.copyOf(nodes.values());
}
public List<ComputerFile> listTextFiles() {
return nodes.values().stream().filter(file -> file.type() == ComputerFile.Type.TEXT).toList();
}
public Result 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 }
}
}
@@ -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<Integer> queue = new ArrayDeque<>();
boolean[] visited = new boolean[PIXEL_COUNT];
queue.add(start);
int changed = 0;
while (!queue.isEmpty()) {
int current = queue.removeFirst();
if (visited[current] || pixels[current] != original) {
continue;
}
visited[current] = true;
pixels[current] = value;
changed++;
int px = current % WIDTH;
int py = current / WIDTH;
if (px > 0) queue.add(current - 1);
if (px + 1 < WIDTH) queue.add(current + 1);
if (py > 0) queue.add(current - WIDTH);
if (py + 1 < HEIGHT) queue.add(current + WIDTH);
}
return changed;
}
public byte[] pack() {
byte[] packed = new byte[PACKED_LENGTH];
for (int i = 0; i < PIXEL_COUNT; i++) {
if (pixels[i]) {
packed[i / 8] |= (byte) (1 << (i % 8));
}
}
return packed;
}
public static AntPaintCanvas unpack(byte[] packed) {
if (packed == null || packed.length != PACKED_LENGTH) {
throw new IllegalArgumentException("Invalid AntPaint pixel data length");
}
boolean[] pixels = new boolean[PIXEL_COUNT];
for (int i = 0; i < PIXEL_COUNT; i++) {
pixels[i] = (packed[i / 8] & (1 << (i % 8))) != 0;
}
return new AntPaintCanvas(pixels);
}
public AntPaintCanvas copy() {
return new AntPaintCanvas(Arrays.copyOf(pixels, pixels.length));
}
public boolean[] copyPixels() {
return Arrays.copyOf(pixels, pixels.length);
}
private boolean setIfChanged(int x, int y, boolean value) {
int index = index(x, y);
if (pixels[index] == value) {
return false;
}
pixels[index] = value;
return true;
}
private static int index(int x, int y) {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) {
throw new IndexOutOfBoundsException("AntPaint pixel outside canvas");
}
return y * WIDTH + x;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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<AntPaintCanvas> undo = new ArrayDeque<>();
private final ArrayDeque<AntPaintCanvas> redo = new ArrayDeque<>();
public void record(AntPaintCanvas canvas) {
undo.addLast(canvas.copy());
while (undo.size() > MAX_STATES) {
undo.removeFirst();
}
redo.clear();
}
public boolean canUndo() {
return !undo.isEmpty();
}
public boolean canRedo() {
return !redo.isEmpty();
}
public AntPaintCanvas undo(AntPaintCanvas current) {
if (undo.isEmpty()) {
return current;
}
redo.addLast(current.copy());
return undo.removeLast().copy();
}
public AntPaintCanvas redo(AntPaintCanvas current) {
if (redo.isEmpty()) {
return current;
}
undo.addLast(current.copy());
return redo.removeLast().copy();
}
public void clear() {
undo.clear();
redo.clear();
}
}
@@ -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");
}
}
}
@@ -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<AntPaintFileMetadata> files) {
public List {
files = files == null ? java.util.List.of() : java.util.List.copyOf(files);
}
}
public record Delete(Code code, String fileId) {
}
public record AntPaintFileMetadata(String fileId, String filename, long modifiedTime) {
}
}
@@ -0,0 +1,7 @@
package com.craisinlord.antarchy.content.computer.paint;
public enum AntPaintTool {
PENCIL,
ERASER,
FILL
}
@@ -0,0 +1,11 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.List;
public record TerminalCommand(String name, List<String> arguments, String rawInput) {
public TerminalCommand {
name = name.toLowerCase(java.util.Locale.ROOT);
arguments = List.copyOf(arguments);
rawInput = rawInput == null ? "" : rawInput;
}
}
@@ -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<String> tokens = new ArrayList<>();
StringBuilder token = new StringBuilder();
Character quote = null;
boolean escaped = false;
for (int index = 0; index < trimmed.length(); index++) {
char character = trimmed.charAt(index);
if (escaped) {
token.append(character);
escaped = false;
} else if (character == '\\') {
escaped = true;
} else if (quote != null) {
if (character == quote) {
quote = null;
} else {
token.append(character);
}
} else if (character == '\'' || character == '"') {
quote = character;
} else if (Character.isWhitespace(character)) {
if (!token.isEmpty()) {
tokens.add(token.toString());
token.setLength(0);
}
} else {
token.append(character);
}
}
if (escaped) {
token.append('\\');
}
if (quote != null) {
return ParseResult.error("ERROR: UNTERMINATED QUOTE");
}
if (!token.isEmpty()) {
tokens.add(token.toString());
}
if (tokens.isEmpty()) {
return ParseResult.error("ERROR: EMPTY INPUT");
}
return ParseResult.success(new TerminalCommand(tokens.get(0), tokens.subList(1, tokens.size()), input));
}
public record ParseResult(TerminalCommand command, String error) {
public boolean isSuccess() {
return command != null;
}
public static ParseResult success(TerminalCommand command) {
return new ParseResult(command, null);
}
public static ParseResult error(String message) {
return new ParseResult(null, message);
}
}
}
@@ -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<String> COMMANDS = List.of(
"help", "clear", "pwd", "ls", "cd", "mkdir", "touch", "cat", "write", "rm", "rmdir", "mv", "open"
);
public TerminalResult execute(TerminalFileSystem fileSystem, String workingDirectory, String input) {
String currentDirectory = normalizeWorkingDirectory(fileSystem, workingDirectory);
TerminalCommandParser.ParseResult parsed = TerminalCommandParser.parse(input);
if (!parsed.isSuccess()) {
return TerminalResult.error(currentDirectory, parsed.error());
}
TerminalCommand command = parsed.command();
if (command.arguments().stream().anyMatch(argument -> argument.length() > MAX_ARGUMENT_LENGTH)) {
return TerminalResult.error(currentDirectory, "ERROR: ARGUMENT TOO LONG");
}
return switch (command.name()) {
case "help" -> help(currentDirectory, command);
case "clear" -> requireArguments(currentDirectory, command, 0, TerminalResult.clear(currentDirectory));
case "pwd" -> requireArguments(currentDirectory, command, 0, TerminalResult.success(currentDirectory, currentDirectory));
case "ls" -> list(fileSystem, currentDirectory, command);
case "cd" -> changeDirectory(fileSystem, currentDirectory, command);
case "mkdir" -> createDirectory(fileSystem, currentDirectory, command);
case "touch" -> createFile(fileSystem, currentDirectory, command);
case "cat" -> readFile(fileSystem, currentDirectory, command);
case "write" -> writeFile(fileSystem, currentDirectory, command);
case "rm" -> removeFile(fileSystem, currentDirectory, command);
case "rmdir" -> removeDirectory(fileSystem, currentDirectory, command);
case "mv" -> move(fileSystem, currentDirectory, command);
case "open" -> open(fileSystem, currentDirectory, command);
default -> TerminalResult.error(currentDirectory, "ERROR: INVALID COMMAND: " + command.name());
};
}
private TerminalResult help(String workingDirectory, TerminalCommand command) {
if (command.arguments().isEmpty()) {
return TerminalResult.success(workingDirectory,
"AVAILABLE COMMANDS",
"HELP CLEAR PWD LS CD MKDIR TOUCH CAT WRITE RM RMDIR MV OPEN");
}
if (command.arguments().size() != 1) {
return usage(workingDirectory, "help [command]");
}
return switch (command.arguments().get(0).toLowerCase(Locale.ROOT)) {
case "help" -> TerminalResult.success(workingDirectory, "HELP [COMMAND] // SHOW COMMAND HELP");
case "clear" -> TerminalResult.success(workingDirectory, "CLEAR // CLEAR TERMINAL OUTPUT");
case "pwd" -> TerminalResult.success(workingDirectory, "PWD // PRINT WORKING DIRECTORY");
case "ls" -> TerminalResult.success(workingDirectory, "LS [PATH] // LIST DIRECTORY CONTENTS");
case "cd" -> TerminalResult.success(workingDirectory, "CD [PATH] // CHANGE WORKING DIRECTORY");
case "mkdir" -> TerminalResult.success(workingDirectory, "MKDIR <PATH> // CREATE DIRECTORY");
case "touch" -> TerminalResult.success(workingDirectory, "TOUCH <PATH> // CREATE EMPTY TEXT FILE");
case "cat" -> TerminalResult.success(workingDirectory, "CAT <PATH> // PRINT TEXT FILE");
case "write" -> TerminalResult.success(workingDirectory, "WRITE <PATH> [TEXT] // REPLACE TEXT FILE CONTENTS");
case "rm" -> TerminalResult.success(workingDirectory, "RM <PATH> // DELETE TEXT FILE");
case "rmdir" -> TerminalResult.success(workingDirectory, "RMDIR <PATH> // DELETE EMPTY DIRECTORY");
case "mv" -> TerminalResult.success(workingDirectory, "MV <SOURCE> <DESTINATION> // MOVE OR RENAME");
case "open" -> TerminalResult.success(workingDirectory, "OPEN <PATH> // OPEN TEXT FILE IN ANTTEXT");
default -> TerminalResult.error(workingDirectory, "ERROR: NO HELP FOR " + command.arguments().get(0));
};
}
private TerminalResult list(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() > 1) {
return usage(workingDirectory, "ls [path]");
}
String path = command.arguments().isEmpty() ? workingDirectory : command.arguments().get(0);
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, path, workingDirectory);
if (entry.isEmpty()) {
return missing(workingDirectory, path);
}
if (!entry.get().isDirectory()) {
return TerminalResult.success(workingDirectory, entry.get().name());
}
List<TerminalFileSystem.Entry> entries = new ArrayList<>(fileSystem.list(path, workingDirectory));
entries.sort(Comparator.comparing(TerminalFileSystem.Entry::isDirectory).reversed().thenComparing(TerminalFileSystem.Entry::name));
if (entries.isEmpty()) {
return TerminalResult.success(workingDirectory, "DIRECTORY EMPTY");
}
return new TerminalResult(TerminalResult.Status.SUCCESS, entries.stream()
.map(value -> (value.isDirectory() ? "[DIR] " : " ") + value.name()).toList(), workingDirectory, false, null);
}
private TerminalResult changeDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() > 1) {
return usage(workingDirectory, "cd [path]");
}
String path = command.arguments().isEmpty() ? TerminalFileSystem.ROOT_PATH : command.arguments().get(0);
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, path, workingDirectory);
if (entry.isEmpty() || !entry.get().isDirectory()) {
return missing(workingDirectory, path);
}
return TerminalResult.success(entry.get().path(), entry.get().path());
}
private TerminalResult createDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "mkdir <path>");
}
return fileSystem.createDirectory(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT CREATE DIRECTORY");
}
private TerminalResult createFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "touch <path>");
}
return fileSystem.createFile(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT CREATE FILE");
}
private TerminalResult readFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "cat <path>");
}
Optional<String> contents = fileSystem.readText(command.arguments().get(0), workingDirectory);
return contents.map(value -> TerminalResult.success(workingDirectory, value.split("\\R", -1)))
.orElseGet(() -> missing(workingDirectory, command.arguments().get(0)));
}
private TerminalResult writeFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() < 1 || command.arguments().size() > 2) {
return usage(workingDirectory, "write <path> [text]");
}
String contents = command.arguments().size() == 2 ? command.arguments().get(1) : "";
return fileSystem.writeText(command.arguments().get(0), workingDirectory, contents)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT WRITE FILE");
}
private TerminalResult removeFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "rm <path>");
}
return fileSystem.deleteFile(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT DELETE FILE");
}
private TerminalResult removeDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "rmdir <path>");
}
return fileSystem.deleteEmptyDirectory(command.arguments().get(0), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: DIRECTORY NOT EMPTY OR NOT FOUND");
}
private TerminalResult move(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 2) {
return usage(workingDirectory, "mv <source> <destination>");
}
return fileSystem.move(command.arguments().get(0), command.arguments().get(1), workingDirectory)
? TerminalResult.success(workingDirectory)
: TerminalResult.error(workingDirectory, "ERROR: CANNOT MOVE PATH");
}
private TerminalResult open(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
if (command.arguments().size() != 1) {
return usage(workingDirectory, "open <path>");
}
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, command.arguments().get(0), workingDirectory);
if (entry.isEmpty() || entry.get().isDirectory()) {
return missing(workingDirectory, command.arguments().get(0));
}
return TerminalResult.open(workingDirectory, entry.get().path());
}
private Optional<TerminalFileSystem.Entry> find(TerminalFileSystem fileSystem, String path, String workingDirectory) {
try {
return fileSystem.find(path, workingDirectory);
} catch (RuntimeException ignored) {
return Optional.empty();
}
}
private String normalizeWorkingDirectory(TerminalFileSystem fileSystem, String workingDirectory) {
try {
String normalized = fileSystem.normalize(workingDirectory, TerminalFileSystem.ROOT_PATH);
Optional<TerminalFileSystem.Entry> entry = fileSystem.find(normalized, TerminalFileSystem.ROOT_PATH);
return entry.filter(TerminalFileSystem.Entry::isDirectory).map(TerminalFileSystem.Entry::path).orElse(TerminalFileSystem.ROOT_PATH);
} catch (RuntimeException ignored) {
return TerminalFileSystem.ROOT_PATH;
}
}
private TerminalResult missing(String workingDirectory, String path) {
return TerminalResult.error(workingDirectory, "ERROR: PATH NOT FOUND: " + path);
}
private TerminalResult usage(String workingDirectory, String usage) {
return TerminalResult.error(workingDirectory, "USAGE: " + usage);
}
private TerminalResult requireArguments(String workingDirectory, TerminalCommand command, int count, TerminalResult result) {
return command.arguments().size() == count ? result : usage(workingDirectory, command.name() + " requires " + count + " arguments");
}
}
@@ -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<Entry> find(String path, String workingDirectory);
List<Entry> list(String path, String workingDirectory);
boolean createDirectory(String path, String workingDirectory);
boolean createFile(String path, String workingDirectory);
Optional<String> readText(String path, String workingDirectory);
boolean writeText(String path, String workingDirectory, String contents);
boolean deleteFile(String path, String workingDirectory);
boolean deleteEmptyDirectory(String path, String workingDirectory);
boolean move(String source, String destination, String workingDirectory);
String normalize(String path, String workingDirectory);
record Entry(String name, String path, EntryType type) {
public boolean isDirectory() {
return type == EntryType.DIRECTORY;
}
}
enum EntryType {
FILE,
DIRECTORY
}
}
@@ -0,0 +1,33 @@
package com.craisinlord.antarchy.content.computer.terminal;
import java.util.List;
public record TerminalResult(Status status, List<String> lines, String workingDirectory, boolean clearOutput, String openPath) {
public TerminalResult {
status = status == null ? Status.ERROR : status;
lines = List.copyOf(lines);
workingDirectory = workingDirectory == null || workingDirectory.isBlank() ? TerminalFileSystem.ROOT_PATH : workingDirectory;
}
public static TerminalResult success(String workingDirectory, String... lines) {
return new TerminalResult(Status.SUCCESS, List.of(lines), workingDirectory, false, null);
}
public static TerminalResult error(String workingDirectory, String... lines) {
return new TerminalResult(Status.ERROR, List.of(lines), workingDirectory, false, null);
}
public static TerminalResult clear(String workingDirectory) {
return new TerminalResult(Status.SUCCESS, List.of(), workingDirectory, true, null);
}
public static TerminalResult open(String workingDirectory, String path) {
return new TerminalResult(Status.OPEN, List.of("OPENING " + path), workingDirectory, false, path);
}
public enum Status {
SUCCESS,
ERROR,
OPEN
}
}
@@ -75,11 +75,9 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this);
// Squid-style wander
private float wanderDx, wanderDy, wanderDz; private float wanderDx, wanderDy, wanderDz;
private int wanderRetargetTicks; private int wanderRetargetTicks;
// Combat
private int attackCooldown; private int attackCooldown;
private int actionTicks; private int actionTicks;
private int lastAttackState; private int lastAttackState;
@@ -88,7 +86,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
private float orbitDirection = 1.0F; private float orbitDirection = 1.0F;
private int inkSprayCooldown; private int inkSprayCooldown;
// Kraken spawn flag
private boolean spawnedByKraken; private boolean spawnedByKraken;
// Explosive impact (fired from the R.P.O. Launcher) // Explosive impact (fired from the R.P.O. Launcher)
@@ -364,10 +361,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
this.hasImpulse = true; this.hasImpulse = true;
} }
// -------------------------------------------------------------------------
// Squid-style wander
// -------------------------------------------------------------------------
private void tickWanderMovement() { private void tickWanderMovement() {
if (--this.wanderRetargetTicks <= 0) { if (--this.wanderRetargetTicks <= 0) {
this.wanderRetargetTicks = 40 + this.random.nextInt(40); 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)); this.setDeltaMovement(this.getDeltaMovement().add(this.wanderDx, dy, this.wanderDz));
} }
// -------------------------------------------------------------------------
// Ink spray
// -------------------------------------------------------------------------
private boolean hasHitExplosiveTarget() { private boolean hasHitExplosiveTarget() {
AABB box = this.getBoundingBox(); AABB box = this.getBoundingBox();
return !this.level().getEntitiesOfClass(LivingEntity.class, box, 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); serverLevel.sendParticles(OCTOPUS_BOMB_INK_PARTICLE, cx, cy, cz, 30, 3.5D, 1.8D, 3.5D, 0.02D);
} }
// -------------------------------------------------------------------------
// Combat
// -------------------------------------------------------------------------
private void tickPursuitMovement(LivingEntity target) { private void tickPursuitMovement(LivingEntity target) {
if (this.strafeRetargetTicks-- <= 0) { if (this.strafeRetargetTicks-- <= 0) {
this.strafeRetargetTicks = 10 + this.random.nextInt(12); this.strafeRetargetTicks = 10 + this.random.nextInt(12);
@@ -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 NEST_SEARCH_INTERVAL_TICKS = 80;
private static final int FOOD_NEST_SEARCH_INTERVAL_TICKS = 20; private static final int FOOD_NEST_SEARCH_INTERVAL_TICKS = 20;
private static final int CHEST_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 int SHARED_CHEST_MEMORY_TICKS = 120;
private static final double FOOD_CHEST_SEARCH_RADIUS = 20.0D; private static final double FOOD_CHEST_SEARCH_RADIUS = 20.0D;
private static final int GROUND_FOOD_CACHE_INTERVAL_TICKS = 20; 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 BlockPos targetFoodChestPos;
private int nextFoodChestSearchTick; private int nextFoodChestSearchTick;
@Nullable @Nullable
private BlockPos cachedFoodChestValidationPos;
private boolean cachedFoodChestValidationResult;
private int nextFoodChestValidationTick;
@Nullable
private BlockPos recentFoodChestPos; private BlockPos recentFoodChestPos;
private int recentFoodChestExpireTick; private int recentFoodChestExpireTick;
@Nullable @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; return bestPos;
} }
private boolean isValidFoodChest(BlockPos chestPos) { 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); 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() { private void clearFoodChestTarget() {
this.targetFoodChestPos = null; this.targetFoodChestPos = null;
this.nextFoodChestSearchTick = 0; this.nextFoodChestSearchTick = 0;
this.cachedFoodChestValidationPos = null;
this.cachedFoodChestValidationResult = false;
this.nextFoodChestValidationTick = 0;
} }
private boolean stealFoodFromChest(BlockPos chestPos) { private boolean stealFoodFromChest(BlockPos chestPos) {
@@ -1390,13 +1414,16 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity {
return null; 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; return leader.targetFoodChestPos;
} }
if (leader.recentFoodChestPos != null if (leader.recentFoodChestPos != null
&& leader.tickCount <= leader.recentFoodChestExpireTick && leader.tickCount <= leader.recentFoodChestExpireTick) {
&& this.isValidFoodChest(leader.recentFoodChestPos)) {
return leader.recentFoodChestPos; return leader.recentFoodChestPos;
} }
@@ -994,7 +994,7 @@ public class KingEntity extends RoyalBossEntity {
} }
@Override public void onActive(int elapsedTicks) { @Override public void onActive(int elapsedTicks) {
KingEntity.this.raiseIceSpikes(level, target); 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; })) return;
this.iceSpikeCooldownTicks = AntarchySettings.kingIceSpikeCooldownTicks(); this.iceSpikeCooldownTicks = AntarchySettings.kingIceSpikeCooldownTicks();
@@ -18,6 +18,7 @@ import java.util.Map;
public final class ComputerGuideData extends SimplePreparableReloadListener<ComputerGuideData.LoadedData> { public final class ComputerGuideData extends SimplePreparableReloadListener<ComputerGuideData.LoadedData> {
private static final ComputerGuideData INSTANCE = new ComputerGuideData(); private static final ComputerGuideData INSTANCE = new ComputerGuideData();
private static final ResourceLocation INTRODUCTION = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
private static volatile Map<ResourceLocation, Entry> entries = Map.of(); private static volatile Map<ResourceLocation, Entry> entries = Map.of();
private static volatile Map<ResourceLocation, Disk> disks = Map.of(); private static volatile Map<ResourceLocation, Disk> disks = Map.of();
@@ -39,6 +40,10 @@ public final class ComputerGuideData extends SimplePreparableReloadListener<Comp
public static List<Entry> entriesFor(List<ResourceLocation> diskIds) { public static List<Entry> entriesFor(List<ResourceLocation> diskIds) {
Map<ResourceLocation, Entry> result = new LinkedHashMap<>(); Map<ResourceLocation, Entry> result = new LinkedHashMap<>();
for (ResourceLocation diskId : diskIds) { 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); Disk disk = disks.get(diskId);
if (disk == null) { if (disk == null) {
continue; continue;
@@ -176,7 +176,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
return; return;
} }
// Toreterror jump tracking
if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) { if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) {
player.fallDistance = 0.0F; // prevent fall damage during the crash-down player.fallDistance = 0.0F; // prevent fall damage during the crash-down
boolean launched = readInt(stack, TORETERROR_LAUNCHED_TAG) == 1; 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) { if (level.isClientSide && clientShakeTicks > 0) {
clientShakeTicks--; clientShakeTicks--;
} }
@@ -18,7 +18,7 @@ import java.util.function.Consumer;
public final class ComputerItem extends BlockItem implements GeoItem { public final class ComputerItem extends BlockItem implements GeoItem {
private static final ResourceLocation MODEL = ResourceLocation.fromNamespaceAndPath("antarchy", "geo/computer.geo.json"); 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 static final ResourceLocation ANIMATION = ResourceLocation.fromNamespaceAndPath("antarchy", "animations/computer.animation.json");
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
@@ -29,7 +29,7 @@ public final class ComputerItem extends BlockItem implements GeoItem {
@Override @Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { 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 @Override
@@ -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<Object> sender = payload -> { };
private AntmailNetworking() {
}
public static void setSender(Consumer<Object> 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<AntmailAttachment> 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));
}
}
@@ -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<CustomPacketPayload.Type<?>> all() {
return List.of(
AntmailSetupPayload.TYPE,
AntmailStateRequestPayload.TYPE,
AntmailSendPayload.TYPE,
AntmailReadPayload.TYPE,
AntmailResultPayload.TYPE
);
}
}
@@ -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<AntmailReadPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_read"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailReadPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailReadPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailReadPayload::messageId, ByteBufCodecs.BOOL, AntmailReadPayload::read, AntmailReadPayload::new);
@Override public Type<AntmailReadPayload> type() { return TYPE; }
}
@@ -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<AntmailResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_result"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailResultPayload> 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<AntmailResultPayload> type() { return TYPE; }
}
@@ -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<AntmailSendPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_send"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailSendPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSendPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailSendPayload::recipient, ByteBufCodecs.stringUtf8(64), AntmailSendPayload::subject, ByteBufCodecs.stringUtf8(16384), AntmailSendPayload::body, ByteBufCodecs.stringUtf8(65536), AntmailSendPayload::attachments, AntmailSendPayload::new);
@Override public Type<AntmailSendPayload> type() { return TYPE; }
}
@@ -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<ServerPlayer, AntmailResultPayload> resultSender = (player, payload) -> { };
private AntmailServerHandler() {
}
public static void setResultSender(BiConsumer<ServerPlayer, AntmailResultPayload> 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<AntmailAttachment> attachments = AntmailWire.decodeAttachments(payload.attachments());
AntmailMessage message = AntmailMessage.create(sender, recipient, payload.subject(), payload.body(), player.serverLevel().getGameTime(), attachments);
AntmailDeliveryResult delivery = data.deliver(player.server, message);
result(player, payload.pos(), delivery.status().ordinal(), 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));
}
}
@@ -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<AntmailSetupPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_setup"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailSetupPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSetupPayload::pos, ByteBufCodecs.stringUtf8(32), AntmailSetupPayload::username, AntmailSetupPayload::new);
@Override public Type<AntmailSetupPayload> type() { return TYPE; }
}
@@ -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<AntmailStateRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_state_request"));
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailStateRequestPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailStateRequestPayload::pos, AntmailStateRequestPayload::new);
@Override public Type<AntmailStateRequestPayload> type() { return TYPE; }
}
@@ -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<ServerPlayer, ComputerAccessResultPayload> resultSender = (player, result) -> {
};
private ComputerAccessHandler() {
}
public static void setResultSender(BiConsumer<ServerPlayer, ComputerAccessResultPayload> sender) {
resultSender = sender;
}
public static void handle(ServerPlayer player, ComputerAccessPayload payload) {
if (payload.value().length() > 65536 || player.level().isClientSide || !player.serverLevel().hasChunkAt(payload.pos()) ||
player.distanceToSqr(payload.pos().getX() + 0.5D, payload.pos().getY() + 0.5D, payload.pos().getZ() + 0.5D) > 64.0D) {
return;
}
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
if (!(blockEntity instanceof ComputerBlockEntity computer)) {
send(player, payload, ComputerAccessResultPayload.INVALID);
return;
}
switch (payload.action()) {
case ComputerAccessPayload.OPEN -> open(player, computer, payload);
case ComputerAccessPayload.SETUP -> setup(player, computer, payload);
case ComputerAccessPayload.LOGIN -> login(player, computer, payload);
case ComputerAccessPayload.LOGOUT -> {
computer.logout();
send(player, payload, ComputerAccessResultPayload.READY);
}
case ComputerAccessPayload.CLOSE -> {
computer.releaseUser(player);
send(player, payload, computer.isAuthenticated() ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.READY);
}
case ComputerAccessPayload.CHANGE_PASSWORD -> {
boolean changed = computer.setPassword(player, payload.value());
send(player, payload, changed ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID_PASSWORD);
}
case ComputerAccessPayload.EJECT -> eject(player, computer, payload);
case ComputerAccessPayload.FILE_LIST -> fileList(player, computer, payload);
case ComputerAccessPayload.FILE_OPEN -> fileOpen(player, computer, payload);
case ComputerAccessPayload.FILE_CREATE -> fileCreate(player, computer, payload);
case ComputerAccessPayload.FILE_SAVE -> fileSave(player, computer, payload);
case ComputerAccessPayload.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));
}
}
@@ -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<ComputerAccessPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access"));
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerAccessPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, ComputerAccessPayload::pos,
ByteBufCodecs.VAR_INT, ComputerAccessPayload::action,
ByteBufCodecs.stringUtf8(65536), ComputerAccessPayload::value,
ComputerAccessPayload::new
);
public ComputerAccessPayload(BlockPos pos, int action) {
this(pos, action, "");
}
@Override
public Type<ComputerAccessPayload> type() {
return TYPE;
}
}
@@ -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<ComputerAccessResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access_result"));
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerAccessResultPayload> STREAM_CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, ComputerAccessResultPayload::pos,
ByteBufCodecs.VAR_INT, ComputerAccessResultPayload::result,
ByteBufCodecs.BOOL, ComputerAccessResultPayload::hasPassword,
ByteBufCodecs.BOOL, ComputerAccessResultPayload::authenticated,
ByteBufCodecs.stringUtf8(65536), ComputerAccessResultPayload::data,
ComputerAccessResultPayload::new
);
public ComputerAccessResultPayload(BlockPos pos, int result, boolean hasPassword, boolean authenticated) {
this(pos, result, hasPassword, authenticated, "");
}
@Override
public Type<ComputerAccessResultPayload> type() {
return TYPE;
}
}
@@ -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<ComputerAccessPayload> sender = payload -> {
};
private ComputerNetworking() {
}
public static void setSender(Consumer<ComputerAccessPayload> sender) {
ComputerNetworking.sender = sender;
}
public static void open(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.OPEN));
}
public static void setup(BlockPos pos, String password) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.SETUP, password));
}
public static void login(BlockPos pos, String password) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.LOGIN, password));
}
public static void logout(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.LOGOUT));
}
public static void close(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.CLOSE));
}
public static void changePassword(BlockPos pos, String password) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.CHANGE_PASSWORD, password));
}
public static void eject(BlockPos pos, ResourceLocation diskId) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.EJECT, diskId.toString()));
}
public static void listFiles(BlockPos pos) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_LIST));
}
public static void openFile(BlockPos pos, String path) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_OPEN, path));
}
public static void createFile(BlockPos pos, String path, String contents) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_CREATE, path + "\0" + contents));
}
public static void saveFile(BlockPos pos, String path, String contents) {
sender.accept(new ComputerAccessPayload(pos, ComputerAccessPayload.FILE_SAVE, path + "\0" + contents));
}
public static void 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()));
}
}
@@ -1,7 +1,9 @@
{ {
"variants": { "multipart": [
"": { {
"model": "antarchy:block/computer" "apply": {
"model": "antarchy:block/computer"
}
} }
} ]
} }
@@ -1053,6 +1053,7 @@
"subtitles.antarchy.king_death": "The King falls", "subtitles.antarchy.king_death": "The King falls",
"subtitles.antarchy.king_beam_shoot": "The King fires a beam", "subtitles.antarchy.king_beam_shoot": "The King fires a beam",
"subtitles.antarchy.king_fireball_shoot": "The King breathes fire", "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_cast": "The King pronounces a Decree",
"subtitles.antarchy.king_decree": "The King issues a Decree", "subtitles.antarchy.king_decree": "The King issues a Decree",
"subtitles.antarchy.king_success": "The King acknowledges compliance", "subtitles.antarchy.king_success": "The King acknowledges compliance",
@@ -1444,7 +1445,7 @@
,"block.antarchy.computer": "Computer" ,"block.antarchy.computer": "Computer"
,"item.antarchy.computer": "Computer" ,"item.antarchy.computer": "Computer"
,"item.antarchy.floppy_disk": "Floppy Disk" ,"item.antarchy.floppy_disk": "Floppy Disk"
,"screen.antarchy.computer": "ANTARCHY ARCHIVE" ,"screen.antarchy.computer": "ANTINTOSH OS"
,"guide.antarchy.category.general": "GENERAL" ,"guide.antarchy.category.general": "GENERAL"
,"guide.antarchy.category.elythia": "ELYTHIA" ,"guide.antarchy.category.elythia": "ELYTHIA"
,"guide.antarchy.category.thoraxis": "THORAXIS" ,"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.title": "High Fructose Corn Syrup"
,"guide.antarchy.entry.high_fructose_corn_syrup.subtitle": "Nutritional and operational notes" ,"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." ,"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"
} }
@@ -1,3 +1,3 @@
{ {
"parent": "antarchy:block/computer" "parent": "builtin/entity"
} }
@@ -2850,6 +2850,14 @@
} }
] ]
}, },
"king_ice_spikes": {
"subtitle": "subtitles.antarchy.king_ice_spikes",
"sounds": [
{
"name": "antarchy:mob/king/ice_spikes"
}
]
},
"king_decree_cast": { "king_decree_cast": {
"subtitle": "subtitles.antarchy.king_decree_cast", "subtitle": "subtitles.antarchy.king_decree_cast",
"sounds": [ "sounds": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,5 +1,5 @@
{ {
"category": "general", "category": "thoraxis",
"title": "guide.antarchy.disk.chronosphere.title", "title": "guide.antarchy.disk.chronosphere.title",
"entries": ["antarchy:chronosphere"] "entries": ["antarchy:chronosphere"]
} }
@@ -1,5 +0,0 @@
{
"category": "general",
"title": "guide.antarchy.disk.introduction.title",
"entries": ["antarchy:introduction"]
}
@@ -1,6 +1,6 @@
{ {
"type": "item", "type": "item",
"category": "general", "category": "thoraxis",
"title": "guide.antarchy.entry.chronosphere.title", "title": "guide.antarchy.entry.chronosphere.title",
"subtitle": "guide.antarchy.entry.chronosphere.subtitle", "subtitle": "guide.antarchy.entry.chronosphere.subtitle",
"description": ["guide.antarchy.entry.chronosphere.description"], "description": ["guide.antarchy.entry.chronosphere.description"],
@@ -60,8 +60,7 @@
"antarchy:biowart_patch", "antarchy:biowart_patch",
"antarchy:cavaryn_creepvine", "antarchy:cavaryn_creepvine",
"antarchy:cavaryn_hanging_creeproots" "antarchy:cavaryn_hanging_creeproots"
], ]
[]
], ],
"creature_spawn_probability": 0.98, "creature_spawn_probability": 0.98,
"spawners": { "spawners": {
@@ -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.
@@ -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
}
@@ -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
}
@@ -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}
}
@@ -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"]
}
@@ -321,6 +321,7 @@ public final class AntarchyFabricContent {
AntarchyFabricSounds.QUEEN_ROAR, AntarchyFabricSounds.QUEEN_ROAR,
AntarchyFabricSounds.KING_BEAM_SHOOT, AntarchyFabricSounds.KING_BEAM_SHOOT,
AntarchyFabricSounds.KING_FIREBALL_SHOOT, AntarchyFabricSounds.KING_FIREBALL_SHOOT,
AntarchyFabricSounds.KING_ICE_SPIKES,
AntarchyFabricSounds.KING_DECREE_CAST, AntarchyFabricSounds.KING_DECREE_CAST,
AntarchyFabricSounds.QUEEN_BEAM_SHOOT, AntarchyFabricSounds.QUEEN_BEAM_SHOOT,
AntarchyFabricSounds.QUEEN_BEAM_START, AntarchyFabricSounds.QUEEN_BEAM_START,
@@ -33,11 +33,6 @@ public final class AntarchyClothConfigScreenBuilder {
ConfigBuilder builder = ConfigBuilder.create() ConfigBuilder builder = ConfigBuilder.create()
.setParentScreen(parent) .setParentScreen(parent)
.setTitle(Component.translatable("antarchy.config.title")); .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); builder.setSavingRunnable(AntarchyConfigModuleFabric::persist);
ConfigEntryBuilder entryBuilder = builder.entryBuilder(); ConfigEntryBuilder entryBuilder = builder.entryBuilder();
@@ -74,11 +69,6 @@ public final class AntarchyClothConfigScreenBuilder {
.setSaveConsumer(binding::setValue) .setSaveConsumer(binding::setValue)
.build()); .build());
} }
// String-typed bindings back the ResourceKey<Level> 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) { private static String displayName(ConfigSection section) {
@@ -36,6 +36,11 @@ public final class AntarchyFabricClientNetworking {
} }
public static void register() { 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) -> ClientPlayNetworking.registerGlobalReceiver(GravityStatePayload.TYPE, (payload, context) ->
context.client().execute(() -> AntarchyFabricNetworking.handleGravityState(context.player(), payload))); context.client().execute(() -> AntarchyFabricNetworking.handleGravityState(context.player(), payload)));
ClientPlayNetworking.registerGlobalReceiver(BloodglassStatePayload.TYPE, (payload, context) -> ClientPlayNetworking.registerGlobalReceiver(BloodglassStatePayload.TYPE, (payload, context) ->
@@ -49,6 +49,9 @@ public final class AntarchyFabricNetworking {
public static void register() { public static void register() {
registerPayloadTypes(); registerPayloadTypes();
registerServerReceivers(); 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() { public static void bootstrapMultipartCommon() {
@@ -72,6 +75,8 @@ public final class AntarchyFabricNetworking {
} }
private static void registerPayloadTypes() { 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(GravityStatePayload.TYPE, GravityStatePayload.STREAM_CODEC);
PayloadTypeRegistry.playS2C().register(BloodglassStatePayload.TYPE, BloodglassStatePayload.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(BloodglassStatePayload.TYPE, BloodglassStatePayload.STREAM_CODEC);
PayloadTypeRegistry.playS2C().register(TigerEyeCamouflageStatePayload.TYPE, TigerEyeCamouflageStatePayload.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(HerculesBeetleMountedChargePayload.TYPE, HerculesBeetleMountedChargePayload.STREAM_CODEC);
PayloadTypeRegistry.playC2S().register(MultipartAttackPayload.TYPE, MultipartAttackPayload.STREAM_CODEC); PayloadTypeRegistry.playC2S().register(MultipartAttackPayload.TYPE, MultipartAttackPayload.STREAM_CODEC);
PayloadTypeRegistry.playC2S().register(MultipartInteractPayload.TYPE, MultipartInteractPayload.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() { 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) -> ServerPlayNetworking.registerGlobalReceiver(GravityGunPrimaryPayload.TYPE, (payload, context) ->
context.server().execute(() -> handleGravityGunPrimary(context.player(), payload))); context.server().execute(() -> handleGravityGunPrimary(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(PortalGunPrimaryPayload.TYPE, (payload, context) -> ServerPlayNetworking.registerGlobalReceiver(PortalGunPrimaryPayload.TYPE, (payload, context) ->
@@ -1548,9 +1548,6 @@ public final class AntarchyFabricItems {
() -> new EyeOfTheStormItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.EPIC))); () -> 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<MinersDreamItem> MINERS_DREAM = ITEMS.register("miners_dream",
// () -> new MinersDreamItem(new Item.Properties().stacksTo(1).rarity(Rarity.RARE)));
public static final DeferredItem<SquidzookaItem> SQUIDZOOKA = ITEMS.register("squidzooka", public static final DeferredItem<SquidzookaItem> SQUIDZOOKA = ITEMS.register("squidzooka",
@@ -542,6 +542,7 @@ public final class AntarchyFabricSounds {
public static final DeferredHolder<SoundEvent, SoundEvent> KING_DEATH = registerSoundEvent("king_death"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_DEATH = registerSoundEvent("king_death");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_BEAM_SHOOT = registerSoundEvent("king_beam_shoot"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_BEAM_SHOOT = registerSoundEvent("king_beam_shoot");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_FIREBALL_SHOOT = registerSoundEvent("king_fireball_shoot"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_FIREBALL_SHOOT = registerSoundEvent("king_fireball_shoot");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_ICE_SPIKES = registerSoundEvent("king_ice_spikes");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE_CAST = registerSoundEvent("king_decree_cast"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE_CAST = registerSoundEvent("king_decree_cast");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE = registerSoundEvent("king_decree"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE = registerSoundEvent("king_decree");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_SUCCESS = registerSoundEvent("king_success"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_SUCCESS = registerSoundEvent("king_success");
@@ -216,6 +216,7 @@ public class AntarchyNeoforge {
AntarchyNeoforgeSounds.QUEEN_ROAR, AntarchyNeoforgeSounds.QUEEN_ROAR,
AntarchyNeoforgeSounds.KING_BEAM_SHOOT, AntarchyNeoforgeSounds.KING_BEAM_SHOOT,
AntarchyNeoforgeSounds.KING_FIREBALL_SHOOT, AntarchyNeoforgeSounds.KING_FIREBALL_SHOOT,
AntarchyNeoforgeSounds.KING_ICE_SPIKES,
AntarchyNeoforgeSounds.KING_DECREE_CAST, AntarchyNeoforgeSounds.KING_DECREE_CAST,
AntarchyNeoforgeSounds.QUEEN_BEAM_SHOOT, AntarchyNeoforgeSounds.QUEEN_BEAM_SHOOT,
AntarchyNeoforgeSounds.QUEEN_BEAM_START, AntarchyNeoforgeSounds.QUEEN_BEAM_START,
@@ -287,6 +288,12 @@ public class AntarchyNeoforge {
AntarchyNeoforgeCreativeModeTabs.register(modEventBus); AntarchyNeoforgeCreativeModeTabs.register(modEventBus);
AntarchyNeoforgeEntityAttributes.register(modEventBus); AntarchyNeoforgeEntityAttributes.register(modEventBus);
AntarchyNeoforgePayloadHandlers.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(); Antarchy.init();
} }
@@ -1020,9 +1020,6 @@ public final class AntarchyNeoforgeItems {
)); ));
public static final DeferredItem<EyeOfTheStormItem> EYE_OF_THE_STORM = ITEMS.register("eye_of_the_storm", public static final DeferredItem<EyeOfTheStormItem> EYE_OF_THE_STORM = ITEMS.register("eye_of_the_storm",
() -> new EyeOfTheStormItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.EPIC))); () -> 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<MinersDreamItem> MINERS_DREAM = ITEMS.register("miners_dream",
// () -> new MinersDreamItem(new Item.Properties().stacksTo(1).rarity(Rarity.RARE)));
public static final DeferredItem<SquidzookaItem> SQUIDZOOKA = ITEMS.register("squidzooka", public static final DeferredItem<SquidzookaItem> SQUIDZOOKA = ITEMS.register("squidzooka",
() -> new SquidzookaItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.RARE))); () -> new SquidzookaItem(new Item.Properties().stacksTo(1).durability(384).rarity(Rarity.RARE)));
public static final DeferredItem<RpoLauncherItem> RPO_LAUNCHER = ITEMS.register("rpo_launcher", public static final DeferredItem<RpoLauncherItem> RPO_LAUNCHER = ITEMS.register("rpo_launcher",
@@ -1,7 +1,17 @@
package com.craisinlord.antarchy.neoforge.registry; package com.craisinlord.antarchy.neoforge.registry;
import com.craisinlord.antarchy.content.network.BloodCrystalKatanaTrailPayload; 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.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.item.TigerEyeArmorUtil;
import com.craisinlord.antarchy.content.tigereye.TigerEyeCamouflageController; import com.craisinlord.antarchy.content.tigereye.TigerEyeCamouflageController;
import com.craisinlord.antarchy.neoforge.network.AntarchyGravityNetworking; import com.craisinlord.antarchy.neoforge.network.AntarchyGravityNetworking;
@@ -23,6 +33,38 @@ public class AntarchyNeoforgePayloadHandlers {
static void registerPayloadHandlers(RegisterPayloadHandlersEvent event) { static void registerPayloadHandlers(RegisterPayloadHandlersEvent event) {
var registrar = event.registrar("1"); 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); AntarchyGravityNetworking.register(registrar);
AntarchyTimeDilationNetworking.register(registrar); AntarchyTimeDilationNetworking.register(registrar);
BrutalflyElytraNetworking.register(registrar); BrutalflyElytraNetworking.register(registrar);
@@ -225,6 +225,7 @@ public final class AntarchyNeoforgeSounds {
public static final DeferredHolder<SoundEvent, SoundEvent> KING_DEATH = registerSoundEvent("king_death"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_DEATH = registerSoundEvent("king_death");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_BEAM_SHOOT = registerSoundEvent("king_beam_shoot"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_BEAM_SHOOT = registerSoundEvent("king_beam_shoot");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_FIREBALL_SHOOT = registerSoundEvent("king_fireball_shoot"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_FIREBALL_SHOOT = registerSoundEvent("king_fireball_shoot");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_ICE_SPIKES = registerSoundEvent("king_ice_spikes");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE_CAST = registerSoundEvent("king_decree_cast"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE_CAST = registerSoundEvent("king_decree_cast");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE = registerSoundEvent("king_decree"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_DECREE = registerSoundEvent("king_decree");
public static final DeferredHolder<SoundEvent, SoundEvent> KING_SUCCESS = registerSoundEvent("king_success"); public static final DeferredHolder<SoundEvent, SoundEvent> KING_SUCCESS = registerSoundEvent("king_success");