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

@@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger;
public class Antarchy {
public static final String MODID = "antarchy";
public static final String MOD_VERSION = "2.0.0";
public static final Logger LOGGER = LogManager.getLogger();
public static void init() {
@@ -197,6 +197,7 @@ public final class AntarchySoundEvents {
public static Supplier<SoundEvent> QUEEN_ROAR = (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_ICE_SPIKES = (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_START = (Supplier<SoundEvent>) UNBOUND;
@@ -409,6 +410,7 @@ public final class AntarchySoundEvents {
Supplier<SoundEvent> queenRoar,
Supplier<SoundEvent> kingBeamShoot,
Supplier<SoundEvent> kingFireballShoot,
Supplier<SoundEvent> kingIceSpikes,
Supplier<SoundEvent> kingDecreeCast,
Supplier<SoundEvent> queenBeamShoot,
Supplier<SoundEvent> queenBeamStart,
@@ -617,6 +619,7 @@ public final class AntarchySoundEvents {
QUEEN_ROAR = queenRoar;
KING_BEAM_SHOOT = kingBeamShoot;
KING_FIREBALL_SHOOT = kingFireballShoot;
KING_ICE_SPIKES = kingIceSpikes;
KING_DECREE_CAST = kingDecreeCast;
QUEEN_BEAM_SHOOT = queenBeamShoot;
QUEEN_BEAM_START = queenBeamStart;
@@ -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.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.entity.player.Player;
@@ -13,7 +14,9 @@ import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -21,6 +24,8 @@ import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
@@ -28,11 +33,12 @@ import net.minecraft.world.phys.shapes.VoxelShape;
public final class ComputerBlock extends BaseEntityBlock {
public static final MapCodec<ComputerBlock> CODEC = Block.simpleCodec(ComputerBlock::new);
public static final BooleanProperty ACTIVE = BooleanProperty.create("active");
public static final DirectionProperty FACING = net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING;
private static final VoxelShape SHAPE = Block.box(1.0D, 0.0D, 1.0D, 15.0D, 16.0D, 15.0D);
public ComputerBlock(BlockBehaviour.Properties properties) {
super(properties.lightLevel(state -> state.getValue(ACTIVE) ? 7 : 0));
registerDefaultState(stateDefinition.any().setValue(ACTIVE, false));
registerDefaultState(stateDefinition.any().setValue(ACTIVE, false).setValue(FACING, Direction.NORTH));
}
@Override
@@ -42,7 +48,22 @@ public final class ComputerBlock extends BaseEntityBlock {
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<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
@@ -55,6 +76,25 @@ public final class ComputerBlock extends BaseEntityBlock {
return SHAPE;
}
@Override
public boolean isSignalSource(BlockState state) {
return true;
}
@Override
protected int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
return isAuthenticated(level, pos) ? 15 : 0;
}
@Override
protected int getDirectSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
return getSignal(state, level, pos, direction);
}
private static boolean isAuthenticated(BlockGetter level, BlockPos pos) {
return level.getBlockEntity(pos) instanceof ComputerBlockEntity computer && computer.isAuthenticated();
}
@Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
if (!state.is(newState.getBlock()) && level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
@@ -92,12 +132,6 @@ public final class ComputerBlock extends BaseEntityBlock {
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player,
BlockHitResult hitResult) {
if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
if (player.isShiftKeyDown()) {
if (computer.ejectOne(player)) {
return net.minecraft.world.InteractionResult.SUCCESS;
}
return net.minecraft.world.InteractionResult.PASS;
}
computer.activate();
if (level.isClientSide) {
AntarchyClientHooks.openComputer(pos);
@@ -2,6 +2,8 @@ package com.craisinlord.antarchy.content.block.entity;
import com.craisinlord.antarchy.content.AntarchyObjects;
import com.craisinlord.antarchy.content.block.ComputerBlock;
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
import com.craisinlord.antarchy.content.computer.ComputerDesktopState;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
@@ -10,12 +12,12 @@ import net.minecraft.nbt.StringTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.Containers;
import net.minecraft.world.entity.player.Player;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.LinkedHashSet;
@@ -31,15 +33,27 @@ import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.util.GeckoLibUtil;
public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEntity {
private static final ResourceLocation INTRODUCTION_DISK = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
private static final String DISKS_TAG = "Disks";
private static final String ACTIVE_UNTIL_TAG = "ActiveUntil";
private final Set<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 static final RawAnimation ON = RawAnimation.begin().thenLoop("on_state");
private static final RawAnimation OFF = RawAnimation.begin().thenLoop("off_state");
private static final RawAnimation TURN_ON = RawAnimation.begin().thenPlay("turn_on");
private static final RawAnimation TURN_OFF = RawAnimation.begin().thenPlay("turn_off");
private long activeUntil;
private String password = "";
private boolean authenticated;
private long authenticatedUntil;
private java.util.UUID activeUser;
private boolean lastActive;
public ComputerBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<ComputerBlockEntity>> type) {
@@ -60,6 +74,11 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
computer.setChanged();
level.sendBlockUpdated(pos, state, state, 3);
}
if (!level.isClientSide && computer.authenticated && (level.getGameTime() >= computer.authenticatedUntil || computer.activeUser == null ||
(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getServer().getPlayerList().getPlayer(computer.activeUser) == null))) {
computer.clearAuthentication();
level.sendBlockUpdated(pos, state, state, 3);
}
}
public boolean insert(ItemStack stack, Player player) {
@@ -67,12 +86,12 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
return false;
}
ResourceLocation diskId = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
if (diskId == null || this.diskIds.contains(diskId)) {
if (diskId == null || this.physicalDiskIds.contains(diskId)) {
stack.shrink(1);
activate();
return true;
}
this.diskIds.add(diskId);
this.physicalDiskIds.add(diskId);
stack.shrink(1);
activate();
setChanged();
@@ -94,12 +113,7 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
}
public boolean isActive() {
return level != null && (level.hasNeighborSignal(worldPosition) || level.getGameTime() < activeUntil || hasNearbyPlayer());
}
private boolean hasNearbyPlayer() {
Vec3 center = Vec3.atCenterOf(worldPosition);
return level.players().stream().anyMatch(player -> player.distanceToSqr(center) <= 25.0D);
return level != null && (level.hasNeighborSignal(worldPosition) || isAuthenticated());
}
@Override
@@ -116,15 +130,23 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
}
public List<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) {
if (level == null || level.isClientSide || diskIds.isEmpty()) {
if (physicalDiskIds.isEmpty() || !(player instanceof ServerPlayer serverPlayer)) {
return false;
}
return ejectOne(serverPlayer, physicalDiskIds.iterator().next());
}
public boolean ejectOne(ServerPlayer player, ResourceLocation diskId) {
if (level == null || level.isClientSide || diskId == null || !isAuthenticated() || !playerCanReach(player) || !physicalDiskIds.remove(diskId)) {
return false;
}
ResourceLocation diskId = diskIds.iterator().next();
diskIds.remove(diskId);
ItemStack disk = diskStack(diskId);
if (!player.addItem(disk)) {
player.drop(disk, false);
@@ -138,10 +160,158 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
if (level.isClientSide) {
return;
}
for (ResourceLocation diskId : diskIds) {
for (ResourceLocation diskId : physicalDiskIds) {
Containers.dropItemStack(level, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, diskStack(diskId));
}
diskIds.clear();
physicalDiskIds.clear();
}
public boolean hasPassword() {
return !password.isEmpty();
}
public boolean setPassword(ServerPlayer player, String newPassword) {
if (newPassword == null || newPassword.isBlank() || !isAuthenticated()) {
return false;
}
password = newPassword;
setChanged();
return true;
}
public boolean initializePassword(ServerPlayer player, String newPassword) {
if (hasPassword() || newPassword == null || newPassword.isBlank() || !claimUser(player)) {
return false;
}
password = newPassword;
authenticated = true;
authenticatedUntil = level.getGameTime() + AUTHENTICATION_TIMEOUT;
setChanged();
return true;
}
public boolean initializePassword(String newPassword) {
return false;
}
public boolean authenticate(ServerPlayer player, String attemptedPassword) {
if (!hasPassword() || !password.equals(attemptedPassword) || !claimUser(player)) {
return false;
}
authenticated = true;
authenticatedUntil = level == null ? 0L : level.getGameTime() + AUTHENTICATION_TIMEOUT;
setChanged();
return true;
}
public boolean authenticate(String attemptedPassword) {
return false;
}
public boolean isAuthenticated() {
if (!authenticated) {
return false;
}
if (level != null && level.getGameTime() >= authenticatedUntil) {
authenticated = false;
authenticatedUntil = 0L;
setChanged();
return false;
}
return true;
}
public boolean isAuthenticatedBy(ServerPlayer player) {
return isAuthenticated() && activeUser != null && activeUser.equals(player.getUUID());
}
public boolean claimUser(ServerPlayer player) {
if (level == null || level.isClientSide || !playerCanReach(player)) {
return false;
}
if (activeUser != null && !activeUser.equals(player.getUUID()) && isAuthenticated()) {
return false;
}
activeUser = player.getUUID();
return true;
}
public boolean playerCanReach(ServerPlayer player) {
return player.level() == level && player.distanceToSqr(worldPosition.getX() + 0.5D, worldPosition.getY() + 0.5D, worldPosition.getZ() + 0.5D) <= 64.0D;
}
public ComputerFileSystem fileSystem() {
return fileSystem;
}
public ComputerDesktopState desktopState() {
return desktopState;
}
public boolean unlockWallpaper(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.unlockWallpaper(id));
}
public boolean installProgram(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.installProgram(id));
}
public boolean uninstallProgram(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.uninstallProgram(id));
}
public boolean selectWallpaper(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.selectWallpaper(id));
}
public boolean setIconPosition(ResourceLocation id, int x, int y) {
return serverDesktopMutation(() -> desktopState.setIconPosition(id, x, y));
}
public boolean removeIconPosition(ResourceLocation id) {
return serverDesktopMutation(() -> desktopState.removeIconPosition(id));
}
private boolean serverDesktopMutation(java.util.function.BooleanSupplier mutation) {
if (level == null || level.isClientSide) {
return false;
}
boolean changed = mutation.getAsBoolean();
if (changed) {
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
}
return changed;
}
public boolean canUseFileSystem(ServerPlayer player) {
return level != null && !level.isClientSide && isAuthenticated() && playerCanReach(player) && (activeUser == null || activeUser.equals(player.getUUID()));
}
public void logout() {
clearAuthentication();
}
public void releaseUser(ServerPlayer player) {
if (activeUser != null && activeUser.equals(player.getUUID())) {
activeUser = null;
setChanged();
}
}
public void clearAuthentication() {
authenticated = false;
authenticatedUntil = 0L;
activeUser = null;
setChanged();
}
public long authenticationTimeout() {
return AUTHENTICATION_TIMEOUT;
}
public boolean hasActiveUser() {
return activeUser != null && isAuthenticated();
}
private static ItemStack diskStack(ResourceLocation diskId) {
@@ -153,32 +323,45 @@ public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEn
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
diskIds.clear();
physicalDiskIds.clear();
ListTag disks = tag.getList(DISKS_TAG, 8);
for (int index = 0; index < disks.size(); index++) {
try {
diskIds.add(ResourceLocation.parse(disks.getString(index)));
ResourceLocation diskId = ResourceLocation.parse(disks.getString(index));
if (!INTRODUCTION_DISK.equals(diskId)) {
physicalDiskIds.add(diskId);
}
} catch (Exception ignored) {
}
}
activeUntil = tag.getLong(ACTIVE_UNTIL_TAG);
password = tag.getString(PASSWORD_TAG);
fileSystem.load(tag, registries);
desktopState.load(tag.getCompound(DESKTOP_TAG));
authenticated = false;
authenticatedUntil = 0L;
activeUser = null;
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
ListTag disks = new ListTag();
for (ResourceLocation diskId : diskIds) {
for (ResourceLocation diskId : physicalDiskIds) {
disks.add(StringTag.valueOf(diskId.toString()));
}
tag.put(DISKS_TAG, disks);
tag.putLong(ACTIVE_UNTIL_TAG, activeUntil);
tag.putString(PASSWORD_TAG, password);
fileSystem.save(tag, registries);
tag.put(DESKTOP_TAG, desktopState.save());
}
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
CompoundTag tag = new CompoundTag();
saveAdditional(tag, registries);
tag.putBoolean("HasPassword", hasPassword());
tag.putBoolean(AUTHENTICATED_TAG, isAuthenticated());
return tag;
}
@@ -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.client.model.ComputerModel;
import net.minecraft.core.Direction;
import software.bernie.geckolib.renderer.GeoBlockRenderer;
public final class ComputerRenderer extends GeoBlockRenderer<ComputerBlockEntity> {
public ComputerRenderer(net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context context) {
super(new ComputerModel());
}
@Override
protected Direction getFacing(ComputerBlockEntity computer) {
return computer.getBlockState().getValue(com.craisinlord.antarchy.content.block.ComputerBlock.FACING);
}
}
@@ -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);
// Squid-style wander
private float wanderDx, wanderDy, wanderDz;
private int wanderRetargetTicks;
// Combat
private int attackCooldown;
private int actionTicks;
private int lastAttackState;
@@ -88,7 +86,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
private float orbitDirection = 1.0F;
private int inkSprayCooldown;
// Kraken spawn flag
private boolean spawnedByKraken;
// Explosive impact (fired from the R.P.O. Launcher)
@@ -364,10 +361,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
this.hasImpulse = true;
}
// -------------------------------------------------------------------------
// Squid-style wander
// -------------------------------------------------------------------------
private void tickWanderMovement() {
if (--this.wanderRetargetTicks <= 0) {
this.wanderRetargetTicks = 40 + this.random.nextInt(40);
@@ -386,10 +379,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
this.setDeltaMovement(this.getDeltaMovement().add(this.wanderDx, dy, this.wanderDz));
}
// -------------------------------------------------------------------------
// Ink spray
// -------------------------------------------------------------------------
private boolean hasHitExplosiveTarget() {
AABB box = this.getBoundingBox();
return !this.level().getEntitiesOfClass(LivingEntity.class, box,
@@ -445,10 +434,6 @@ public class OctopusBombEntity extends Monster implements GeoEntity {
serverLevel.sendParticles(OCTOPUS_BOMB_INK_PARTICLE, cx, cy, cz, 30, 3.5D, 1.8D, 3.5D, 0.02D);
}
// -------------------------------------------------------------------------
// Combat
// -------------------------------------------------------------------------
private void tickPursuitMovement(LivingEntity target) {
if (this.strafeRetargetTicks-- <= 0) {
this.strafeRetargetTicks = 10 + this.random.nextInt(12);
@@ -95,6 +95,7 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity {
private static final int NEST_SEARCH_INTERVAL_TICKS = 80;
private static final int FOOD_NEST_SEARCH_INTERVAL_TICKS = 20;
private static final int CHEST_SEARCH_INTERVAL_TICKS = 20;
private static final int CHEST_VALIDATION_INTERVAL_TICKS = 10;
private static final int SHARED_CHEST_MEMORY_TICKS = 120;
private static final double FOOD_CHEST_SEARCH_RADIUS = 20.0D;
private static final int GROUND_FOOD_CACHE_INTERVAL_TICKS = 20;
@@ -136,6 +137,10 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity {
private BlockPos targetFoodChestPos;
private int nextFoodChestSearchTick;
@Nullable
private BlockPos cachedFoodChestValidationPos;
private boolean cachedFoodChestValidationResult;
private int nextFoodChestValidationTick;
@Nullable
private BlockPos recentFoodChestPos;
private int recentFoodChestExpireTick;
@Nullable
@@ -749,17 +754,36 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity {
}
}
if (bestPos != null) {
this.cachedFoodChestValidationPos = bestPos;
this.cachedFoodChestValidationResult = true;
this.nextFoodChestValidationTick = this.tickCount + CHEST_VALIDATION_INTERVAL_TICKS;
}
return bestPos;
}
private boolean isValidFoodChest(BlockPos chestPos) {
if (this.cachedFoodChestValidationPos != null
&& this.cachedFoodChestValidationPos.equals(chestPos)
&& this.tickCount < this.nextFoodChestValidationTick) {
return this.cachedFoodChestValidationResult;
}
BlockEntity blockEntity = this.level().getBlockEntity(chestPos);
return blockEntity instanceof ChestBlockEntity chestBlockEntity && this.findStealableFoodSlot(chestBlockEntity) >= 0;
boolean valid = blockEntity instanceof ChestBlockEntity chestBlockEntity
&& this.findStealableFoodSlot(chestBlockEntity) >= 0;
this.cachedFoodChestValidationPos = chestPos.immutable();
this.cachedFoodChestValidationResult = valid;
this.nextFoodChestValidationTick = this.tickCount + CHEST_VALIDATION_INTERVAL_TICKS;
return valid;
}
private void clearFoodChestTarget() {
this.targetFoodChestPos = null;
this.nextFoodChestSearchTick = 0;
this.cachedFoodChestValidationPos = null;
this.cachedFoodChestValidationResult = false;
this.nextFoodChestValidationTick = 0;
}
private boolean stealFoodFromChest(BlockPos chestPos) {
@@ -1390,13 +1414,16 @@ public abstract class BaseAntEntity extends Animal implements GeoEntity {
return null;
}
if (leader.targetFoodChestPos != null && this.isValidFoodChest(leader.targetFoodChestPos)) {
if (leader.targetFoodChestPos != null
&& leader.cachedFoodChestValidationPos != null
&& leader.cachedFoodChestValidationPos.equals(leader.targetFoodChestPos)
&& leader.cachedFoodChestValidationResult
&& leader.tickCount < leader.nextFoodChestValidationTick) {
return leader.targetFoodChestPos;
}
if (leader.recentFoodChestPos != null
&& leader.tickCount <= leader.recentFoodChestExpireTick
&& this.isValidFoodChest(leader.recentFoodChestPos)) {
&& leader.tickCount <= leader.recentFoodChestExpireTick) {
return leader.recentFoodChestPos;
}
@@ -994,7 +994,7 @@ public class KingEntity extends RoyalBossEntity {
}
@Override public void onActive(int elapsedTicks) {
KingEntity.this.raiseIceSpikes(level, target);
KingEntity.this.playRoyalSound(AntarchySoundEvents.KING_BEAM_SHOOT.get(), 0.8F);
KingEntity.this.playRoyalSound(AntarchySoundEvents.KING_ICE_SPIKES.get(), 0.8F);
}
})) return;
this.iceSpikeCooldownTicks = AntarchySettings.kingIceSpikeCooldownTicks();
@@ -18,6 +18,7 @@ import java.util.Map;
public final class ComputerGuideData extends SimplePreparableReloadListener<ComputerGuideData.LoadedData> {
private static final ComputerGuideData INSTANCE = new ComputerGuideData();
private static final ResourceLocation INTRODUCTION = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
private static volatile Map<ResourceLocation, Entry> entries = Map.of();
private static volatile Map<ResourceLocation, Disk> disks = Map.of();
@@ -39,6 +40,10 @@ public final class ComputerGuideData extends SimplePreparableReloadListener<Comp
public static List<Entry> entriesFor(List<ResourceLocation> diskIds) {
Map<ResourceLocation, Entry> result = new LinkedHashMap<>();
for (ResourceLocation diskId : diskIds) {
if (INTRODUCTION.equals(diskId)) {
result.put(INTRODUCTION, new Entry(INTRODUCTION, "article", "general", "guide.antarchy.entry.introduction.title", "guide.antarchy.entry.introduction.subtitle", List.of("guide.antarchy.entry.introduction.description"), "", "", ""));
continue;
}
Disk disk = disks.get(diskId);
if (disk == null) {
continue;
@@ -176,7 +176,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
return;
}
// Toreterror jump tracking
if (isToreterrorJumping(stack) && player.getMainHandItem() == stack) {
player.fallDistance = 0.0F; // prevent fall damage during the crash-down
boolean launched = readInt(stack, TORETERROR_LAUNCHED_TAG) == 1;
@@ -197,7 +196,6 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
}
}
// Tick client shake counter
if (level.isClientSide && clientShakeTicks > 0) {
clientShakeTicks--;
}
@@ -18,7 +18,7 @@ import java.util.function.Consumer;
public final class ComputerItem extends BlockItem implements GeoItem {
private static final ResourceLocation MODEL = ResourceLocation.fromNamespaceAndPath("antarchy", "geo/computer.geo.json");
private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("antarchy", "textures/block/computer/computer.png");
private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("antarchy", "textures/item/computer.png");
private static final ResourceLocation ANIMATION = ResourceLocation.fromNamespaceAndPath("antarchy", "animations/computer.animation.json");
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
@@ -29,7 +29,7 @@ public final class ComputerItem extends BlockItem implements GeoItem {
@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "state", state -> state.setAndContinue(RawAnimation.begin().thenLoop("off_state"))));
controllers.add(new AnimationController<>(this, "state", state -> state.setAndContinue(RawAnimation.begin().thenLoop("on_state"))));
}
@Override
@@ -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": {
"": {
"model": "antarchy:block/computer"
"multipart": [
{
"apply": {
"model": "antarchy:block/computer"
}
}
}
]
}
@@ -1053,6 +1053,7 @@
"subtitles.antarchy.king_death": "The King falls",
"subtitles.antarchy.king_beam_shoot": "The King fires a beam",
"subtitles.antarchy.king_fireball_shoot": "The King breathes fire",
"subtitles.antarchy.king_ice_spikes": "The King summons ice spikes",
"subtitles.antarchy.king_decree_cast": "The King pronounces a Decree",
"subtitles.antarchy.king_decree": "The King issues a Decree",
"subtitles.antarchy.king_success": "The King acknowledges compliance",
@@ -1444,7 +1445,7 @@
,"block.antarchy.computer": "Computer"
,"item.antarchy.computer": "Computer"
,"item.antarchy.floppy_disk": "Floppy Disk"
,"screen.antarchy.computer": "ANTARCHY ARCHIVE"
,"screen.antarchy.computer": "ANTINTOSH OS"
,"guide.antarchy.category.general": "GENERAL"
,"guide.antarchy.category.elythia": "ELYTHIA"
,"guide.antarchy.category.thoraxis": "THORAXIS"
@@ -1467,4 +1468,21 @@
,"guide.antarchy.entry.high_fructose_corn_syrup.title": "High Fructose Corn Syrup"
,"guide.antarchy.entry.high_fructose_corn_syrup.subtitle": "Nutritional and operational notes"
,"guide.antarchy.entry.high_fructose_corn_syrup.description": "A sweet substance of uncertain strategic value. Doctoral enthusiasm surrounding it is statistically suspicious."
,"screen.antarchy.computer.first_use.title": "INITIAL PASSWORD SETUP"
,"screen.antarchy.computer.first_use.prompt": "CREATE A PASSWORD TO CONTINUE"
,"screen.antarchy.computer.first_use.confirm": "CONFIRM PASSWORD"
,"screen.antarchy.computer.busy": "COMPUTER IN USE"
,"screen.antarchy.computer.eject_denied": "ACCESS DENIED: SETTINGS REQUIRED"
,"screen.antarchy.computer.ejected": "DISK EJECTED"
,"antintosh.antarchy.desktop.archive": "ARCHIVE"
,"antintosh.antarchy.desktop.settings": "SETTINGS"
,"antintosh.antarchy.desktop.terminal": "TERMINAL"
,"antintosh.antarchy.desktop.antpaint": "ANTPAINT"
,"antintosh.antarchy.desktop.antmail": "ANTMAIL"
,"antintosh.antarchy.desktop.text_editor": "TEXT EDITOR"
,"antintosh.antarchy.antmail.compose": "COMPOSE MESSAGE"
,"antintosh.antarchy.antmail.attach_text": "ATTACH TEXT FILE"
,"antintosh.antarchy.antmail.attach_image": "ATTACH ANTPAINT IMAGE"
,"antintosh.antarchy.antmail.save_attachment": "SAVE ATTACHMENT"
,"antintosh.antarchy.antmail.address": "COMPUTER ADDRESS"
}
@@ -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": {
"subtitle": "subtitles.antarchy.king_decree_cast",
"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",
"entries": ["antarchy:chronosphere"]
}
@@ -1,5 +0,0 @@
{
"category": "general",
"title": "guide.antarchy.disk.introduction.title",
"entries": ["antarchy:introduction"]
}
@@ -1,6 +1,6 @@
{
"type": "item",
"category": "general",
"category": "thoraxis",
"title": "guide.antarchy.entry.chronosphere.title",
"subtitle": "guide.antarchy.entry.chronosphere.subtitle",
"description": ["guide.antarchy.entry.chronosphere.description"],
@@ -60,8 +60,7 @@
"antarchy:biowart_patch",
"antarchy:cavaryn_creepvine",
"antarchy:cavaryn_hanging_creeproots"
],
[]
]
],
"creature_spawn_probability": 0.98,
"spawners": {