infinite dimension fallback dims
seperated computer to different mod
This commit is contained in:
1 parent
c77d1da53b
commit
a72bd7d590
262 files changed
+1779
-9286
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -17,6 +17,7 @@ public final class AntarchySettings {
|
||||
private static boolean rainbowAntRequiresReagent = true;
|
||||
private static boolean rainbowAntRightClickDimension = true;
|
||||
private static boolean termiteRequiresReagent = false;
|
||||
private static boolean termitesFearBlockLight = true;
|
||||
private static boolean termiteRightClickDimension = true;
|
||||
private static boolean antsStealFromChests = true;
|
||||
private static boolean duplicatorTreeEnabled = true;
|
||||
@@ -261,14 +262,14 @@ public final class AntarchySettings {
|
||||
private static int royalGuardianBoundaryProjectileCap = 64;
|
||||
private static double royalBoltDamage = 6.0D;
|
||||
private static int royalEggHatchChance = 3;
|
||||
private static double princeHealth = 120.0D;
|
||||
private static double princeHealth = 360.0D;
|
||||
private static double princeAttackDamage = 14.0D;
|
||||
private static double princeMovementSpeed = 0.28D;
|
||||
private static double princeFlyingSpeed = 0.9D;
|
||||
private static double princeArmor = 10.0D;
|
||||
private static double princeKnockbackResistance = 0.6D;
|
||||
private static double princeFollowRange = 48.0D;
|
||||
private static double princessHealth = 110.0D;
|
||||
private static double princessHealth = 330.0D;
|
||||
private static double princessAttackDamage = 10.0D;
|
||||
private static double princessMovementSpeed = 0.28D;
|
||||
private static double princessFlyingSpeed = 0.95D;
|
||||
@@ -557,10 +558,10 @@ public final class AntarchySettings {
|
||||
private static int bloodCrystalLeggingsDefense = 6;
|
||||
private static int bloodCrystalBootsDefense = 3;
|
||||
private static double bloodCrystalArmorToughness = 2.0D;
|
||||
private static int bloodCrystalHelmetDurability = 77;
|
||||
private static int bloodCrystalChestplateDurability = 112;
|
||||
private static int bloodCrystalLeggingsDurability = 105;
|
||||
private static int bloodCrystalBootsDurability = 91;
|
||||
private static int bloodCrystalHelmetDurability = 92;
|
||||
private static int bloodCrystalChestplateDurability = 134;
|
||||
private static int bloodCrystalLeggingsDurability = 126;
|
||||
private static int bloodCrystalBootsDurability = 109;
|
||||
private static double toreterrorHealth = 450.0D;
|
||||
private static double toreterrorJumpAttackDamage = 14.0D;
|
||||
private static double toreterrorJumpAttackKnockback = 2.5D;
|
||||
@@ -705,6 +706,10 @@ public final class AntarchySettings {
|
||||
return termiteRequiresReagent;
|
||||
}
|
||||
|
||||
public static boolean termitesFearBlockLight() {
|
||||
return termitesFearBlockLight;
|
||||
}
|
||||
|
||||
public static boolean antsStealFromChests() {
|
||||
return antsStealFromChests;
|
||||
}
|
||||
@@ -1328,6 +1333,10 @@ public final class AntarchySettings {
|
||||
termiteRequiresReagent = value;
|
||||
}
|
||||
|
||||
public static void setTermitesFearBlockLight(boolean value) {
|
||||
termitesFearBlockLight = value;
|
||||
}
|
||||
|
||||
public static void setAntsStealFromChests(boolean value) {
|
||||
antsStealFromChests = value;
|
||||
}
|
||||
|
||||
@@ -206,16 +206,6 @@ public final class AntarchyObjects {
|
||||
public static Supplier<net.minecraft.core.component.DataComponentType<com.craisinlord.antarchy.content.entity.glimmer.GlimmerVariant>> GLIMMER_VARIANT_COMPONENT =
|
||||
(Supplier<net.minecraft.core.component.DataComponentType<com.craisinlord.antarchy.content.entity.glimmer.GlimmerVariant>>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<net.minecraft.core.component.DataComponentType<net.minecraft.resources.ResourceLocation>> FLOPPY_DISK_COMPONENT =
|
||||
(Supplier<net.minecraft.core.component.DataComponentType<net.minecraft.resources.ResourceLocation>>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<Item> FLOPPY_DISK = (Supplier<Item>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<Block> COMPUTER = (Supplier<Block>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<BlockEntityType<com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity>> COMPUTER_BLOCK_ENTITY =
|
||||
(Supplier<BlockEntityType<com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity>>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<MenuType<com.craisinlord.antarchy.content.menu.DorrieInventoryMenu>> DORRIE_INVENTORY_MENU = (Supplier<MenuType<com.craisinlord.antarchy.content.menu.DorrieInventoryMenu>>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<MenuType<com.craisinlord.antarchy.content.menu.RoyalJudgmentMenu>> ROYAL_JUDGMENT_MENU = (Supplier<MenuType<com.craisinlord.antarchy.content.menu.RoyalJudgmentMenu>>) UNBOUND;
|
||||
@@ -403,6 +393,8 @@ public final class AntarchyObjects {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<SimpleParticleType> STINKY_GAS = (Supplier<SimpleParticleType>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<SimpleParticleType> BUG_SPRAY_PARTICLE = (Supplier<SimpleParticleType>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<SimpleParticleType> NIGHTMARE_FIRE_FLAME = (Supplier<SimpleParticleType>) UNBOUND;
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Supplier<SimpleParticleType> STINKY_FLY = (Supplier<SimpleParticleType>) UNBOUND;
|
||||
@@ -650,6 +642,10 @@ public final class AntarchyObjects {
|
||||
OCTOPUS_BOMB = supplier;
|
||||
}
|
||||
|
||||
public static void setBugSprayParticle(Supplier<SimpleParticleType> supplier) {
|
||||
BUG_SPRAY_PARTICLE = supplier;
|
||||
}
|
||||
|
||||
public static void setTentacle(Supplier<EntityType<TentacleEntity>> supplier) {
|
||||
TENTACLE = supplier;
|
||||
}
|
||||
@@ -912,22 +908,6 @@ public final class AntarchyObjects {
|
||||
GLIMMER_VARIANT_COMPONENT = supplier;
|
||||
}
|
||||
|
||||
public static void setFloppyDiskComponent(Supplier<net.minecraft.core.component.DataComponentType<net.minecraft.resources.ResourceLocation>> supplier) {
|
||||
FLOPPY_DISK_COMPONENT = supplier;
|
||||
}
|
||||
|
||||
public static void setFloppyDisk(Supplier<Item> supplier) {
|
||||
FLOPPY_DISK = supplier;
|
||||
}
|
||||
|
||||
public static void setComputer(Supplier<Block> supplier) {
|
||||
COMPUTER = supplier;
|
||||
}
|
||||
|
||||
public static void setComputerBlockEntity(Supplier<BlockEntityType<com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity>> supplier) {
|
||||
COMPUTER_BLOCK_ENTITY = supplier;
|
||||
}
|
||||
|
||||
public static void setDorrieInventoryMenu(Supplier<MenuType<com.craisinlord.antarchy.content.menu.DorrieInventoryMenu>> supplier) {
|
||||
DORRIE_INVENTORY_MENU = supplier;
|
||||
}
|
||||
|
||||
@@ -221,6 +221,9 @@ public final class AntarchySoundEvents {
|
||||
public static Supplier<SoundEvent> KING_DECREE = (Supplier<SoundEvent>) UNBOUND;
|
||||
public static Supplier<SoundEvent> KING_SUCCESS = (Supplier<SoundEvent>) UNBOUND;
|
||||
public static Supplier<SoundEvent> KING_JUDGEMENT = (Supplier<SoundEvent>) UNBOUND;
|
||||
public static Supplier<SoundEvent> HOVERBOARD_MOUNT = (Supplier<SoundEvent>) UNBOUND;
|
||||
public static Supplier<SoundEvent> HOVERBOARD_DISMOUNT = (Supplier<SoundEvent>) UNBOUND;
|
||||
public static Supplier<SoundEvent> HOVERBOARD_IDLE = (Supplier<SoundEvent>) UNBOUND;
|
||||
|
||||
private AntarchySoundEvents() {
|
||||
}
|
||||
@@ -656,4 +659,10 @@ public final class AntarchySoundEvents {
|
||||
DIMENSIONAL_TEAR_CREATURE_LEAVES = dimensionalTearCreatureLeaves;
|
||||
DIMENSIONAL_TEAR_CLOSE = dimensionalTearClose;
|
||||
}
|
||||
|
||||
public static void bindHoverboardSounds(Supplier<SoundEvent> mount, Supplier<SoundEvent> dismount, Supplier<SoundEvent> idle) {
|
||||
HOVERBOARD_MOUNT = mount;
|
||||
HOVERBOARD_DISMOUNT = dismount;
|
||||
HOVERBOARD_IDLE = idle;
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public record AntmailAddress(String username) {
|
||||
public static final String DOMAIN = "antmail.com";
|
||||
private static final Pattern USERNAME = Pattern.compile("[a-z0-9_]{3,16}");
|
||||
|
||||
public AntmailAddress {
|
||||
username = normalizeUsername(username);
|
||||
}
|
||||
|
||||
public static AntmailAddress parse(String value) {
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("Address is null");
|
||||
}
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
String suffix = "@" + DOMAIN;
|
||||
if (!normalized.endsWith(suffix)) {
|
||||
throw new IllegalArgumentException("Invalid Antmail domain");
|
||||
}
|
||||
return new AntmailAddress(normalized.substring(0, normalized.length() - suffix.length()));
|
||||
}
|
||||
|
||||
public static AntmailAddress ofUsername(String username) {
|
||||
return new AntmailAddress(username);
|
||||
}
|
||||
|
||||
public String fullAddress() {
|
||||
return username + "@" + DOMAIN;
|
||||
}
|
||||
|
||||
public static String normalizeUsername(String value) {
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("Username is null");
|
||||
}
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
if (!USERNAME.matcher(normalized).matches()) {
|
||||
throw new IllegalArgumentException("Username must match [a-z0-9_]{3,16}");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static boolean isValidUsername(String value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return USERNAME.matcher(value.trim().toLowerCase(Locale.ROOT)).matches();
|
||||
}
|
||||
|
||||
public static boolean isValidAddress(String value) {
|
||||
try {
|
||||
parse(value);
|
||||
return true;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return fullAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return object instanceof AntmailAddress other && username.equals(other.username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(username);
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public sealed interface AntmailAttachment permits AntmailAttachment.TextFile, AntmailAttachment.PaintImage {
|
||||
String TYPE_TAG = "Type";
|
||||
String NAME_TAG = "Name";
|
||||
|
||||
String fileName();
|
||||
|
||||
int byteSize();
|
||||
|
||||
CompoundTag toTag();
|
||||
|
||||
static AntmailAttachment fromTag(CompoundTag tag) {
|
||||
return switch (tag.getString(TYPE_TAG)) {
|
||||
case "text_file" -> TextFile.fromTag(tag);
|
||||
case "paint_image" -> PaintImage.fromTag(tag);
|
||||
default -> throw new IllegalArgumentException("Unknown Antmail attachment type");
|
||||
};
|
||||
}
|
||||
|
||||
record TextFile(String fileName, String contents) implements AntmailAttachment {
|
||||
public TextFile {
|
||||
fileName = validateFileName(fileName);
|
||||
contents = Objects.requireNonNull(contents, "contents");
|
||||
if (contents.length() > AntmailValidation.MAX_TEXT_ATTACHMENT_CHARACTERS) {
|
||||
throw new IllegalArgumentException("Text attachment is too large");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int byteSize() {
|
||||
return contents.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag toTag() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putString(TYPE_TAG, "text_file");
|
||||
tag.putString(NAME_TAG, fileName);
|
||||
tag.putString("Contents", contents);
|
||||
return tag;
|
||||
}
|
||||
|
||||
static TextFile fromTag(CompoundTag tag) {
|
||||
return new TextFile(tag.getString(NAME_TAG), tag.getString("Contents"));
|
||||
}
|
||||
}
|
||||
|
||||
record PaintImage(String fileName, int width, int height, byte[] pixels) implements AntmailAttachment {
|
||||
public PaintImage {
|
||||
fileName = validateFileName(fileName);
|
||||
if (width != AntmailValidation.MAX_PAINT_WIDTH || height != AntmailValidation.MAX_PAINT_HEIGHT) {
|
||||
throw new IllegalArgumentException("Invalid Paint image dimensions");
|
||||
}
|
||||
if (pixels == null || pixels.length != width * height) {
|
||||
throw new IllegalArgumentException("Paint pixel data does not match dimensions");
|
||||
}
|
||||
for (byte pixel : pixels) {
|
||||
if (pixel != 0 && pixel != 1) throw new IllegalArgumentException("Paint pixel data must be monochrome");
|
||||
}
|
||||
pixels = Arrays.copyOf(pixels, pixels.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] pixels() {
|
||||
return Arrays.copyOf(pixels, pixels.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int byteSize() {
|
||||
return pixels.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag toTag() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putString(TYPE_TAG, "paint_image");
|
||||
tag.putString(NAME_TAG, fileName);
|
||||
tag.putInt("Width", width);
|
||||
tag.putInt("Height", height);
|
||||
tag.putByteArray("Pixels", pixels);
|
||||
return tag;
|
||||
}
|
||||
|
||||
static PaintImage fromTag(CompoundTag tag) {
|
||||
return new PaintImage(tag.getString(NAME_TAG), tag.getInt("Width"), tag.getInt("Height"), tag.getByteArray("Pixels"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String validateFileName(String value) {
|
||||
if (value == null || value.isBlank() || value.length() > AntmailValidation.MAX_FILE_NAME_LENGTH || value.contains("/") || value.contains("\\")) {
|
||||
throw new IllegalArgumentException("Invalid attachment filename");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
|
||||
import com.craisinlord.antarchy.content.computer.paint.AntPaintCanvas;
|
||||
import com.craisinlord.antarchy.content.computer.paint.AntPaintFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class AntmailAttachmentFiles {
|
||||
private AntmailAttachmentFiles() {
|
||||
}
|
||||
|
||||
public static Optional<AntmailAttachment> fromComputerFile(ComputerFileSystem.ComputerFile file) {
|
||||
if (file == null || file.type() != ComputerFileSystem.ComputerFile.Type.TEXT) return Optional.empty();
|
||||
try {
|
||||
return Optional.of(new AntmailAttachment.TextFile(filename(file.path()), file.contents()));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static AntmailAttachment.TextFile fromTextFile(String path, String contents) {
|
||||
return new AntmailAttachment.TextFile(filename(path), contents);
|
||||
}
|
||||
|
||||
public static AntmailAttachment.PaintImage fromPaintFile(AntPaintFile file) {
|
||||
if (file == null) throw new IllegalArgumentException("Paint file cannot be null");
|
||||
AntPaintCanvas canvas = file.canvas();
|
||||
if (canvas == null || canvas.copyPixels().length != AntPaintCanvas.PIXEL_COUNT) {
|
||||
throw new IllegalArgumentException("Invalid AntPaint canvas");
|
||||
}
|
||||
boolean[] source = canvas.copyPixels();
|
||||
byte[] pixels = new byte[source.length];
|
||||
for (int index = 0; index < source.length; index++) pixels[index] = source[index] ? (byte) 1 : (byte) 0;
|
||||
return new AntmailAttachment.PaintImage(file.filename(), AntPaintCanvas.WIDTH, AntPaintCanvas.HEIGHT, pixels);
|
||||
}
|
||||
|
||||
public static AntPaintFile toPaintFile(AntmailAttachment.PaintImage image, String fileId, long createdTime, long modifiedTime) {
|
||||
if (image == null) throw new IllegalArgumentException("Paint attachment cannot be null");
|
||||
if (image.width() != AntPaintCanvas.WIDTH || image.height() != AntPaintCanvas.HEIGHT) {
|
||||
throw new IllegalArgumentException("Unsupported AntPaint canvas size");
|
||||
}
|
||||
byte[] source = image.pixels();
|
||||
boolean[] pixels = new boolean[source.length];
|
||||
for (int index = 0; index < source.length; index++) {
|
||||
if (source[index] != 0 && source[index] != 1) throw new IllegalArgumentException("Invalid AntPaint pixel");
|
||||
pixels[index] = source[index] == 1;
|
||||
}
|
||||
AntPaintCanvas canvas = new AntPaintCanvas();
|
||||
for (int index = 0; index < pixels.length; index++) {
|
||||
if (pixels[index]) canvas.set(index % AntPaintCanvas.WIDTH, index / AntPaintCanvas.WIDTH, true);
|
||||
}
|
||||
return new AntPaintFile(fileId, image.fileName(), canvas, createdTime, modifiedTime);
|
||||
}
|
||||
|
||||
public static ComputerFileSystem.ComputerFile toComputerTextFile(String path, AntmailAttachment.TextFile file) {
|
||||
if (file == null) throw new IllegalArgumentException("Text attachment cannot be null");
|
||||
String normalized = ComputerFileSystem.normalize(path);
|
||||
if (normalized.isEmpty() || normalized.equals(ComputerFileSystem.ROOT)) throw new IllegalArgumentException("Invalid text file path");
|
||||
return ComputerFileSystem.ComputerFile.text(normalized, file.contents());
|
||||
}
|
||||
|
||||
public static List<ComputerFileSystem.ComputerFile> selectableTextFiles(Collection<ComputerFileSystem.ComputerFile> files) {
|
||||
if (files == null) return List.of();
|
||||
List<ComputerFileSystem.ComputerFile> result = new ArrayList<>();
|
||||
for (ComputerFileSystem.ComputerFile file : files) {
|
||||
if (fromComputerFile(file).isPresent()) result.add(file);
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public static List<AntmailAttachment> selectablePaintFiles(Collection<AntPaintFile> files) {
|
||||
if (files == null) return List.of();
|
||||
List<AntmailAttachment> result = new ArrayList<>();
|
||||
for (AntPaintFile file : files) {
|
||||
try {
|
||||
result.add(fromPaintFile(file));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static String filename(String path) {
|
||||
String normalized = ComputerFileSystem.normalize(path);
|
||||
if (normalized.isEmpty() || normalized.equals(ComputerFileSystem.ROOT)) throw new IllegalArgumentException("Invalid file path");
|
||||
int separator = normalized.lastIndexOf('/');
|
||||
return normalized.substring(separator + 1);
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
|
||||
/** Opt-in diagnostics for AntMail networking and mailbox state transitions. */
|
||||
public final class AntmailDebug {
|
||||
private static final String PROPERTY = "antarchy.antmail.debug";
|
||||
|
||||
private AntmailDebug() {
|
||||
}
|
||||
|
||||
public static void log(String message) {
|
||||
if (Boolean.getBoolean(PROPERTY)) Antarchy.LOGGER.info("[AntMail debug] {}", message);
|
||||
}
|
||||
|
||||
public static void error(String message, Throwable throwable) {
|
||||
Antarchy.LOGGER.error("[AntMail] {}", message, throwable);
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record AntmailDeliveryResult(Status status, UUID messageId, AntmailAddress sender, AntmailAddress recipient, String detail) {
|
||||
public enum Status {
|
||||
DELIVERED,
|
||||
QUEUED,
|
||||
INVALID_ADDRESS,
|
||||
ADDRESS_NOT_FOUND,
|
||||
MAILBOX_FULL,
|
||||
MESSAGE_INVALID,
|
||||
DUPLICATE,
|
||||
FAILED
|
||||
}
|
||||
|
||||
public static AntmailDeliveryResult delivered(AntmailMessage message) {
|
||||
return new AntmailDeliveryResult(Status.DELIVERED, message.id(), message.sender(), message.recipient(), "");
|
||||
}
|
||||
|
||||
public static AntmailDeliveryResult queued(AntmailMessage message) {
|
||||
return new AntmailDeliveryResult(Status.QUEUED, message.id(), message.sender(), message.recipient(), "");
|
||||
}
|
||||
|
||||
public static AntmailDeliveryResult failed(Status status, AntmailMessage message, String detail) {
|
||||
return new AntmailDeliveryResult(status, message == null ? null : message.id(), message == null ? null : message.sender(), message == null ? null : message.recipient(), detail);
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record AntmailDraft(UUID id, AntmailAddress sender, AntmailAddress recipient, String subject, String body, List<AntmailAttachment> attachments) {
|
||||
public AntmailDraft {
|
||||
id = id == null ? UUID.randomUUID() : id;
|
||||
subject = subject == null ? "" : subject;
|
||||
body = body == null ? "" : body;
|
||||
attachments = attachments == null ? List.of() : List.copyOf(attachments);
|
||||
if (subject.length() > AntmailValidation.MAX_SUBJECT_LENGTH || body.length() > AntmailValidation.MAX_BODY_LENGTH || attachments.size() > AntmailValidation.MAX_ATTACHMENTS) {
|
||||
throw new IllegalArgumentException("Draft exceeds Antmail limits");
|
||||
}
|
||||
}
|
||||
|
||||
public CompoundTag toTag() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putString("Id", id.toString());
|
||||
if (sender != null) tag.putString("From", sender.fullAddress());
|
||||
if (recipient != null) tag.putString("To", recipient.fullAddress());
|
||||
tag.putString("Subject", subject);
|
||||
tag.putString("Body", body);
|
||||
ListTag attachmentTags = new ListTag();
|
||||
for (AntmailAttachment attachment : attachments) attachmentTags.add(attachment.toTag());
|
||||
tag.put("Attachments", attachmentTags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static AntmailDraft fromTag(CompoundTag tag) {
|
||||
List<AntmailAttachment> attachments = new ArrayList<>();
|
||||
ListTag attachmentTags = tag.getList("Attachments", 10);
|
||||
for (int index = 0; index < attachmentTags.size(); index++) attachments.add(AntmailAttachment.fromTag(attachmentTags.getCompound(index)));
|
||||
UUID id = UUID.fromString(tag.getString("Id"));
|
||||
AntmailAddress sender = tag.contains("From") ? AntmailAddress.parse(tag.getString("From")) : null;
|
||||
AntmailAddress recipient = tag.contains("To") ? AntmailAddress.parse(tag.getString("To")) : null;
|
||||
return new AntmailDraft(id, sender, recipient, tag.getString("Subject"), tag.getString("Body"), attachments);
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.advancements.AdvancementHolder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class AntmailEventData extends SimplePreparableReloadListener<Map<ResourceLocation, AntmailEventData.Definition>> {
|
||||
private static final AntmailEventData INSTANCE = new AntmailEventData();
|
||||
private static volatile Map<ResourceLocation, Definition> definitions = Map.of();
|
||||
private AntmailEventData() {
|
||||
}
|
||||
|
||||
public static AntmailEventData instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static void onTick(MinecraftServer server) {
|
||||
long dayTime = server.overworld().getDayTime();
|
||||
long day = dayTime / 24000L;
|
||||
if (dayTime % 24000L >= 20L) return;
|
||||
AntmailServerData data = AntmailServerData.access(server);
|
||||
if (!data.beginRandomMailDay(day)) return;
|
||||
for (AntmailAddress address : data.registeredAddresses()) {
|
||||
for (Map.Entry<ResourceLocation, Definition> entry : definitions.entrySet()) {
|
||||
Definition definition = entry.getValue();
|
||||
if (definition.trigger().type().equals("random") && server.overworld().random.nextDouble() < definition.trigger().chance()) deliver(server, address, entry.getKey(), definition, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onAdvancement(ServerPlayer player, AdvancementHolder advancement) {
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress address = data.lastUsedAddress(player);
|
||||
if (address == null) return;
|
||||
for (Map.Entry<ResourceLocation, Definition> entry : definitions.entrySet()) {
|
||||
Trigger trigger = entry.getValue().trigger();
|
||||
if (trigger.type().equals("advancement") && trigger.value().equals(advancement.id().toString())) deliver(player.server, address, entry.getKey(), entry.getValue(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void deliver(MinecraftServer server, AntmailAddress recipient, ResourceLocation id, Definition definition, boolean permanentClaim) {
|
||||
AntmailServerData data = AntmailServerData.access(server);
|
||||
try {
|
||||
AntmailAddress sender = AntmailAddress.parse(definition.sender());
|
||||
AntmailMessage message = AntmailMessage.create(sender, recipient, definition.subject(), definition.body(), server.overworld().getGameTime(), List.of());
|
||||
AntmailDeliveryResult result = data.deliver(server, message);
|
||||
if (permanentClaim && result.status() != AntmailDeliveryResult.Status.MAILBOX_FULL
|
||||
&& result.status() != AntmailDeliveryResult.Status.FAILED
|
||||
&& result.status() != AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND) {
|
||||
data.claimTrigger(recipient, id);
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<ResourceLocation, Definition> prepare(ResourceManager manager, ProfilerFiller profiler) {
|
||||
Map<ResourceLocation, Definition> loaded = new HashMap<>();
|
||||
for (Map.Entry<ResourceLocation, net.minecraft.server.packs.resources.Resource> resource : manager.listResources("antmail/message", path -> path.getPath().endsWith(".json")).entrySet()) {
|
||||
try {
|
||||
JsonElement element = JsonParser.parseReader(resource.getValue().openAsReader());
|
||||
if (!element.isJsonObject()) continue;
|
||||
var object = element.getAsJsonObject();
|
||||
var trigger = object.getAsJsonObject("trigger");
|
||||
String type = trigger.get("type").getAsString();
|
||||
String value = trigger.has("value") ? trigger.get("value").getAsString() : String.valueOf(trigger.get("day").getAsInt());
|
||||
ResourceLocation id = ResourceLocation.fromNamespaceAndPath(resource.getKey().getNamespace(), resource.getKey().getPath().substring("antmail/message/".length(), resource.getKey().getPath().length() - 5));
|
||||
loaded.put(id, new Definition(object.get("sender").getAsString(), object.get("subject").getAsString(), object.get("body").getAsString(), new Trigger(type, value)));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return Map.copyOf(loaded);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Map<ResourceLocation, Definition> loaded, ResourceManager manager, ProfilerFiller profiler) {
|
||||
definitions = loaded;
|
||||
}
|
||||
|
||||
public record Definition(String sender, String subject, String body, Trigger trigger) {
|
||||
}
|
||||
|
||||
public record Trigger(String type, String value) {
|
||||
double chance() {
|
||||
try {
|
||||
return Math.max(0.0D, Math.min(1.0D, Double.parseDouble(value)));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0.0D;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AntmailMailbox {
|
||||
public static final int PAGE_SIZE = 20;
|
||||
private final AntmailAddress address;
|
||||
private final List<AntmailMessage> inbox = new ArrayList<>();
|
||||
private final List<AntmailMessage> sent = new ArrayList<>();
|
||||
private final List<AntmailDraft> drafts = new ArrayList<>();
|
||||
private int reportedUnreadCount = -1;
|
||||
|
||||
public AntmailMailbox(AntmailAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public AntmailAddress address() { return address; }
|
||||
public List<AntmailMessage> inbox() { return List.copyOf(inbox); }
|
||||
public List<AntmailMessage> sent() { return List.copyOf(sent); }
|
||||
public List<AntmailDraft> drafts() { return List.copyOf(drafts); }
|
||||
public int unreadCount() { return reportedUnreadCount >= 0 ? reportedUnreadCount : (int) inbox.stream().filter(message -> !message.read()).count(); }
|
||||
|
||||
public boolean addIncoming(AntmailMessage message) {
|
||||
if (!address.equals(message.recipient()) || inbox.size() >= AntmailValidation.MAX_MAILBOX_MESSAGES || contains(inbox, message.id())) return false;
|
||||
inbox.add(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean addSent(AntmailMessage message) {
|
||||
if (!address.equals(message.sender()) || sent.size() >= AntmailValidation.MAX_MAILBOX_MESSAGES || contains(sent, message.id())) return false;
|
||||
sent.add(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean saveDraft(AntmailDraft draft) {
|
||||
if (!address.equals(draft.sender()) || drafts.size() >= AntmailValidation.MAX_DRAFTS && findDraft(draft.id()) == null) return false;
|
||||
drafts.removeIf(existing -> existing.id().equals(draft.id()));
|
||||
drafts.add(draft);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeDraft(UUID id) { return drafts.removeIf(draft -> draft.id().equals(id)); }
|
||||
public boolean removeInbox(UUID id) { return inbox.removeIf(message -> message.id().equals(id)); }
|
||||
public boolean removeSent(UUID id) { return sent.removeIf(message -> message.id().equals(id)); }
|
||||
public AntmailMessage findSent(UUID id) { return sent.stream().filter(message -> message.id().equals(id)).findFirst().orElse(null); }
|
||||
public AntmailMessage findInbox(UUID id) { return inbox.stream().filter(message -> message.id().equals(id)).findFirst().orElse(null); }
|
||||
|
||||
public boolean markRead(UUID id) {
|
||||
for (AntmailMessage message : inbox) if (message.id().equals(id)) { message.markRead(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
public AntmailDraft findDraft(UUID id) { return drafts.stream().filter(draft -> draft.id().equals(id)).findFirst().orElse(null); }
|
||||
|
||||
public CompoundTag toTag() {
|
||||
return toTag(true);
|
||||
}
|
||||
|
||||
public CompoundTag toTag(boolean includeMessageAttachments) {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putString("Address", address.fullAddress());
|
||||
tag.put("Inbox", messagesToTag(inbox, includeMessageAttachments));
|
||||
tag.put("Sent", messagesToTag(sent, includeMessageAttachments));
|
||||
ListTag draftTags = new ListTag();
|
||||
for (AntmailDraft draft : drafts) draftTags.add(draft.toTag());
|
||||
tag.put("Drafts", draftTags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public CompoundTag toPageTag(int folder, int page) {
|
||||
int safePage = Math.max(0, page);
|
||||
int start = safePage * PAGE_SIZE;
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putString("Address", address.fullAddress());
|
||||
tag.putInt("Folder", folder);
|
||||
tag.putInt("Page", safePage);
|
||||
tag.putInt("PageSize", PAGE_SIZE);
|
||||
tag.putInt("TotalInbox", inbox.size());
|
||||
tag.putInt("TotalSent", sent.size());
|
||||
tag.putInt("UnreadCount", (int) inbox.stream().filter(message -> !message.read()).count());
|
||||
tag.put("Inbox", folder == 0 ? messagesToTag(page(inbox, start), false) : new ListTag());
|
||||
tag.put("Sent", folder == 1 ? messagesToTag(page(sent, start), false) : new ListTag());
|
||||
ListTag draftTags = new ListTag();
|
||||
for (AntmailDraft draft : drafts) draftTags.add(draft.toTag());
|
||||
tag.put("Drafts", draftTags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static <T> List<T> page(List<T> values, int start) {
|
||||
if (start >= values.size()) return List.of();
|
||||
return values.subList(start, Math.min(start + PAGE_SIZE, values.size()));
|
||||
}
|
||||
|
||||
public static AntmailMailbox fromTag(CompoundTag tag) {
|
||||
AntmailMailbox mailbox = new AntmailMailbox(AntmailAddress.parse(tag.getString("Address")));
|
||||
if (tag.contains("UnreadCount")) mailbox.reportedUnreadCount = Math.max(0, tag.getInt("UnreadCount"));
|
||||
readMessages(tag.getList("Inbox", 10), mailbox.inbox);
|
||||
readMessages(tag.getList("Sent", 10), mailbox.sent);
|
||||
ListTag draftTags = tag.getList("Drafts", 10);
|
||||
for (int index = 0; index < draftTags.size() && mailbox.drafts.size() < AntmailValidation.MAX_DRAFTS; index++) {
|
||||
try { mailbox.drafts.add(AntmailDraft.fromTag(draftTags.getCompound(index))); } catch (RuntimeException ignored) { }
|
||||
}
|
||||
return mailbox;
|
||||
}
|
||||
|
||||
private static ListTag messagesToTag(List<AntmailMessage> messages, boolean includeMessageAttachments) {
|
||||
ListTag tags = new ListTag();
|
||||
for (AntmailMessage message : messages) tags.add(message.toTag(includeMessageAttachments));
|
||||
return tags;
|
||||
}
|
||||
|
||||
private static void readMessages(ListTag tags, List<AntmailMessage> target) {
|
||||
for (int index = 0; index < tags.size() && target.size() < AntmailValidation.MAX_MAILBOX_MESSAGES; index++) {
|
||||
try { target.add(AntmailMessage.fromTag(tags.getCompound(index))); } catch (RuntimeException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean contains(List<AntmailMessage> messages, UUID id) {
|
||||
return messages.stream().anyMatch(message -> message.id().equals(id));
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class AntmailMailboxStore {
|
||||
private final Map<AntmailAddress, AntmailMailbox> mailboxes = new LinkedHashMap<>();
|
||||
|
||||
public AntmailMailbox getOrCreate(AntmailAddress address) {
|
||||
return mailboxes.computeIfAbsent(address, AntmailMailbox::new);
|
||||
}
|
||||
|
||||
public AntmailMailbox get(AntmailAddress address) {
|
||||
return mailboxes.get(address);
|
||||
}
|
||||
|
||||
public boolean contains(AntmailAddress address) {
|
||||
return mailboxes.containsKey(address);
|
||||
}
|
||||
|
||||
public void put(AntmailMailbox mailbox) {
|
||||
mailboxes.put(mailbox.address(), mailbox);
|
||||
}
|
||||
|
||||
public Collection<AntmailMailbox> mailboxes() {
|
||||
return List.copyOf(mailboxes.values());
|
||||
}
|
||||
|
||||
public CompoundTag toTag() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
ListTag mailboxTags = new ListTag();
|
||||
for (AntmailMailbox mailbox : mailboxes.values()) mailboxTags.add(mailbox.toTag());
|
||||
tag.put("Mailboxes", mailboxTags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static AntmailMailboxStore fromTag(CompoundTag tag) {
|
||||
AntmailMailboxStore store = new AntmailMailboxStore();
|
||||
ListTag mailboxTags = tag.getList("Mailboxes", 10);
|
||||
for (int index = 0; index < mailboxTags.size(); index++) {
|
||||
try {
|
||||
AntmailMailbox mailbox = AntmailMailbox.fromTag(mailboxTags.getCompound(index));
|
||||
store.mailboxes.put(mailbox.address(), mailbox);
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
return store;
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AntmailMessage {
|
||||
private static final String ID_TAG = "Id";
|
||||
private static final String FROM_TAG = "From";
|
||||
private static final String TO_TAG = "To";
|
||||
private static final String SUBJECT_TAG = "Subject";
|
||||
private static final String BODY_TAG = "Body";
|
||||
private static final String CREATED_TAG = "Created";
|
||||
private static final String READ_TAG = "Read";
|
||||
private static final String ATTACHMENTS_TAG = "Attachments";
|
||||
private static final String DELIVERY_STATUS_TAG = "DeliveryStatus";
|
||||
|
||||
private final UUID id;
|
||||
private final AntmailAddress sender;
|
||||
private final AntmailAddress recipient;
|
||||
private final String subject;
|
||||
private final String body;
|
||||
private final long createdAt;
|
||||
private final List<AntmailAttachment> attachments;
|
||||
private boolean read;
|
||||
private String deliveryStatus = "DELIVERED";
|
||||
|
||||
public AntmailMessage(UUID id, AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List<AntmailAttachment> attachments, boolean read) {
|
||||
AntmailValidation.Result result = AntmailValidation.validateMessage(sender, recipient, subject, body, attachments);
|
||||
if (!result.valid()) {
|
||||
throw new IllegalArgumentException(result.reason());
|
||||
}
|
||||
this.id = id == null ? UUID.randomUUID() : id;
|
||||
this.sender = sender;
|
||||
this.recipient = recipient;
|
||||
this.subject = subject;
|
||||
this.body = body;
|
||||
this.createdAt = createdAt;
|
||||
this.attachments = List.copyOf(attachments);
|
||||
this.read = read;
|
||||
}
|
||||
|
||||
public static AntmailMessage create(AntmailAddress sender, AntmailAddress recipient, String subject, String body, long createdAt, List<AntmailAttachment> attachments) {
|
||||
return new AntmailMessage(UUID.randomUUID(), sender, recipient, subject, body, createdAt, attachments, false);
|
||||
}
|
||||
|
||||
public UUID id() { return id; }
|
||||
public AntmailAddress sender() { return sender; }
|
||||
public AntmailAddress recipient() { return recipient; }
|
||||
public String subject() { return subject; }
|
||||
public String body() { return body; }
|
||||
public long createdAt() { return createdAt; }
|
||||
public List<AntmailAttachment> attachments() { return attachments; }
|
||||
public boolean read() { return read; }
|
||||
public String deliveryStatus() { return deliveryStatus; }
|
||||
public void setDeliveryStatus(String status) { deliveryStatus = status == null || status.isBlank() ? "DELIVERED" : status; }
|
||||
public void markRead() { read = true; }
|
||||
public void markUnread() { read = false; }
|
||||
|
||||
public CompoundTag toTag() {
|
||||
return toTag(true);
|
||||
}
|
||||
|
||||
public CompoundTag toTag(boolean includeAttachments) {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putString(ID_TAG, id.toString());
|
||||
tag.putString(FROM_TAG, sender.fullAddress());
|
||||
tag.putString(TO_TAG, recipient.fullAddress());
|
||||
tag.putString(SUBJECT_TAG, subject);
|
||||
tag.putString(BODY_TAG, body);
|
||||
tag.putLong(CREATED_TAG, createdAt);
|
||||
tag.putBoolean(READ_TAG, read);
|
||||
tag.putString(DELIVERY_STATUS_TAG, deliveryStatus);
|
||||
if (includeAttachments) {
|
||||
ListTag attachmentTags = new ListTag();
|
||||
for (AntmailAttachment attachment : attachments) attachmentTags.add(attachment.toTag());
|
||||
tag.put(ATTACHMENTS_TAG, attachmentTags);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static AntmailMessage fromTag(CompoundTag tag) {
|
||||
List<AntmailAttachment> attachments = new ArrayList<>();
|
||||
ListTag attachmentTags = tag.getList(ATTACHMENTS_TAG, 10);
|
||||
for (int index = 0; index < attachmentTags.size(); index++) attachments.add(AntmailAttachment.fromTag(attachmentTags.getCompound(index)));
|
||||
AntmailMessage message = new AntmailMessage(UUID.fromString(tag.getString(ID_TAG)), AntmailAddress.parse(tag.getString(FROM_TAG)), AntmailAddress.parse(tag.getString(TO_TAG)), tag.getString(SUBJECT_TAG), tag.getString(BODY_TAG), tag.getLong(CREATED_TAG), attachments, tag.getBoolean(READ_TAG));
|
||||
message.setDeliveryStatus(tag.getString(DELIVERY_STATUS_TAG));
|
||||
return message;
|
||||
}
|
||||
}
|
||||
-339
@@ -1,339 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AntmailServerData extends SavedData {
|
||||
private static final String ID = "antarchy_antmail";
|
||||
private static final int MAX_PENDING_MESSAGES = AntmailValidation.MAX_MAILBOX_MESSAGES;
|
||||
private final Map<AntmailAddress, Endpoint> endpoints = new LinkedHashMap<>();
|
||||
private final AntmailMailboxStore mailboxes = new AntmailMailboxStore();
|
||||
private final List<PendingMessage> pending = new ArrayList<>();
|
||||
private final Map<AntmailAddress, java.util.Set<ResourceLocation>> triggered = new LinkedHashMap<>();
|
||||
private final Map<AntmailAddress, Long> mailboxVersions = new LinkedHashMap<>();
|
||||
private final Map<UUID, AntmailAddress> lastUsedAddresses = new LinkedHashMap<>();
|
||||
private long randomMailDay = Long.MIN_VALUE;
|
||||
private int retryTicks;
|
||||
|
||||
public static AntmailServerData create() {
|
||||
return new AntmailServerData();
|
||||
}
|
||||
|
||||
public static AntmailServerData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AntmailServerData data = new AntmailServerData();
|
||||
ListTag endpointTags = tag.getList("Endpoints", 10);
|
||||
for (int i = 0; i < endpointTags.size(); i++) {
|
||||
try {
|
||||
CompoundTag endpoint = endpointTags.getCompound(i);
|
||||
AntmailAddress address = AntmailAddress.parse(endpoint.getString("Address"));
|
||||
data.endpoints.put(address, new Endpoint(endpoint.getString("Dimension"), endpoint.getLong("Position")));
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
AntmailServerData loaded = data;
|
||||
AntmailMailboxStore restored = AntmailMailboxStore.fromTag(tag.getCompound("Mailboxes"));
|
||||
for (AntmailMailbox mailbox : restored.mailboxes()) {
|
||||
loaded.mailboxes.put(mailbox);
|
||||
}
|
||||
ListTag pendingTags = tag.getList("Pending", 10);
|
||||
for (int i = 0; i < pendingTags.size(); i++) {
|
||||
try {
|
||||
CompoundTag pendingTag = pendingTags.getCompound(i);
|
||||
loaded.pending.add(new PendingMessage(AntmailAddress.parse(pendingTag.getString("Recipient")), AntmailMessage.fromTag(pendingTag.getCompound("Message"))));
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
ListTag triggeredTags = tag.getList("Triggered", 10);
|
||||
for (int i = 0; i < triggeredTags.size(); i++) {
|
||||
CompoundTag triggeredTag = triggeredTags.getCompound(i);
|
||||
try {
|
||||
AntmailAddress address = AntmailAddress.parse(triggeredTag.getString("Address"));
|
||||
java.util.Set<ResourceLocation> ids = new java.util.LinkedHashSet<>();
|
||||
ListTag idsTag = triggeredTag.getList("Ids", 8);
|
||||
for (int j = 0; j < idsTag.size(); j++) ids.add(ResourceLocation.parse(idsTag.getString(j)));
|
||||
data.triggered.put(address, ids);
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
ListTag lastUsedTags = tag.getList("LastUsedAddresses", 10);
|
||||
for (int i = 0; i < lastUsedTags.size(); i++) {
|
||||
try {
|
||||
CompoundTag lastUsedTag = lastUsedTags.getCompound(i);
|
||||
data.lastUsedAddresses.put(UUID.fromString(lastUsedTag.getString("Player")), AntmailAddress.parse(lastUsedTag.getString("Address")));
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
data.randomMailDay = tag.contains("RandomMailDay") ? tag.getLong("RandomMailDay") : Long.MIN_VALUE;
|
||||
ListTag versionTags = tag.getList("MailboxVersions", 10);
|
||||
for (int i = 0; i < versionTags.size(); i++) {
|
||||
try {
|
||||
CompoundTag versionTag = versionTags.getCompound(i);
|
||||
data.mailboxVersions.put(AntmailAddress.parse(versionTag.getString("Address")), Math.max(1L, versionTag.getLong("Version")));
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
ListTag endpointTags = new ListTag();
|
||||
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
|
||||
CompoundTag endpoint = new CompoundTag();
|
||||
endpoint.putString("Address", entry.getKey().fullAddress());
|
||||
endpoint.putString("Dimension", entry.getValue().dimension());
|
||||
endpoint.putLong("Position", entry.getValue().position());
|
||||
endpointTags.add(endpoint);
|
||||
}
|
||||
tag.put("Endpoints", endpointTags);
|
||||
tag.put("Mailboxes", mailboxes.toTag());
|
||||
ListTag pendingTags = new ListTag();
|
||||
for (PendingMessage message : pending) {
|
||||
CompoundTag pendingTag = new CompoundTag();
|
||||
pendingTag.putString("Recipient", message.recipient().fullAddress());
|
||||
pendingTag.put("Message", message.message().toTag());
|
||||
pendingTags.add(pendingTag);
|
||||
}
|
||||
tag.put("Pending", pendingTags);
|
||||
ListTag triggeredTags = new ListTag();
|
||||
for (Map.Entry<AntmailAddress, java.util.Set<ResourceLocation>> entry : triggered.entrySet()) {
|
||||
CompoundTag triggeredTag = new CompoundTag();
|
||||
triggeredTag.putString("Address", entry.getKey().fullAddress());
|
||||
ListTag idsTag = new ListTag();
|
||||
for (ResourceLocation id : entry.getValue()) idsTag.add(net.minecraft.nbt.StringTag.valueOf(id.toString()));
|
||||
triggeredTag.put("Ids", idsTag);
|
||||
triggeredTags.add(triggeredTag);
|
||||
}
|
||||
tag.put("Triggered", triggeredTags);
|
||||
ListTag lastUsedTags = new ListTag();
|
||||
for (Map.Entry<UUID, AntmailAddress> entry : lastUsedAddresses.entrySet()) {
|
||||
CompoundTag lastUsedTag = new CompoundTag();
|
||||
lastUsedTag.putString("Player", entry.getKey().toString());
|
||||
lastUsedTag.putString("Address", entry.getValue().fullAddress());
|
||||
lastUsedTags.add(lastUsedTag);
|
||||
}
|
||||
tag.put("LastUsedAddresses", lastUsedTags);
|
||||
tag.putLong("RandomMailDay", randomMailDay);
|
||||
ListTag versionTags = new ListTag();
|
||||
for (Map.Entry<AntmailAddress, Long> entry : mailboxVersions.entrySet()) {
|
||||
CompoundTag versionTag = new CompoundTag();
|
||||
versionTag.putString("Address", entry.getKey().fullAddress());
|
||||
versionTag.putLong("Version", entry.getValue());
|
||||
versionTags.add(versionTag);
|
||||
}
|
||||
tag.put("MailboxVersions", versionTags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static AntmailServerData get(MinecraftServer server) {
|
||||
return server.overworld().getDataStorage().computeIfAbsent(
|
||||
new SavedData.Factory<>(AntmailServerData::create, AntmailServerData::load, null), ID);
|
||||
}
|
||||
|
||||
public static AntmailServerData access(MinecraftServer server) {
|
||||
return get(server);
|
||||
}
|
||||
|
||||
public synchronized Registration register(MinecraftServer server, ServerLevel level, BlockPos position, String username) {
|
||||
final AntmailAddress address;
|
||||
try {
|
||||
address = AntmailAddress.ofUsername(username);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return Registration.invalid("invalid_username");
|
||||
}
|
||||
AntmailAddress previous = addressAt(level.dimension().location(), position);
|
||||
Endpoint existing = endpoints.get(address);
|
||||
if (existing != null && (!existing.dimension().equals(level.dimension().location().toString()) || existing.position() != position.asLong())) {
|
||||
return Registration.invalid("username_taken");
|
||||
}
|
||||
if (previous != null && !previous.equals(address)) {
|
||||
endpoints.remove(previous);
|
||||
}
|
||||
endpoints.put(address, new Endpoint(level.dimension().location().toString(), position.asLong()));
|
||||
get(server).drain(server, address);
|
||||
setDirty();
|
||||
return Registration.accepted(address);
|
||||
}
|
||||
|
||||
public synchronized AntmailAddress addressAt(ResourceLocation dimension, BlockPos position) {
|
||||
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
|
||||
if (entry.getValue().dimension().equals(dimension.toString()) && entry.getValue().position() == position.asLong()) return entry.getKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized boolean unregister(ResourceLocation dimension, BlockPos position) {
|
||||
AntmailAddress address = addressAt(dimension, position);
|
||||
if (address == null) return false;
|
||||
endpoints.remove(address);
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized Endpoint endpoint(AntmailAddress address) {
|
||||
return endpoints.get(address);
|
||||
}
|
||||
|
||||
public synchronized List<AntmailAddress> registeredAddresses() {
|
||||
return List.copyOf(endpoints.keySet());
|
||||
}
|
||||
|
||||
public synchronized AntmailAddress addressFor(ServerPlayer player) {
|
||||
AntmailAddress address = lastUsedAddresses.get(player.getUUID());
|
||||
return address != null && endpoints.containsKey(address) ? address : null;
|
||||
}
|
||||
|
||||
public synchronized AntmailAddress lastUsedAddress(ServerPlayer player) {
|
||||
AntmailAddress address = lastUsedAddresses.get(player.getUUID());
|
||||
return address != null && endpoints.containsKey(address) ? address : null;
|
||||
}
|
||||
|
||||
public synchronized void setLastUsedAddress(ServerPlayer player, AntmailAddress address) {
|
||||
if (address == null) return;
|
||||
lastUsedAddresses.put(player.getUUID(), address);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
public synchronized boolean claimTrigger(AntmailAddress address, ResourceLocation triggerId) {
|
||||
java.util.Set<ResourceLocation> ids = triggered.computeIfAbsent(address, ignored -> new java.util.LinkedHashSet<>());
|
||||
if (!ids.add(triggerId)) return false;
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean beginRandomMailDay(long day) {
|
||||
if (randomMailDay == day) return false;
|
||||
randomMailDay = day;
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized AntmailMailbox mailbox(AntmailAddress address) {
|
||||
return mailboxes.get(address);
|
||||
}
|
||||
|
||||
public synchronized AntmailMailbox mailboxOrCreate(AntmailAddress address) {
|
||||
AntmailMailbox mailbox = mailboxes.getOrCreate(address);
|
||||
setDirty();
|
||||
return mailbox;
|
||||
}
|
||||
|
||||
public synchronized long mailboxVersion(AntmailAddress address) {
|
||||
return mailboxVersions.getOrDefault(address, 1L);
|
||||
}
|
||||
|
||||
public synchronized void touchMailbox(AntmailAddress address) {
|
||||
if (address == null) return;
|
||||
mailboxVersions.put(address, mailboxVersion(address) + 1L);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
public synchronized void drainAddress(MinecraftServer server, ServerLevel level, BlockPos position) {
|
||||
AntmailAddress address = addressAt(level.dimension().location(), position);
|
||||
if (address != null) drain(server, address);
|
||||
}
|
||||
|
||||
public synchronized void drainAvailable(MinecraftServer server) {
|
||||
if (pending.isEmpty() || ++retryTicks < 20) return;
|
||||
retryTicks = 0;
|
||||
List<AntmailAddress> available = new ArrayList<>();
|
||||
for (Map.Entry<AntmailAddress, Endpoint> entry : endpoints.entrySet()) {
|
||||
Endpoint endpoint = entry.getValue();
|
||||
ServerLevel level = server.getLevel(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, ResourceLocation.parse(endpoint.dimension())));
|
||||
if (level != null && level.hasChunkAt(BlockPos.of(endpoint.position()))) available.add(entry.getKey());
|
||||
}
|
||||
for (AntmailAddress address : available) drain(server, address);
|
||||
}
|
||||
|
||||
public synchronized AntmailDeliveryResult deliver(MinecraftServer server, AntmailMessage message) {
|
||||
if (message == null) return AntmailDeliveryResult.failed(AntmailDeliveryResult.Status.MESSAGE_INVALID, null, "missing_message");
|
||||
if (endpoint(message.recipient()) == null) return failedAndRecorded(message, AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND, "address_not_found");
|
||||
AntmailMailbox destination = mailboxes.getOrCreate(message.recipient());
|
||||
if (destination.inbox().size() >= AntmailValidation.MAX_MAILBOX_MESSAGES) return failedAndRecorded(message, AntmailDeliveryResult.Status.MAILBOX_FULL, "mailbox_full");
|
||||
Endpoint target = endpoint(message.recipient());
|
||||
ServerLevel level = server.getLevel(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, ResourceLocation.parse(target.dimension())));
|
||||
if (level == null || !level.hasChunkAt(BlockPos.of(target.position()))) {
|
||||
if (pending.stream().anyMatch(item -> item.message().id().equals(message.id()))) {
|
||||
return AntmailDeliveryResult.failed(AntmailDeliveryResult.Status.DUPLICATE, message, "already_pending");
|
||||
}
|
||||
if (pending.size() >= MAX_PENDING_MESSAGES) {
|
||||
return failedAndRecorded(message, AntmailDeliveryResult.Status.FAILED, "pending_queue_full");
|
||||
}
|
||||
message.setDeliveryStatus("QUEUED");
|
||||
pending.add(new PendingMessage(message.recipient(), message));
|
||||
mailboxes.getOrCreate(message.sender()).addSent(message);
|
||||
touchMailbox(message.recipient());
|
||||
touchMailbox(message.sender());
|
||||
setDirty();
|
||||
return AntmailDeliveryResult.queued(message);
|
||||
}
|
||||
message.setDeliveryStatus("DELIVERED");
|
||||
destination.addIncoming(message);
|
||||
mailboxes.getOrCreate(message.sender()).addSent(message);
|
||||
touchMailbox(message.recipient());
|
||||
touchMailbox(message.sender());
|
||||
setDirty();
|
||||
return AntmailDeliveryResult.delivered(message);
|
||||
}
|
||||
|
||||
private AntmailDeliveryResult failedAndRecorded(AntmailMessage message, AntmailDeliveryResult.Status status, String detail) {
|
||||
message.setDeliveryStatus("FAILED // " + detail.toUpperCase());
|
||||
mailboxes.getOrCreate(message.sender()).addSent(message);
|
||||
touchMailbox(message.sender());
|
||||
setDirty();
|
||||
return AntmailDeliveryResult.failed(status, message, detail);
|
||||
}
|
||||
|
||||
public synchronized void drain(MinecraftServer server, AntmailAddress address) {
|
||||
AntmailMailbox mailbox = mailboxes.getOrCreate(address);
|
||||
final boolean[] delivered = {false};
|
||||
Set<AntmailAddress> deliveredSenders = new LinkedHashSet<>();
|
||||
pending.removeIf(pendingMessage -> {
|
||||
if (!pendingMessage.recipient().equals(address) || mailbox.inbox().size() >= AntmailValidation.MAX_MAILBOX_MESSAGES) return false;
|
||||
if (!mailbox.addIncoming(pendingMessage.message())) return false;
|
||||
pendingMessage.message().setDeliveryStatus("DELIVERED");
|
||||
delivered[0] = true;
|
||||
deliveredSenders.add(pendingMessage.message().sender());
|
||||
return true;
|
||||
});
|
||||
if (delivered[0]) {
|
||||
touchMailbox(address);
|
||||
for (AntmailAddress sender : deliveredSenders) touchMailbox(sender);
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized AntmailDeliveryResult retry(MinecraftServer server, AntmailAddress sender, UUID messageId) {
|
||||
AntmailMessage message = mailboxes.getOrCreate(sender).findSent(messageId);
|
||||
if (message == null) return null;
|
||||
pending.removeIf(item -> item.message().id().equals(messageId));
|
||||
return deliver(server, message);
|
||||
}
|
||||
|
||||
public record Endpoint(String dimension, long position) {
|
||||
}
|
||||
|
||||
public record Registration(boolean valid, AntmailAddress address, String reason) {
|
||||
static Registration accepted(AntmailAddress address) { return new Registration(true, address, ""); }
|
||||
static Registration invalid(String reason) { return new Registration(false, null, reason); }
|
||||
}
|
||||
|
||||
private record PendingMessage(AntmailAddress recipient, AntmailMessage message) {
|
||||
}
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public final class AntmailValidation {
|
||||
public static final int MAX_SUBJECT_LENGTH = 64;
|
||||
public static final int MAX_BODY_LENGTH = 4096;
|
||||
public static final int MAX_FILE_NAME_LENGTH = 32;
|
||||
public static final int MAX_ATTACHMENTS = 8;
|
||||
public static final int MAX_TEXT_ATTACHMENT_CHARACTERS = 16384;
|
||||
public static final int MAX_PAINT_WIDTH = 32;
|
||||
public static final int MAX_PAINT_HEIGHT = 24;
|
||||
public static final int MAX_MESSAGE_BYTES = 65536;
|
||||
public static final int MAX_ATTACHMENT_WIRE_CHARS = 65536;
|
||||
public static final int MAX_MAILBOX_MESSAGES = 100;
|
||||
public static final int MAX_DRAFTS = 25;
|
||||
|
||||
private AntmailValidation() {
|
||||
}
|
||||
|
||||
public static Result validateMessage(AntmailAddress sender, AntmailAddress recipient, String subject, String body, List<AntmailAttachment> attachments) {
|
||||
if (sender == null || recipient == null) {
|
||||
return Result.invalid("missing address");
|
||||
}
|
||||
if (subject == null || subject.length() > MAX_SUBJECT_LENGTH) {
|
||||
return Result.invalid("subject too long");
|
||||
}
|
||||
if (body == null || body.length() > MAX_BODY_LENGTH) {
|
||||
return Result.invalid("body too long");
|
||||
}
|
||||
Result attachmentResult = validateAttachments(attachments);
|
||||
if (!attachmentResult.valid()) return attachmentResult;
|
||||
int bytes = subject.getBytes(StandardCharsets.UTF_8).length + body.getBytes(StandardCharsets.UTF_8).length;
|
||||
for (AntmailAttachment attachment : attachments) bytes += attachment.byteSize();
|
||||
if (bytes > MAX_MESSAGE_BYTES) {
|
||||
return Result.invalid("message too large");
|
||||
}
|
||||
return Result.accepted();
|
||||
}
|
||||
|
||||
public static Result validateAttachments(List<AntmailAttachment> attachments) {
|
||||
if (attachments == null || attachments.size() > MAX_ATTACHMENTS) return Result.invalid("too many attachments");
|
||||
int textCharacters = 0;
|
||||
for (AntmailAttachment attachment : attachments) {
|
||||
if (attachment == null) return Result.invalid("null attachment");
|
||||
if (attachment instanceof AntmailAttachment.TextFile textFile) {
|
||||
if (textFile.contents().length() > MAX_TEXT_ATTACHMENT_CHARACTERS) return Result.invalid("text attachment too large");
|
||||
textCharacters += textFile.contents().length();
|
||||
} else if (attachment instanceof AntmailAttachment.PaintImage paintImage) {
|
||||
if (paintImage.width() != MAX_PAINT_WIDTH || paintImage.height() != MAX_PAINT_HEIGHT) return Result.invalid("unsupported paint size");
|
||||
}
|
||||
}
|
||||
if (textCharacters > MAX_TEXT_ATTACHMENT_CHARACTERS) return Result.invalid("text attachments too large");
|
||||
return Result.accepted();
|
||||
}
|
||||
|
||||
public record Result(boolean valid, String reason) {
|
||||
public static Result accepted() {
|
||||
return new Result(true, "");
|
||||
}
|
||||
|
||||
public static Result invalid(String reason) {
|
||||
return new Result(false, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.antmail;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.NbtAccounter;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
public final class AntmailWire {
|
||||
private static final int MAX_COMPRESSED_BYTES = 1024 * 1024;
|
||||
private static final long MAX_DECOMPRESSED_BYTES = 8L * 1024L * 1024L;
|
||||
private AntmailWire() {
|
||||
}
|
||||
|
||||
public static String encodeAttachments(List<AntmailAttachment> attachments) {
|
||||
AntmailValidation.Result validation = AntmailValidation.validateAttachments(attachments);
|
||||
if (!validation.valid()) throw new IllegalArgumentException(validation.reason());
|
||||
CompoundTag root = new CompoundTag();
|
||||
ListTag list = new ListTag();
|
||||
for (AntmailAttachment attachment : attachments) list.add(attachment.toTag());
|
||||
root.put("Attachments", list);
|
||||
String encoded = encodeTag(root);
|
||||
if (encoded.length() > AntmailValidation.MAX_ATTACHMENT_WIRE_CHARS) throw new IllegalArgumentException("Attachment payload is too large");
|
||||
return encoded;
|
||||
}
|
||||
|
||||
public static String encodeTag(CompoundTag tag) {
|
||||
if (tag == null) throw new IllegalArgumentException("Antmail tag cannot be null");
|
||||
try {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
NbtIo.writeCompressed(tag, output);
|
||||
byte[] compressed = output.toByteArray();
|
||||
if (compressed.length > MAX_COMPRESSED_BYTES) throw new IllegalArgumentException("Antmail data is too large");
|
||||
return Base64.getEncoder().encodeToString(compressed);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalArgumentException("Could not encode Antmail data", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static CompoundTag decodeTag(String encoded) {
|
||||
try {
|
||||
if (encoded == null || encoded.isBlank()) throw new IllegalArgumentException("Antmail data is empty");
|
||||
byte[] compressed = Base64.getDecoder().decode(encoded);
|
||||
if (compressed.length > MAX_COMPRESSED_BYTES) throw new IllegalArgumentException("Antmail data is too large");
|
||||
return NbtIo.readCompressed(new ByteArrayInputStream(compressed), NbtAccounter.create(MAX_DECOMPRESSED_BYTES));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalArgumentException("Invalid Antmail data", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<AntmailAttachment> decodeAttachments(String encoded) {
|
||||
if (encoded == null || encoded.isBlank()) return List.of();
|
||||
if (encoded.length() > AntmailValidation.MAX_ATTACHMENT_WIRE_CHARS) throw new IllegalArgumentException("Attachment payload is too large");
|
||||
try {
|
||||
CompoundTag root = decodeTag(encoded);
|
||||
ListTag list = root.getList("Attachments", 10);
|
||||
List<AntmailAttachment> result = new ArrayList<>();
|
||||
for (int i = 0; i < list.size(); i++) result.add(AntmailAttachment.fromTag(list.getCompound(i)));
|
||||
AntmailValidation.Result validation = AntmailValidation.validateAttachments(result);
|
||||
if (!validation.valid()) throw new IllegalArgumentException(validation.reason());
|
||||
return List.copyOf(result);
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalArgumentException("Invalid attachment data", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.craisinlord.antarchy.content.block;
|
||||
|
||||
import com.craisinlord.antarchy.content.block.entity.AntNestBlockEntity;
|
||||
import com.craisinlord.antarchy.content.entity.ant.BaseAntEntity;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
|
||||
public final class AntTrapBlock extends AntNestBlock {
|
||||
private static final VoxelShape SHAPE = Block.box(3.0D, 0.0D, 3.0D, 13.0D, 5.0D, 13.0D);
|
||||
|
||||
public AntTrapBlock(Supplier<List<EntityType<? extends BaseAntEntity>>> antTypesSupplier,
|
||||
Supplier<? extends BlockEntityType<AntNestBlockEntity>> blockEntityTypeSupplier,
|
||||
BlockBehaviour.Properties properties) {
|
||||
super(antTypesSupplier, blockEntityTypeSupplier, 10, false, false, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
|
||||
if (!state.is(newState.getBlock()) && level instanceof ServerLevel serverLevel
|
||||
&& level.getBlockEntity(pos) instanceof AntNestBlockEntity trap) {
|
||||
trap.releaseAll(serverLevel);
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, movedByPiston);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playerDestroy(Level level, Player player, BlockPos pos, BlockState state, BlockEntity blockEntity, ItemStack stack) {
|
||||
if (blockEntity instanceof AntNestBlockEntity trap && level instanceof ServerLevel serverLevel) {
|
||||
trap.releaseAll(serverLevel);
|
||||
}
|
||||
super.playerDestroy(level, player, pos, state, blockEntity, stack);
|
||||
}
|
||||
}
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.block;
|
||||
|
||||
import com.craisinlord.antarchy.content.AntarchyObjects;
|
||||
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailServerData;
|
||||
import com.craisinlord.antarchy.content.client.AntarchyClientHooks;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.ItemInteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
public final class ComputerBlock extends BaseEntityBlock {
|
||||
public static final MapCodec<ComputerBlock> CODEC = Block.simpleCodec(ComputerBlock::new);
|
||||
public static final BooleanProperty ACTIVE = BooleanProperty.create("active");
|
||||
public static final DirectionProperty FACING = net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING;
|
||||
private static final VoxelShape SHAPE = Block.box(1.0D, 0.0D, 1.0D, 15.0D, 16.0D, 15.0D);
|
||||
|
||||
public ComputerBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties.lightLevel(state -> state.getValue(ACTIVE) ? 7 : 0));
|
||||
registerDefaultState(stateDefinition.any().setValue(ACTIVE, false).setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<ComputerBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(ACTIVE, FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState rotate(BlockState state, Rotation rotation) {
|
||||
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState mirror(BlockState state, Mirror mirror) {
|
||||
return state.rotate(mirror.getRotation(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.ENTITYBLOCK_ANIMATED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSignalSource(BlockState state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
|
||||
return isAuthenticated(level, pos) ? 15 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDirectSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
|
||||
return getSignal(state, level, pos, direction);
|
||||
}
|
||||
|
||||
private static boolean isAuthenticated(BlockGetter level, BlockPos pos) {
|
||||
return level.getBlockEntity(pos) instanceof ComputerBlockEntity computer && computer.isAuthenticated();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
|
||||
if (!state.is(newState.getBlock()) && level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
|
||||
computer.dropDisks(level, pos);
|
||||
if (level instanceof net.minecraft.server.level.ServerLevel serverLevel) {
|
||||
AntmailServerData.access(serverLevel.getServer()).unregister(serverLevel.dimension().location(), pos);
|
||||
}
|
||||
}
|
||||
super.onRemove(state, level, pos, newState, movedByPiston);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new ComputerBlockEntity(pos, state, AntarchyObjects.COMPUTER_BLOCK_ENTITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
|
||||
return createTickerHelper(blockEntityType, AntarchyObjects.COMPUTER_BLOCK_ENTITY.get(), ComputerBlockEntity::tick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player,
|
||||
InteractionHand hand, BlockHitResult hitResult) {
|
||||
if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
|
||||
if (level.isClientSide) {
|
||||
if (stack.is(AntarchyObjects.FLOPPY_DISK.get())) {
|
||||
return ItemInteractionResult.SUCCESS;
|
||||
}
|
||||
} else if (computer.insert(stack, player)) {
|
||||
return ItemInteractionResult.CONSUME;
|
||||
}
|
||||
}
|
||||
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player,
|
||||
BlockHitResult hitResult) {
|
||||
if (level.getBlockEntity(pos) instanceof ComputerBlockEntity computer) {
|
||||
computer.activate();
|
||||
if (level.isClientSide) {
|
||||
AntarchyClientHooks.openComputer(pos);
|
||||
}
|
||||
return net.minecraft.world.InteractionResult.SUCCESS;
|
||||
}
|
||||
return net.minecraft.world.InteractionResult.PASS;
|
||||
}
|
||||
}
|
||||
+21
@@ -6,6 +6,11 @@ import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.SpawnEggItem;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.ItemInteractionResult;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
@@ -62,6 +67,22 @@ public final class UndertrialSpawnerBlock extends BaseEntityBlock {
|
||||
return new UndertrialSpawnerBlockEntity(pos, state, blockEntityType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
|
||||
Player player, net.minecraft.world.InteractionHand hand,
|
||||
BlockHitResult hitResult) {
|
||||
if (!(stack.getItem() instanceof SpawnEggItem)) {
|
||||
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
|
||||
}
|
||||
if (level.getBlockEntity(pos) instanceof UndertrialSpawnerBlockEntity spawner) {
|
||||
if (!level.isClientSide) {
|
||||
spawner.setSpawnEntity(((SpawnEggItem) stack.getItem()).getType(stack));
|
||||
}
|
||||
return ItemInteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
|
||||
if (level.isClientSide) {
|
||||
|
||||
+58
-12
@@ -40,6 +40,28 @@ public class AntNestBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
public static void serverTick(ServerLevel level, BlockPos pos, BlockState state, AntNestBlockEntity nest) {
|
||||
if (state.getBlock() instanceof com.craisinlord.antarchy.content.block.AntTrapBlock) {
|
||||
if (level.getGameTime() % 20L == 0L && nest.storedAnts.size() < nest.maxOccupants()) {
|
||||
net.minecraft.world.phys.AABB searchArea = new net.minecraft.world.phys.AABB(pos).inflate(15.0D);
|
||||
for (BaseAntEntity ant : level.getEntitiesOfClass(BaseAntEntity.class, searchArea, BaseAntEntity::isAlive)) {
|
||||
if (ant.distanceToSqr(net.minecraft.world.phys.Vec3.atCenterOf(pos)) > 225.0D) {
|
||||
continue;
|
||||
}
|
||||
if (!nest.canAccept(ant.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (ant.distanceToSqr(net.minecraft.world.phys.Vec3.atCenterOf(pos)) <= 2.25D) {
|
||||
nest.tryStoreAnt(ant);
|
||||
if (nest.storedAnts.size() >= nest.maxOccupants()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ant.getNavigation().moveTo(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, 1.05D);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
boolean playerNearby = level.hasNearbyAlivePlayer(
|
||||
pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, PLAYER_ACTIVATION_RANGE);
|
||||
if (!playerNearby) {
|
||||
@@ -115,12 +137,17 @@ public class AntNestBlockEntity extends BlockEntity {
|
||||
}
|
||||
|
||||
public void releaseAll(ServerLevel level) {
|
||||
while (!this.storedAnts.isEmpty()) {
|
||||
this.releaseOne(level);
|
||||
int remaining = this.storedAnts.size();
|
||||
while (remaining-- > 0 && !this.storedAnts.isEmpty()) {
|
||||
this.releaseOne(level, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseOne(ServerLevel level) {
|
||||
this.releaseOne(level, false);
|
||||
}
|
||||
|
||||
private void releaseOne(ServerLevel level, boolean forceRelease) {
|
||||
if (this.storedAnts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -132,20 +159,39 @@ public class AntNestBlockEntity extends BlockEntity {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos spawnPos = this.getBlockPos().above();
|
||||
if (!level.getBlockState(spawnPos).canBeReplaced()) {
|
||||
BlockPos origin = this.getBlockPos();
|
||||
java.util.List<BlockPos> candidates = new java.util.ArrayList<>();
|
||||
for (int y = 1; y <= 3; y++) {
|
||||
candidates.add(origin.above(y));
|
||||
for (int radius = 1; radius <= 2; radius++) {
|
||||
candidates.add(origin.offset(radius, y, 0));
|
||||
candidates.add(origin.offset(-radius, y, 0));
|
||||
candidates.add(origin.offset(0, y, radius));
|
||||
candidates.add(origin.offset(0, y, -radius));
|
||||
}
|
||||
}
|
||||
BlockPos spawnPos = null;
|
||||
for (BlockPos candidate : candidates) {
|
||||
antEntity.moveTo(candidate.getX() + SPAWN_OFFSET_XZ, candidate.getY() + SPAWN_OFFSET_Y, candidate.getZ() + SPAWN_OFFSET_XZ,
|
||||
level.random.nextFloat() * 360.0F, 0.0F);
|
||||
if (level.noCollision(antEntity)) {
|
||||
spawnPos = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (spawnPos == null && !forceRelease) {
|
||||
return;
|
||||
}
|
||||
|
||||
antEntity.moveTo(spawnPos.getX() + SPAWN_OFFSET_XZ, spawnPos.getY() + SPAWN_OFFSET_Y, spawnPos.getZ() + SPAWN_OFFSET_XZ, level.random.nextFloat() * 360.0F, 0.0F);
|
||||
if (!level.noCollision(antEntity)) {
|
||||
return;
|
||||
if (spawnPos == null) {
|
||||
antEntity.moveTo(origin.getX() + SPAWN_OFFSET_XZ, origin.getY() + SPAWN_OFFSET_Y, origin.getZ() + SPAWN_OFFSET_XZ,
|
||||
level.random.nextFloat() * 360.0F, 0.0F);
|
||||
}
|
||||
|
||||
this.storedAnts.remove(0);
|
||||
antEntity.setNestPos(this.getBlockPos());
|
||||
antEntity.setNestPos(origin);
|
||||
antEntity.onExitNest();
|
||||
level.addFreshEntity(antEntity);
|
||||
if (!level.addFreshEntity(antEntity) && !forceRelease) {
|
||||
return;
|
||||
}
|
||||
this.storedAnts.remove(0);
|
||||
this.setChanged();
|
||||
}
|
||||
|
||||
|
||||
-458
@@ -1,458 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.block.entity;
|
||||
|
||||
import com.craisinlord.antarchy.content.AntarchyObjects;
|
||||
import com.craisinlord.antarchy.content.block.ComputerBlock;
|
||||
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
|
||||
import com.craisinlord.antarchy.content.computer.ComputerDesktopState;
|
||||
import com.craisinlord.antarchy.content.guide.ComputerGuideData;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.Containers;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import software.bernie.geckolib.animatable.GeoBlockEntity;
|
||||
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
|
||||
import software.bernie.geckolib.animation.AnimatableManager;
|
||||
import software.bernie.geckolib.animation.AnimationController;
|
||||
import software.bernie.geckolib.animation.RawAnimation;
|
||||
import software.bernie.geckolib.animation.PlayState;
|
||||
import software.bernie.geckolib.util.GeckoLibUtil;
|
||||
|
||||
public final class ComputerBlockEntity extends BlockEntity implements GeoBlockEntity {
|
||||
private static final ResourceLocation INTRODUCTION_DISK = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
|
||||
private static final String DISKS_TAG = "Disks";
|
||||
private static final String ACTIVE_UNTIL_TAG = "ActiveUntil";
|
||||
private static final String PASSWORD_TAG = "Password";
|
||||
private static final String AUTHENTICATED_TAG = "Authenticated";
|
||||
private static final String AUTHENTICATED_UNTIL_TAG = "AuthenticatedUntil";
|
||||
private static final String DESKTOP_TAG = "Desktop";
|
||||
private static final String BASILISK_SCORE_TAG = "BasiliskScore";
|
||||
private static final String BASILISK_HIGH_SCORE_TAG = "BasiliskHighScore";
|
||||
private static final String ANTMAN_SCORE_TAG = "AntmanScore";
|
||||
private static final String ANTMAN_HIGH_SCORE_TAG = "AntmanHighScore";
|
||||
private static final String BLOCKLE_DAY_TAG = "BlockleDay";
|
||||
private static final String BLOCKLE_GUESSES_TAG = "BlockleGuesses";
|
||||
private static final long AUTHENTICATION_TIMEOUT = 6000L;
|
||||
private final Set<ResourceLocation> physicalDiskIds = new LinkedHashSet<>();
|
||||
private final ComputerFileSystem fileSystem = new ComputerFileSystem();
|
||||
private final ComputerDesktopState desktopState = new ComputerDesktopState();
|
||||
private final AnimatableInstanceCache animatableCache = GeckoLibUtil.createInstanceCache(this);
|
||||
private static final RawAnimation ON = RawAnimation.begin().thenLoop("on_state");
|
||||
private static final RawAnimation OFF = RawAnimation.begin().thenLoop("off_state");
|
||||
private static final RawAnimation TURN_ON = RawAnimation.begin().thenPlay("turn_on");
|
||||
private static final RawAnimation TURN_OFF = RawAnimation.begin().thenPlay("turn_off");
|
||||
private long activeUntil;
|
||||
private String password = "";
|
||||
private boolean authenticated;
|
||||
private long authenticatedUntil;
|
||||
private java.util.UUID activeUser;
|
||||
private boolean lastActive;
|
||||
private int basiliskScore;
|
||||
private int basiliskHighScore;
|
||||
private int antmanScore;
|
||||
private int antmanHighScore;
|
||||
private long blockleDay = -1L;
|
||||
private final List<String> blockleGuesses = new ArrayList<>();
|
||||
|
||||
public ComputerBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<ComputerBlockEntity>> type) {
|
||||
super(type.get(), pos, state);
|
||||
}
|
||||
|
||||
public static void tick(Level level, BlockPos pos, BlockState state, ComputerBlockEntity computer) {
|
||||
boolean active = computer.isActive();
|
||||
if (!level.isClientSide && state.getValue(ComputerBlock.ACTIVE) != active) {
|
||||
level.setBlock(pos, state.setValue(ComputerBlock.ACTIVE, active), 3);
|
||||
}
|
||||
if (active != computer.lastActive) {
|
||||
computer.lastActive = active;
|
||||
computer.triggerAnim("transitions", active ? "turn_on" : "turn_off");
|
||||
}
|
||||
if (!level.isClientSide && computer.activeUntil > 0L && level.getGameTime() >= computer.activeUntil && !level.hasNeighborSignal(pos)) {
|
||||
computer.activeUntil = 0L;
|
||||
computer.setChanged();
|
||||
level.sendBlockUpdated(pos, state, state, 3);
|
||||
}
|
||||
if (!level.isClientSide && computer.authenticated && (level.getGameTime() >= computer.authenticatedUntil || computer.activeUser == null ||
|
||||
(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getServer().getPlayerList().getPlayer(computer.activeUser) == null))) {
|
||||
computer.clearAuthentication();
|
||||
level.sendBlockUpdated(pos, state, state, 3);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean insert(ItemStack stack, Player player) {
|
||||
if (!stack.is(AntarchyObjects.FLOPPY_DISK.get())) {
|
||||
return false;
|
||||
}
|
||||
ResourceLocation diskId = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
|
||||
if (diskId == null || this.physicalDiskIds.contains(diskId)) {
|
||||
stack.shrink(1);
|
||||
activate();
|
||||
return true;
|
||||
}
|
||||
this.physicalDiskIds.add(diskId);
|
||||
ComputerGuideData.Disk disk = ComputerGuideData.disk(diskId);
|
||||
if (disk != null && !disk.wallpaper().isBlank()) {
|
||||
try {
|
||||
desktopState.unlockWallpaper(ResourceLocation.parse(disk.wallpaper()));
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
stack.shrink(1);
|
||||
activate();
|
||||
setChanged();
|
||||
if (level != null) {
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
if (level != null && !level.isClientSide) {
|
||||
activeUntil = Math.max(activeUntil, level.getGameTime() + 40L);
|
||||
if (!getBlockState().getValue(ComputerBlock.ACTIVE)) {
|
||||
level.setBlock(worldPosition, getBlockState().setValue(ComputerBlock.ACTIVE, true), 3);
|
||||
}
|
||||
setChanged();
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return level != null && (level.hasNeighborSignal(worldPosition) || isAuthenticated());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
|
||||
controllers.add(new AnimationController<>(this, "state", state -> state.setAndContinue(isActive() ? ON : OFF)));
|
||||
controllers.add(new AnimationController<>(this, "transitions", state -> PlayState.STOP)
|
||||
.triggerableAnim("turn_on", TURN_ON)
|
||||
.triggerableAnim("turn_off", TURN_OFF));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnimatableInstanceCache getAnimatableInstanceCache() {
|
||||
return animatableCache;
|
||||
}
|
||||
|
||||
public List<ResourceLocation> diskIds() {
|
||||
List<ResourceLocation> ids = new ArrayList<>(physicalDiskIds.size() + 1);
|
||||
ids.add(INTRODUCTION_DISK);
|
||||
ids.addAll(physicalDiskIds);
|
||||
return List.copyOf(ids);
|
||||
}
|
||||
|
||||
public boolean ejectOne(Player player) {
|
||||
if (physicalDiskIds.isEmpty() || !(player instanceof ServerPlayer serverPlayer)) {
|
||||
return false;
|
||||
}
|
||||
return ejectOne(serverPlayer, physicalDiskIds.iterator().next());
|
||||
}
|
||||
|
||||
public boolean ejectOne(ServerPlayer player, ResourceLocation diskId) {
|
||||
if (level == null || level.isClientSide || diskId == null || !isAuthenticated() || !playerCanReach(player) || !physicalDiskIds.remove(diskId)) {
|
||||
return false;
|
||||
}
|
||||
ItemStack disk = diskStack(diskId);
|
||||
if (!player.addItem(disk)) {
|
||||
player.drop(disk, false);
|
||||
}
|
||||
setChanged();
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dropDisks(Level level, BlockPos pos) {
|
||||
if (level.isClientSide) {
|
||||
return;
|
||||
}
|
||||
for (ResourceLocation diskId : physicalDiskIds) {
|
||||
Containers.dropItemStack(level, pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, diskStack(diskId));
|
||||
}
|
||||
physicalDiskIds.clear();
|
||||
}
|
||||
|
||||
public boolean hasPassword() {
|
||||
return !password.isEmpty();
|
||||
}
|
||||
|
||||
public boolean setPassword(ServerPlayer player, String newPassword) {
|
||||
if (newPassword == null || newPassword.isBlank() || !isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
password = newPassword;
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean initializePassword(ServerPlayer player, String newPassword) {
|
||||
if (hasPassword() || newPassword == null || newPassword.isBlank() || !claimUser(player)) {
|
||||
return false;
|
||||
}
|
||||
password = newPassword;
|
||||
authenticated = true;
|
||||
authenticatedUntil = level.getGameTime() + AUTHENTICATION_TIMEOUT;
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean initializePassword(String newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean authenticate(ServerPlayer player, String attemptedPassword) {
|
||||
if (!hasPassword() || !password.equals(attemptedPassword) || !claimUser(player)) {
|
||||
return false;
|
||||
}
|
||||
authenticated = true;
|
||||
authenticatedUntil = level == null ? 0L : level.getGameTime() + AUTHENTICATION_TIMEOUT;
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean authenticate(String attemptedPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isAuthenticated() {
|
||||
if (!authenticated) {
|
||||
return false;
|
||||
}
|
||||
if (level != null && level.getGameTime() >= authenticatedUntil) {
|
||||
authenticated = false;
|
||||
authenticatedUntil = 0L;
|
||||
setChanged();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isAuthenticatedBy(ServerPlayer player) {
|
||||
return isAuthenticated() && activeUser != null && activeUser.equals(player.getUUID());
|
||||
}
|
||||
|
||||
public boolean claimUser(ServerPlayer player) {
|
||||
if (level == null || level.isClientSide || !playerCanReach(player)) {
|
||||
return false;
|
||||
}
|
||||
if (activeUser != null && !activeUser.equals(player.getUUID()) && isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
activeUser = player.getUUID();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean playerCanReach(ServerPlayer player) {
|
||||
return player.level() == level && player.distanceToSqr(worldPosition.getX() + 0.5D, worldPosition.getY() + 0.5D, worldPosition.getZ() + 0.5D) <= 64.0D;
|
||||
}
|
||||
|
||||
public ComputerFileSystem fileSystem() {
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
public ComputerDesktopState desktopState() {
|
||||
return desktopState;
|
||||
}
|
||||
|
||||
public int basiliskScore() {
|
||||
return basiliskScore;
|
||||
}
|
||||
|
||||
public int basiliskHighScore() {
|
||||
return basiliskHighScore;
|
||||
}
|
||||
|
||||
public void setBasiliskScore(int score) {
|
||||
basiliskScore = Math.max(0, score);
|
||||
basiliskHighScore = Math.max(basiliskHighScore, basiliskScore);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public int antmanScore() { return antmanScore; }
|
||||
public int antmanHighScore() { return antmanHighScore; }
|
||||
public void setAntmanScore(int score) {
|
||||
antmanScore = Math.max(0, score);
|
||||
antmanHighScore = Math.max(antmanHighScore, antmanScore);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public long blockleDay() {
|
||||
return blockleDay;
|
||||
}
|
||||
|
||||
public List<String> blockleGuesses() {
|
||||
return List.copyOf(blockleGuesses);
|
||||
}
|
||||
|
||||
public void resetBlockle(long day) {
|
||||
if (blockleDay == day) return;
|
||||
blockleDay = day;
|
||||
blockleGuesses.clear();
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public boolean addBlockleGuess(String guess) {
|
||||
if (blockleGuesses.size() >= 6) return false;
|
||||
blockleGuesses.add(guess);
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean unlockWallpaper(ResourceLocation id) {
|
||||
return serverDesktopMutation(() -> desktopState.unlockWallpaper(id));
|
||||
}
|
||||
|
||||
public boolean installProgram(ResourceLocation id) {
|
||||
return serverDesktopMutation(() -> desktopState.installProgram(id));
|
||||
}
|
||||
|
||||
public boolean uninstallProgram(ResourceLocation id) {
|
||||
return serverDesktopMutation(() -> desktopState.uninstallProgram(id));
|
||||
}
|
||||
|
||||
public boolean selectWallpaper(ResourceLocation id) {
|
||||
return serverDesktopMutation(() -> desktopState.selectWallpaper(id));
|
||||
}
|
||||
|
||||
public boolean setIconPosition(ResourceLocation id, int x, int y) {
|
||||
return serverDesktopMutation(() -> desktopState.setIconPosition(id, x, y));
|
||||
}
|
||||
|
||||
public boolean removeIconPosition(ResourceLocation id) {
|
||||
return serverDesktopMutation(() -> desktopState.removeIconPosition(id));
|
||||
}
|
||||
|
||||
private boolean serverDesktopMutation(java.util.function.BooleanSupplier mutation) {
|
||||
if (level == null || level.isClientSide) {
|
||||
return false;
|
||||
}
|
||||
boolean changed = mutation.getAsBoolean();
|
||||
if (changed) {
|
||||
setChanged();
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public boolean canUseFileSystem(ServerPlayer player) {
|
||||
return level != null && !level.isClientSide && isAuthenticated() && playerCanReach(player) && (activeUser == null || activeUser.equals(player.getUUID()));
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
clearAuthentication();
|
||||
}
|
||||
|
||||
public void releaseUser(ServerPlayer player) {
|
||||
if (activeUser != null && activeUser.equals(player.getUUID())) {
|
||||
activeUser = null;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearAuthentication() {
|
||||
authenticated = false;
|
||||
authenticatedUntil = 0L;
|
||||
activeUser = null;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public long authenticationTimeout() {
|
||||
return AUTHENTICATION_TIMEOUT;
|
||||
}
|
||||
|
||||
public boolean hasActiveUser() {
|
||||
return activeUser != null && isAuthenticated();
|
||||
}
|
||||
|
||||
private static ItemStack diskStack(ResourceLocation diskId) {
|
||||
ItemStack stack = new ItemStack(AntarchyObjects.FLOPPY_DISK.get());
|
||||
stack.set(AntarchyObjects.FLOPPY_DISK_COMPONENT.get(), diskId);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.loadAdditional(tag, registries);
|
||||
physicalDiskIds.clear();
|
||||
ListTag disks = tag.getList(DISKS_TAG, 8);
|
||||
for (int index = 0; index < disks.size(); index++) {
|
||||
try {
|
||||
ResourceLocation diskId = ResourceLocation.parse(disks.getString(index));
|
||||
if (!INTRODUCTION_DISK.equals(diskId)) {
|
||||
physicalDiskIds.add(diskId);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
activeUntil = tag.getLong(ACTIVE_UNTIL_TAG);
|
||||
password = tag.getString(PASSWORD_TAG);
|
||||
fileSystem.load(tag, registries);
|
||||
desktopState.load(tag.getCompound(DESKTOP_TAG));
|
||||
basiliskScore = tag.getInt(BASILISK_SCORE_TAG);
|
||||
basiliskHighScore = tag.getInt(BASILISK_HIGH_SCORE_TAG);
|
||||
antmanScore = tag.getInt(ANTMAN_SCORE_TAG);
|
||||
antmanHighScore = tag.getInt(ANTMAN_HIGH_SCORE_TAG);
|
||||
blockleDay = tag.contains(BLOCKLE_DAY_TAG) ? tag.getLong(BLOCKLE_DAY_TAG) : -1L;
|
||||
blockleGuesses.clear();
|
||||
ListTag blockle = tag.getList(BLOCKLE_GUESSES_TAG, 8);
|
||||
for (int index = 0; index < blockle.size(); index++) {
|
||||
String guess = blockle.getString(index);
|
||||
if (guess.length() == 5 && !blockleGuesses.contains(guess) && blockleGuesses.size() < 6) blockleGuesses.add(guess);
|
||||
}
|
||||
authenticated = false;
|
||||
authenticatedUntil = 0L;
|
||||
activeUser = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.saveAdditional(tag, registries);
|
||||
ListTag disks = new ListTag();
|
||||
for (ResourceLocation diskId : physicalDiskIds) {
|
||||
disks.add(StringTag.valueOf(diskId.toString()));
|
||||
}
|
||||
tag.put(DISKS_TAG, disks);
|
||||
tag.putLong(ACTIVE_UNTIL_TAG, activeUntil);
|
||||
tag.putString(PASSWORD_TAG, password);
|
||||
fileSystem.save(tag, registries);
|
||||
tag.put(DESKTOP_TAG, desktopState.save());
|
||||
tag.putInt(BASILISK_SCORE_TAG, basiliskScore);
|
||||
tag.putInt(BASILISK_HIGH_SCORE_TAG, basiliskHighScore);
|
||||
tag.putInt(ANTMAN_SCORE_TAG, antmanScore);
|
||||
tag.putInt(ANTMAN_HIGH_SCORE_TAG, antmanHighScore);
|
||||
tag.putLong(BLOCKLE_DAY_TAG, blockleDay);
|
||||
ListTag blockle = new ListTag();
|
||||
for (String guess : blockleGuesses) blockle.add(StringTag.valueOf(guess));
|
||||
tag.put(BLOCKLE_GUESSES_TAG, blockle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putBoolean("HasPassword", hasPassword());
|
||||
tag.putBoolean(AUTHENTICATED_TAG, isAuthenticated());
|
||||
ListTag disks = new ListTag();
|
||||
for (ResourceLocation diskId : physicalDiskIds) disks.add(StringTag.valueOf(diskId.toString()));
|
||||
tag.put(DISKS_TAG, disks);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.network.protocol.Packet<net.minecraft.network.protocol.game.ClientGamePacketListener> getUpdatePacket() {
|
||||
return net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket.create(this);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -30,6 +30,7 @@ import java.util.function.Supplier;
|
||||
|
||||
public final class UnderVaultBlockEntity extends BlockEntity {
|
||||
private final VaultBlockEntity vaultData;
|
||||
private boolean playerWasNearby;
|
||||
|
||||
public UnderVaultBlockEntity(BlockPos pos, BlockState state, Supplier<? extends BlockEntityType<UnderVaultBlockEntity>> type) {
|
||||
super(type.get(), pos, state);
|
||||
@@ -49,11 +50,12 @@ public final class UnderVaultBlockEntity extends BlockEntity {
|
||||
// The vanilla vault detector can miss this ceiling-mounted wrapper because its
|
||||
// block state has a separate render-facing property. Keep the state synchronized
|
||||
// with the same four-block activation range before running vanilla's state machine.
|
||||
if (state.getValue(UnderVaultBlock.STATE) == VaultState.INACTIVE
|
||||
&& !level.getEntitiesOfClass(Player.class, new AABB(pos).inflate(4.0D), player -> !player.isSpectator()).isEmpty()) {
|
||||
boolean playerNearby = !level.getEntitiesOfClass(Player.class, new AABB(pos).inflate(4.0D), player -> !player.isSpectator()).isEmpty();
|
||||
if (state.getValue(UnderVaultBlock.STATE) == VaultState.INACTIVE && playerNearby && !blockEntity.playerWasNearby) {
|
||||
state = state.setValue(UnderVaultBlock.STATE, VaultState.ACTIVE);
|
||||
level.setBlockAndUpdate(pos, state);
|
||||
}
|
||||
blockEntity.playerWasNearby = playerNearby;
|
||||
VaultBlockEntity.Server.tick(level, pos, state, blockEntity.vaultData.getConfig(), blockEntity.vaultData.getServerData(), blockEntity.vaultData.getSharedData());
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -20,6 +20,7 @@ import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawner;
|
||||
import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerConfig;
|
||||
@@ -182,6 +183,16 @@ public final class UndertrialSpawnerBlockEntity extends BlockEntity implements T
|
||||
return trialSpawner;
|
||||
}
|
||||
|
||||
public void setSpawnEntity(EntityType<?> entityType) {
|
||||
if (this.level == null) {
|
||||
return;
|
||||
}
|
||||
// Match vanilla Trial Spawner interaction: override the next mob's entity id while
|
||||
// preserving weighted potentials, ominous settings, and the trial's reward configuration.
|
||||
this.trialSpawner.getData().setEntityId(this.trialSpawner, this.level.random, entityType);
|
||||
this.markUpdated();
|
||||
}
|
||||
|
||||
public double getClientSpin(float partialTick) {
|
||||
return this.clientOldSpin + (this.clientSpin - this.clientOldSpin) * partialTick;
|
||||
}
|
||||
@@ -199,7 +210,7 @@ public final class UndertrialSpawnerBlockEntity extends BlockEntity implements T
|
||||
|
||||
private TrialSpawner createRewardlessSpawner(TrialSpawner source) {
|
||||
TrialSpawner rewardless = new TrialSpawner(withoutLootTables(source.getNormalConfig()), withoutLootTables(source.getOminousConfig()),
|
||||
source.getData(), source.getRequiredPlayerRange(), targetCooldownLength(source), this,
|
||||
source.getData(), targetCooldownLength(source), source.getRequiredPlayerRange(), this,
|
||||
source.getPlayerDetector(), source.getEntitySelector());
|
||||
rewardless.overridePeacefulAndMobSpawnRule();
|
||||
return rewardless;
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class AntarchyClientHooks {
|
||||
private static Consumer<BlockPos> computerOpener = pos -> {
|
||||
};
|
||||
|
||||
private AntarchyClientHooks() {
|
||||
}
|
||||
|
||||
public static void setComputerOpener(Consumer<BlockPos> opener) {
|
||||
computerOpener = opener;
|
||||
}
|
||||
|
||||
public static void openComputer(BlockPos pos) {
|
||||
computerOpener.accept(pos);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGamePayload;
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGameResultPayload;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
/** Client state for Antarchy-owned games hosted by AntOS. */
|
||||
public final class AntarchyGameClientState {
|
||||
private static final Map<String, String> GAME_STATES = new ConcurrentHashMap<>();
|
||||
|
||||
private AntarchyGameClientState() { }
|
||||
|
||||
public static void update(AntarchyGameResultPayload result) {
|
||||
String[] envelope = result.data().split("\u0000", 3);
|
||||
if (envelope.length < 3) return;
|
||||
if (envelope[0].equals(Integer.toString(AntarchyGamePayload.BASILISK_STATE))
|
||||
|| envelope[0].equals(Integer.toString(AntarchyGamePayload.ANTMAN_STATE))) {
|
||||
GAME_STATES.put(key(result.pos(), envelope[0]),
|
||||
result.success() ? envelope[2] : "");
|
||||
} else if (envelope[0].equals(Integer.toString(AntarchyGamePayload.BLOCKLE_STATE))
|
||||
|| envelope[0].equals(Integer.toString(AntarchyGamePayload.BLOCKLE_GUESS))) {
|
||||
if (!result.success()) return;
|
||||
BlockleClientState.update(result);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getGameState(BlockPos pos, int action) { return GAME_STATES.get(key(pos, Integer.toString(action))); }
|
||||
public static void clearGameState(BlockPos pos, int action) { GAME_STATES.remove(key(pos, Integer.toString(action))); }
|
||||
private static String key(BlockPos pos, String action) { return pos.asLong() + "|" + action; }
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import com.craisinlord.antarchy.content.network.AntmailResultPayload;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailDebug;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailMessage;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class AntmailClientState {
|
||||
private static final int MAX_CACHED_DETAILS_PER_MAILBOX = 8;
|
||||
private static final Map<BlockPos, AntmailResultPayload> RESULTS = new ConcurrentHashMap<>();
|
||||
private static final Map<BlockPos, AntmailResultPayload> MAILBOXES = new ConcurrentHashMap<>();
|
||||
private static final Map<BlockPos, Long> VERSIONS = new ConcurrentHashMap<>();
|
||||
private static final Map<BlockPos, Map<java.util.UUID, AntmailMessage>> DETAILS = new ConcurrentHashMap<>();
|
||||
|
||||
private AntmailClientState() {
|
||||
}
|
||||
|
||||
public static void update(AntmailResultPayload payload) {
|
||||
AntmailDebug.log("S2C result pos=" + payload.pos() + " status=" + payload.status() + " detail=" + payload.detail()
|
||||
+ " addressPresent=" + !payload.address().isBlank() + " dataChars=" + payload.data().length()
|
||||
+ " version=" + payload.version());
|
||||
RESULTS.put(payload.pos(), payload);
|
||||
if ("unchanged".equals(payload.detail())) {
|
||||
if (MAILBOXES.containsKey(payload.pos())) {
|
||||
VERSIONS.put(payload.pos(), payload.version());
|
||||
} else {
|
||||
// An unchanged response cannot initialize a client that has no cached snapshot.
|
||||
// Keep the next poll unconditional instead of getting stuck on "unchanged".
|
||||
AntmailDebug.log("received unchanged without cached mailbox at " + payload.pos() + "; forcing full snapshot on next poll");
|
||||
VERSIONS.remove(payload.pos());
|
||||
}
|
||||
return;
|
||||
} else if ("message_detail".equals(payload.detail()) && !payload.data().isBlank()) {
|
||||
try {
|
||||
AntmailMessage message = AntmailMessage.fromTag(com.craisinlord.antarchy.content.antmail.AntmailWire.decodeTag(payload.data()));
|
||||
Map<java.util.UUID, AntmailMessage> details = DETAILS.computeIfAbsent(payload.pos(), ignored -> new ConcurrentHashMap<>());
|
||||
details.put(message.id(), message);
|
||||
while (details.size() > MAX_CACHED_DETAILS_PER_MAILBOX) {
|
||||
java.util.UUID oldest = details.keySet().iterator().next();
|
||||
details.remove(oldest);
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
} else if (!payload.data().isBlank()) {
|
||||
MAILBOXES.put(payload.pos(), payload);
|
||||
VERSIONS.put(payload.pos(), payload.version());
|
||||
} else {
|
||||
// A failed or unconfigured response is authoritative; never show a
|
||||
// mailbox from a previous address/computer at this position.
|
||||
MAILBOXES.remove(payload.pos());
|
||||
VERSIONS.remove(payload.pos());
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear(BlockPos pos) {
|
||||
RESULTS.remove(pos);
|
||||
MAILBOXES.remove(pos);
|
||||
VERSIONS.remove(pos);
|
||||
DETAILS.remove(pos);
|
||||
}
|
||||
|
||||
public static AntmailResultPayload get(BlockPos pos) {
|
||||
return RESULTS.get(pos);
|
||||
}
|
||||
|
||||
public static AntmailResultPayload getMailbox(BlockPos pos) {
|
||||
return MAILBOXES.get(pos);
|
||||
}
|
||||
|
||||
public static long getVersion(BlockPos pos) {
|
||||
return VERSIONS.getOrDefault(pos, 0L);
|
||||
}
|
||||
|
||||
public static AntmailMessage getMessage(BlockPos pos, java.util.UUID id) {
|
||||
Map<java.util.UUID, AntmailMessage> details = DETAILS.get(pos);
|
||||
return details == null ? null : details.get(id);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,22 +1,22 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGameResultPayload;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class BlockleClientState {
|
||||
private static final Map<BlockPos, ComputerAccessResultPayload> RESULTS = new ConcurrentHashMap<>();
|
||||
private static final Map<BlockPos, AntarchyGameResultPayload> RESULTS = new ConcurrentHashMap<>();
|
||||
|
||||
private BlockleClientState() {
|
||||
}
|
||||
|
||||
public static void update(ComputerAccessResultPayload result) {
|
||||
public static void update(AntarchyGameResultPayload result) {
|
||||
RESULTS.put(result.pos(), result);
|
||||
}
|
||||
|
||||
public static ComputerAccessResultPayload get(BlockPos pos) {
|
||||
public static AntarchyGameResultPayload get(BlockPos pos) {
|
||||
return RESULTS.get(pos);
|
||||
}
|
||||
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ComputerAccessClientState {
|
||||
private static final Map<BlockPos, ComputerAccessResultPayload> RESULTS = new ConcurrentHashMap<>();
|
||||
|
||||
private ComputerAccessClientState() {
|
||||
}
|
||||
|
||||
public static void update(ComputerAccessResultPayload result) {
|
||||
ComputerStructureLocatorClientState.update(result);
|
||||
if (result.data().startsWith(com.craisinlord.antarchy.content.network.ComputerAccessPayload.ARCHIVE_STATE + "\0")) {
|
||||
String[] envelope = result.data().split("\u0000", 3);
|
||||
if (envelope.length == 3) {
|
||||
com.craisinlord.antarchy.content.guide.ComputerGuideData.applyNetworkSnapshot(envelope[2]);
|
||||
}
|
||||
}
|
||||
if (result.data().startsWith("18\u0000") || result.data().startsWith("19\u0000")) {
|
||||
BlockleClientState.update(result);
|
||||
}
|
||||
// File, terminal, desktop, and wallpaper responses carry their own operation
|
||||
// status. They must not replace the last authoritative login state.
|
||||
if (result.data().isBlank()) RESULTS.put(result.pos(), result);
|
||||
ComputerFileSystemClientState.update(result);
|
||||
}
|
||||
|
||||
public static void clear(BlockPos pos) {
|
||||
RESULTS.remove(pos);
|
||||
}
|
||||
|
||||
public static ComputerAccessResultPayload get(BlockPos pos) {
|
||||
return RESULTS.get(pos);
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessPayload;
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ComputerFileSystemClientState {
|
||||
private static final Map<BlockPos, State> STATES = new ConcurrentHashMap<>();
|
||||
|
||||
private ComputerFileSystemClientState() {
|
||||
}
|
||||
|
||||
public static void update(ComputerAccessResultPayload payload) {
|
||||
if (payload.data().isEmpty()) return;
|
||||
String[] envelope = payload.data().split("\u0000", 3);
|
||||
if (envelope.length < 3) return;
|
||||
int action;
|
||||
try {
|
||||
action = Integer.parseInt(envelope[0]);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return;
|
||||
}
|
||||
// Terminal, desktop, and game responses use the same payload envelope,
|
||||
// but their status must not leak into the file explorer's shared state.
|
||||
if (action != ComputerAccessPayload.FILE_LIST
|
||||
&& action != ComputerAccessPayload.FILE_OPEN
|
||||
&& action != ComputerAccessPayload.FILE_CREATE
|
||||
&& action != ComputerAccessPayload.FILE_SAVE
|
||||
&& action != ComputerAccessPayload.FILE_DELETE
|
||||
&& action != ComputerAccessPayload.FILE_MOVE) {
|
||||
return;
|
||||
}
|
||||
State state = STATES.computeIfAbsent(payload.pos(), ignored -> new State());
|
||||
state.success = payload.result() == ComputerAccessResultPayload.SUCCESS;
|
||||
state.error = envelope[1];
|
||||
if (state.success) state.error = "";
|
||||
if (action == ComputerAccessPayload.FILE_CREATE || action == ComputerAccessPayload.FILE_SAVE
|
||||
|| action == ComputerAccessPayload.FILE_DELETE || action == ComputerAccessPayload.FILE_MOVE) {
|
||||
state.lastMutationAction = action;
|
||||
state.lastMutationPath = envelope[2];
|
||||
state.lastMutationSuccess = state.success;
|
||||
state.lastMutationError = envelope[1];
|
||||
}
|
||||
if (action == ComputerAccessPayload.FILE_LIST) {
|
||||
state.files = new ArrayList<>();
|
||||
if (state.success && !envelope[2].isEmpty()) state.files.addAll(List.of(envelope[2].split("\\n")));
|
||||
} else if (action == ComputerAccessPayload.FILE_OPEN) {
|
||||
if (state.success) {
|
||||
String[] file = envelope[2].split("\u0000", 2);
|
||||
if (file.length == 2) {
|
||||
state.openedPath = file[0];
|
||||
state.openedContents = file[1];
|
||||
}
|
||||
} else {
|
||||
state.openedPath = "";
|
||||
state.openedContents = "";
|
||||
}
|
||||
} else if ((action == ComputerAccessPayload.FILE_DELETE || action == ComputerAccessPayload.FILE_MOVE) && state.success) {
|
||||
state.openedPath = "";
|
||||
state.openedContents = "";
|
||||
}
|
||||
}
|
||||
|
||||
public static State get(BlockPos pos) {
|
||||
return STATES.computeIfAbsent(pos, ignored -> new State());
|
||||
}
|
||||
|
||||
public static final class State {
|
||||
private List<String> files = List.of();
|
||||
private String openedPath = "";
|
||||
private String openedContents = "";
|
||||
private String error = "";
|
||||
private boolean success;
|
||||
private int lastMutationAction = -1;
|
||||
private String lastMutationPath = "";
|
||||
private boolean lastMutationSuccess;
|
||||
private String lastMutationError = "";
|
||||
|
||||
public List<String> files() { return List.copyOf(files); }
|
||||
public String openedPath() { return openedPath; }
|
||||
public String openedContents() { return openedContents; }
|
||||
public String error() { return error; }
|
||||
public boolean success() { return success; }
|
||||
public int lastMutationAction() { return lastMutationAction; }
|
||||
public String lastMutationPath() { return lastMutationPath; }
|
||||
public boolean lastMutationSuccess() { return lastMutationSuccess; }
|
||||
public String lastMutationError() { return lastMutationError; }
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client;
|
||||
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessPayload;
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ComputerStructureLocatorClientState {
|
||||
private static final Map<BlockPos, String> RESULTS = new ConcurrentHashMap<>();
|
||||
|
||||
private ComputerStructureLocatorClientState() {
|
||||
}
|
||||
|
||||
public static void searching(BlockPos pos, String dimensionId) {
|
||||
String dimension = dimensionId == null || dimensionId.isBlank() ? "TARGET DIMENSION" : dimensionId.toUpperCase(java.util.Locale.ROOT);
|
||||
RESULTS.put(pos, "SEARCHING IN " + dimension + "...");
|
||||
}
|
||||
|
||||
public static void update(ComputerAccessResultPayload payload) {
|
||||
String[] envelope = payload.data().split("\u0000", 3);
|
||||
if (envelope.length != 3) return;
|
||||
try {
|
||||
if (Integer.parseInt(envelope[0]) != ComputerAccessPayload.LOCATE_STRUCTURE) return;
|
||||
} catch (NumberFormatException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = switch (envelope[1]) {
|
||||
case "not_found" -> "NO STRUCTURE FOUND WITHIN RANGE";
|
||||
case "dimension_unavailable" -> "TARGET DIMENSION UNAVAILABLE";
|
||||
default -> "LOCATOR UNAVAILABLE";
|
||||
};
|
||||
RESULTS.put(payload.pos(), payload.result() == ComputerAccessResultPayload.SUCCESS
|
||||
? "STRUCTURE START // " + envelope[2]
|
||||
: message);
|
||||
}
|
||||
|
||||
public static String get(BlockPos pos) {
|
||||
return RESULTS.getOrDefault(pos, "COORDINATES // SEARCH WHEN SELECTED");
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.craisinlord.antarchy.content.client.game;
|
||||
|
||||
import com.craisinlord.antos.api.client.game.ComputerGame;
|
||||
import com.craisinlord.antos.api.client.game.ComputerGameRegistry;
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGameNetworking;
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGamePayload;
|
||||
import com.craisinlord.antarchy.content.client.AntarchyGameClientState;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import java.util.function.IntPredicate;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Registers Antarchy's native games with the AntOS Games app. */
|
||||
public final class AntarchyComputerGames {
|
||||
private AntarchyComputerGames() {}
|
||||
|
||||
public static void register() {
|
||||
ComputerGameRegistry.register(game("basilisk", "BASILISK", pos -> {
|
||||
BasiliskProgram program = new BasiliskProgram(true, 0, 0, score -> AntarchyGameNetworking.saveBasiliskScore(pos, score));
|
||||
var loaded = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
AntarchyGameNetworking.requestBasiliskState(pos);
|
||||
return new GameSession(() -> { if (!loaded.get()) loaded.set(loadScores(pos, AntarchyGamePayload.BASILISK_STATE, program::setStoredScores)); program.tick(); }, program::render, key -> loaded.get() && program.keyPressed(key), ignored -> false);
|
||||
}));
|
||||
ComputerGameRegistry.register(game("antman", "ANTMAN.EXE", pos -> {
|
||||
AntmanProgram program = new AntmanProgram(true, 0, 0, score -> AntarchyGameNetworking.saveAntmanScore(pos, score));
|
||||
var loaded = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
AntarchyGameNetworking.requestAntmanState(pos);
|
||||
return new GameSession(() -> { if (!loaded.get()) loaded.set(loadScores(pos, AntarchyGamePayload.ANTMAN_STATE, program::setStoredScores)); program.tick(); }, program::render, key -> loaded.get() && program.keyPressed(key), ignored -> false);
|
||||
}));
|
||||
ComputerGameRegistry.register(game("blockle", "BLOCKLE", pos -> {
|
||||
BlockleProgram program = new BlockleProgram(pos);
|
||||
program.setInstalled(true);
|
||||
return new GameSession(program::tick, program::render, program::keyPressed, program::charTyped);
|
||||
}));
|
||||
}
|
||||
|
||||
private static boolean loadScores(BlockPos pos, int action, java.util.function.BiConsumer<Integer, Integer> setter) {
|
||||
String response = AntarchyGameClientState.getGameState(pos, action);
|
||||
if (response == null) return false;
|
||||
if (response.isBlank()) { AntarchyGameClientState.clearGameState(pos, action); return true; }
|
||||
String[] scores = response.split("\0", 2);
|
||||
if (scores.length != 2) return false;
|
||||
try { setter.accept(Integer.parseInt(scores[0]), Integer.parseInt(scores[1])); }
|
||||
catch (NumberFormatException ignored) { return false; }
|
||||
AntarchyGameClientState.clearGameState(pos, action);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ComputerGame game(String path, String title, ProgramFactory factory) {
|
||||
ResourceLocation id = ResourceLocation.fromNamespaceAndPath("antarchy", path);
|
||||
ResourceLocation disk = ResourceLocation.fromNamespaceAndPath("antarchy", path + "_game");
|
||||
return new ComputerGame() {
|
||||
@Override public ResourceLocation id() { return id; }
|
||||
@Override public ResourceLocation diskId() { return disk; }
|
||||
@Override public String title() { return title; }
|
||||
@Override public Session create(BlockPos position) { return factory.create(position); }
|
||||
};
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ProgramFactory { ComputerGame.Session create(BlockPos position); }
|
||||
|
||||
@FunctionalInterface private interface GameRenderer { void render(GuiGraphics graphics, Font font, int x, int y, int width, int height); }
|
||||
private static final class GameSession implements ComputerGame.Session {
|
||||
private final Runnable ticker;
|
||||
private final GameRenderer renderer;
|
||||
private final IntPredicate keyHandler;
|
||||
private final Predicate<Character> charHandler;
|
||||
private GameSession(Runnable ticker, GameRenderer renderer, IntPredicate keyHandler, Predicate<Character> charHandler) {
|
||||
this.ticker = ticker; this.renderer = renderer; this.keyHandler = keyHandler; this.charHandler = charHandler;
|
||||
}
|
||||
@Override public void tick() { ticker.run(); }
|
||||
@Override public void render(GuiGraphics graphics, Font font, int x, int y, int width, int height) { renderer.render(graphics, font, x, y, width, height); }
|
||||
@Override public boolean keyPressed(int keyCode) { return keyHandler.test(keyCode); }
|
||||
@Override public boolean charTyped(char codePoint) { return charHandler.test(codePoint); }
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
package com.craisinlord.antarchy.content.client.game;
|
||||
|
||||
import com.craisinlord.antarchy.content.client.BlockleClientState;
|
||||
import com.craisinlord.antarchy.content.network.ComputerAccessResultPayload;
|
||||
import com.craisinlord.antarchy.content.network.ComputerNetworking;
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGameResultPayload;
|
||||
import com.craisinlord.antarchy.content.network.AntarchyGameNetworking;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -40,11 +40,11 @@ public final class BlockleProgram {
|
||||
}
|
||||
|
||||
public void requestState() {
|
||||
if (installed) ComputerNetworking.requestBlockleState(position);
|
||||
if (installed) AntarchyGameNetworking.requestBlockleState(position);
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
ComputerAccessResultPayload result = BlockleClientState.get(position);
|
||||
AntarchyGameResultPayload result = BlockleClientState.get(position);
|
||||
if (result == null) return;
|
||||
if (result.data().equals(lastResponse)) return;
|
||||
lastResponse = result.data();
|
||||
@@ -72,7 +72,7 @@ public final class BlockleProgram {
|
||||
if (keyCode == 257 || keyCode == 335) {
|
||||
if (!received) return true;
|
||||
if (input.length() == 5 && phase.equals("PLAYING") && !pending) {
|
||||
ComputerNetworking.submitBlockleGuess(position, input);
|
||||
AntarchyGameNetworking.submitBlockleGuess(position, input);
|
||||
pending = true;
|
||||
status = "SUBMITTING";
|
||||
}
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client.model;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import software.bernie.geckolib.model.GeoModel;
|
||||
|
||||
public final class ComputerModel extends GeoModel<ComputerBlockEntity> {
|
||||
private static final ResourceLocation MODEL = ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "geo/computer.geo.json");
|
||||
private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "textures/block/computer/computer.png");
|
||||
private static final ResourceLocation ANIMATION = ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "animations/computer.animation.json");
|
||||
|
||||
@Override
|
||||
public ResourceLocation getModelResource(ComputerBlockEntity animatable) {
|
||||
return MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureResource(ComputerBlockEntity animatable) {
|
||||
return TEXTURE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getAnimationResource(ComputerBlockEntity animatable) {
|
||||
return ANIMATION;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.craisinlord.antarchy.content.client.particle;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.particle.Particle;
|
||||
import net.minecraft.client.particle.ParticleProvider;
|
||||
import net.minecraft.client.particle.ParticleRenderType;
|
||||
import net.minecraft.client.particle.SpriteSet;
|
||||
import net.minecraft.client.particle.TextureSheetParticle;
|
||||
import net.minecraft.core.particles.SimpleParticleType;
|
||||
|
||||
public final class BugSprayParticle extends TextureSheetParticle {
|
||||
private final SpriteSet sprites;
|
||||
|
||||
private BugSprayParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, SpriteSet sprites) {
|
||||
super(level, x, y, z, xd, yd, zd);
|
||||
this.sprites = sprites;
|
||||
this.setSprite(sprites.get(level.random));
|
||||
this.lifetime = 14 + this.random.nextInt(8);
|
||||
this.quadSize = 0.16F + this.random.nextFloat() * 0.14F;
|
||||
this.friction = 0.88F;
|
||||
this.gravity = -0.01F;
|
||||
this.alpha = 0.9F;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (!this.removed) {
|
||||
this.setSprite(this.sprites.get(Math.min(this.age, this.lifetime - 1), this.lifetime));
|
||||
this.alpha = 0.9F * (1.0F - (float) this.age / this.lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParticleRenderType getRenderType() {
|
||||
return ParticleRenderType.PARTICLE_SHEET_TRANSLUCENT;
|
||||
}
|
||||
|
||||
public static final class Provider implements ParticleProvider<SimpleParticleType> {
|
||||
private final SpriteSet sprites;
|
||||
|
||||
public Provider(SpriteSet sprites) {
|
||||
this.sprites = sprites;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Particle createParticle(SimpleParticleType type, ClientLevel level, double x, double y, double z,
|
||||
double xd, double yd, double zd) {
|
||||
return new BugSprayParticle(level, x, y, z, xd, yd, zd, this.sprites);
|
||||
}
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client.renderer;
|
||||
|
||||
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
|
||||
import com.craisinlord.antarchy.content.client.model.ComputerModel;
|
||||
import net.minecraft.core.Direction;
|
||||
import software.bernie.geckolib.renderer.GeoBlockRenderer;
|
||||
|
||||
public final class ComputerRenderer extends GeoBlockRenderer<ComputerBlockEntity> {
|
||||
public ComputerRenderer(net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider.Context context) {
|
||||
super(new ComputerModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Direction getFacing(ComputerBlockEntity computer) {
|
||||
return computer.getBlockState().getValue(com.craisinlord.antarchy.content.block.ComputerBlock.FACING);
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.client.renderer;
|
||||
|
||||
import com.craisinlord.antarchy.content.item.RoyalAssailantBattleAxeItem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Axis;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.model.PlayerModel;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.RenderLayerParent;
|
||||
import net.minecraft.client.renderer.entity.layers.RenderLayer;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class RoyalAssailantAxeAfterimageLayer extends RenderLayer<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>> {
|
||||
private static final int[] TINTS = {
|
||||
0x66FFD966,
|
||||
0x4DFFC94D,
|
||||
0x33FFB52E,
|
||||
0x1FFF8C14
|
||||
};
|
||||
|
||||
public RoyalAssailantAxeAfterimageLayer(RenderLayerParent<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>> renderer) {
|
||||
super(renderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack poseStack, MultiBufferSource buffer, int packedLight, AbstractClientPlayer player,
|
||||
float limbSwing, float limbSwingAmount, float partialTick, float ageInTicks, float netHeadYaw,
|
||||
float headPitch) {
|
||||
if (player.isSpectator() || player.isInvisible() || player.getAttackAnim(partialTick) <= 0.01F) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack stack = player.getMainHandItem();
|
||||
HumanoidArm arm = player.getMainArm();
|
||||
if (!(stack.getItem() instanceof RoyalAssailantBattleAxeItem)) {
|
||||
stack = player.getOffhandItem();
|
||||
arm = player.getMainArm() == HumanoidArm.RIGHT ? HumanoidArm.LEFT : HumanoidArm.RIGHT;
|
||||
}
|
||||
if (!(stack.getItem() instanceof RoyalAssailantBattleAxeItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(RenderType.entityTranslucent(this.getTextureLocation(player)));
|
||||
float swing = player.getAttackAnim(partialTick);
|
||||
for (int i = TINTS.length - 1; i >= 0; i--) {
|
||||
poseStack.pushPose();
|
||||
float offset = (i + 1) * 0.08F;
|
||||
poseStack.translate(0.0D, 0.0D, offset);
|
||||
poseStack.mulPose(Axis.YP.rotation((i + 1) * 0.12F * (swing < 0.5F ? -1.0F : 1.0F)));
|
||||
this.getParentModel().renderToBuffer(
|
||||
poseStack,
|
||||
vertexConsumer,
|
||||
LightTexture.FULL_BRIGHT,
|
||||
LivingEntityRenderer.getOverlayCoords(player, 0.0F),
|
||||
TINTS[i]
|
||||
);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
for (int i = TINTS.length - 1; i >= 0; i--) {
|
||||
poseStack.pushPose();
|
||||
this.getParentModel().translateToHand(arm, poseStack);
|
||||
poseStack.mulPose(Axis.YP.rotation((i + 1) * 0.12F * (swing < 0.5F ? -1.0F : 1.0F)));
|
||||
Minecraft.getInstance().getItemRenderer().renderStatic(
|
||||
player,
|
||||
stack,
|
||||
arm == HumanoidArm.RIGHT ? ItemDisplayContext.THIRD_PERSON_RIGHT_HAND : ItemDisplayContext.THIRD_PERSON_LEFT_HAND,
|
||||
arm == HumanoidArm.LEFT,
|
||||
poseStack,
|
||||
buffer,
|
||||
player.level(),
|
||||
packedLight,
|
||||
LivingEntityRenderer.getOverlayCoords(player, 0.0F),
|
||||
player.getId()
|
||||
);
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -49,11 +49,20 @@ public final class RoyalIndicatorRenderer {
|
||||
|
||||
private static void crownPoints(VertexConsumer out, PoseStack.Pose pose, float radius, float rotation,
|
||||
float r, float g, float b, float a) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double angle = rotation + Math.PI * 2.0D * i / 3.0D;
|
||||
float x = (float) Math.cos(angle) * radius;
|
||||
float z = (float) Math.sin(angle) * radius;
|
||||
line(out, pose, x, 0.0F, z, x * 0.68F, 0.19F, z * 0.68F, r, g, b, a);
|
||||
final int spikes = 5;
|
||||
float innerRadius = radius * 0.72F;
|
||||
for (int i = 0; i < spikes; i++) {
|
||||
double outerAngle = rotation + Math.PI * 2.0D * i / spikes;
|
||||
double innerAngle = rotation + Math.PI * 2.0D * (i + 0.5D) / spikes;
|
||||
float outerX = (float) Math.cos(outerAngle) * radius;
|
||||
float outerZ = (float) Math.sin(outerAngle) * radius;
|
||||
float innerX = (float) Math.cos(innerAngle) * innerRadius;
|
||||
float innerZ = (float) Math.sin(innerAngle) * innerRadius;
|
||||
float nextOuterX = (float) Math.cos(rotation + Math.PI * 2.0D * (i + 1) / spikes) * radius;
|
||||
float nextOuterZ = (float) Math.sin(rotation + Math.PI * 2.0D * (i + 1) / spikes) * radius;
|
||||
line(out, pose, outerX, 0.0F, outerZ, innerX, 0.34F, innerZ, r, g, b, a);
|
||||
line(out, pose, innerX, 0.34F, innerZ, nextOuterX, 0.0F, nextOuterZ, r, g, b, a);
|
||||
line(out, pose, outerX, 0.0F, outerZ, nextOuterX, 0.0F, nextOuterZ, r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-3029
File diff suppressed because it is too large.
Load diff
+75
@@ -0,0 +1,75 @@
|
||||
package com.craisinlord.antarchy.content.computer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
/** Persistent Antarchy game progress keyed by AntOS computer location. */
|
||||
public final class AntarchyGameSavedData extends SavedData {
|
||||
private static final String ID = "antarchy_computer_games";
|
||||
private final Map<String, GameState> games = new HashMap<>();
|
||||
|
||||
public static AntarchyGameSavedData create() { return new AntarchyGameSavedData(); }
|
||||
|
||||
public static AntarchyGameSavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AntarchyGameSavedData data = new AntarchyGameSavedData();
|
||||
for (net.minecraft.nbt.Tag raw : tag.getList("Games", net.minecraft.nbt.Tag.TAG_COMPOUND)) {
|
||||
CompoundTag item = (CompoundTag) raw;
|
||||
GameState state = new GameState();
|
||||
state.basiliskScore = item.getInt("BasiliskScore");
|
||||
state.basiliskHighScore = item.getInt("BasiliskHighScore");
|
||||
state.antmanScore = item.getInt("AntmanScore");
|
||||
state.antmanHighScore = item.getInt("AntmanHighScore");
|
||||
state.blockleDay = item.getLong("BlockleDay");
|
||||
for (net.minecraft.nbt.Tag guess : item.getList("BlockleGuesses", net.minecraft.nbt.Tag.TAG_STRING)) state.blockleGuesses.add(guess.getAsString());
|
||||
data.games.put(item.getString("Computer"), state);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
ListTag list = new ListTag();
|
||||
games.forEach((key, state) -> {
|
||||
CompoundTag item = new CompoundTag();
|
||||
item.putString("Computer", key);
|
||||
item.putInt("BasiliskScore", state.basiliskScore);
|
||||
item.putInt("BasiliskHighScore", state.basiliskHighScore);
|
||||
item.putInt("AntmanScore", state.antmanScore);
|
||||
item.putInt("AntmanHighScore", state.antmanHighScore);
|
||||
item.putLong("BlockleDay", state.blockleDay);
|
||||
ListTag guesses = new ListTag();
|
||||
state.blockleGuesses.forEach(guess -> guesses.add(net.minecraft.nbt.StringTag.valueOf(guess)));
|
||||
item.put("BlockleGuesses", guesses);
|
||||
list.add(item);
|
||||
});
|
||||
tag.put("Games", list);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static GameState get(MinecraftServer server, String key) {
|
||||
AntarchyGameSavedData data = server.overworld().getDataStorage().computeIfAbsent(
|
||||
new SavedData.Factory<>(AntarchyGameSavedData::create, AntarchyGameSavedData::load, null), ID);
|
||||
return data.games.computeIfAbsent(key, ignored -> new GameState());
|
||||
}
|
||||
|
||||
public static void changed(MinecraftServer server) {
|
||||
server.overworld().getDataStorage().computeIfAbsent(
|
||||
new SavedData.Factory<>(AntarchyGameSavedData::create, AntarchyGameSavedData::load, null), ID).setDirty();
|
||||
}
|
||||
|
||||
public static String key(net.minecraft.resources.ResourceLocation dimension, net.minecraft.core.BlockPos pos) {
|
||||
return dimension + "|" + pos.asLong();
|
||||
}
|
||||
|
||||
public static final class GameState {
|
||||
public int basiliskScore, basiliskHighScore, antmanScore, antmanHighScore;
|
||||
public long blockleDay = Long.MIN_VALUE;
|
||||
public final List<String> blockleGuesses = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ComputerDesktopState {
|
||||
public static final ResourceLocation DEFAULT_WALLPAPER = ResourceLocation.fromNamespaceAndPath("antarchy", "grid_ant");
|
||||
public static final int MAX_UNLOCKED_WALLPAPERS = 64;
|
||||
public static final int MAX_INSTALLED_PROGRAMS = 64;
|
||||
public static final int MAX_ICON_POSITIONS = 64;
|
||||
public static final int MAX_ID_LENGTH = 128;
|
||||
public static final int MIN_ICON_COORDINATE = 0;
|
||||
public static final int MAX_ICON_COORDINATE = 4096;
|
||||
private static final String SELECTED_WALLPAPER_TAG = "SelectedWallpaper";
|
||||
private static final String UNLOCKED_WALLPAPERS_TAG = "UnlockedWallpapers";
|
||||
private static final String INSTALLED_PROGRAMS_TAG = "InstalledPrograms";
|
||||
private static final String ICON_POSITIONS_TAG = "IconPositions";
|
||||
private static final String ID_TAG = "Id";
|
||||
private static final String X_TAG = "X";
|
||||
private static final String Y_TAG = "Y";
|
||||
private ResourceLocation selectedWallpaper;
|
||||
private final Set<ResourceLocation> unlockedWallpaperIds = new LinkedHashSet<>();
|
||||
private final Set<ResourceLocation> installedProgramIds = new LinkedHashSet<>();
|
||||
private final Map<ResourceLocation, IconPosition> iconPositions = new LinkedHashMap<>();
|
||||
|
||||
public ResourceLocation selectedWallpaper() {
|
||||
return selectedWallpaper;
|
||||
}
|
||||
|
||||
public Set<ResourceLocation> unlockedWallpaperIds() {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<>(unlockedWallpaperIds));
|
||||
}
|
||||
|
||||
public Set<ResourceLocation> installedProgramIds() {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<>(installedProgramIds));
|
||||
}
|
||||
|
||||
public Map<ResourceLocation, IconPosition> iconPositions() {
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(iconPositions));
|
||||
}
|
||||
|
||||
public boolean hasWallpaper(ResourceLocation id) {
|
||||
return id != null && unlockedWallpaperIds.contains(id);
|
||||
}
|
||||
|
||||
public boolean hasProgram(ResourceLocation id) {
|
||||
return id != null && installedProgramIds.contains(id);
|
||||
}
|
||||
|
||||
public IconPosition iconPosition(ResourceLocation id) {
|
||||
return iconPositions.get(id);
|
||||
}
|
||||
|
||||
public boolean unlockWallpaper(ResourceLocation id) {
|
||||
if (!validId(id) || unlockedWallpaperIds.contains(id)) {
|
||||
return false;
|
||||
}
|
||||
if (unlockedWallpaperIds.size() >= MAX_UNLOCKED_WALLPAPERS) {
|
||||
return false;
|
||||
}
|
||||
return unlockedWallpaperIds.add(id);
|
||||
}
|
||||
|
||||
public boolean installProgram(ResourceLocation id) {
|
||||
if (!validId(id) || installedProgramIds.size() >= MAX_INSTALLED_PROGRAMS && !installedProgramIds.contains(id)) {
|
||||
return false;
|
||||
}
|
||||
return installedProgramIds.add(id);
|
||||
}
|
||||
|
||||
public boolean uninstallProgram(ResourceLocation id) {
|
||||
if (id == null || !installedProgramIds.remove(id)) {
|
||||
return false;
|
||||
}
|
||||
iconPositions.remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean selectWallpaper(ResourceLocation id) {
|
||||
if (id != null && !unlockedWallpaperIds.contains(id)) {
|
||||
return false;
|
||||
}
|
||||
if (id == null ? selectedWallpaper == null : id.equals(selectedWallpaper)) {
|
||||
return false;
|
||||
}
|
||||
selectedWallpaper = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean setIconPosition(ResourceLocation id, int x, int y) {
|
||||
if (!validId(id) || x < MIN_ICON_COORDINATE || x > MAX_ICON_COORDINATE || y < MIN_ICON_COORDINATE || y > MAX_ICON_COORDINATE) {
|
||||
return false;
|
||||
}
|
||||
IconPosition position = new IconPosition(x, y);
|
||||
if (position.equals(iconPositions.get(id))) {
|
||||
return false;
|
||||
}
|
||||
if (!iconPositions.containsKey(id) && iconPositions.size() >= MAX_ICON_POSITIONS) {
|
||||
return false;
|
||||
}
|
||||
iconPositions.put(id, position);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeIconPosition(ResourceLocation id) {
|
||||
return id != null && iconPositions.remove(id) != null;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
unlockedWallpaperIds.clear();
|
||||
installedProgramIds.clear();
|
||||
iconPositions.clear();
|
||||
unlockedWallpaperIds.add(DEFAULT_WALLPAPER);
|
||||
selectedWallpaper = DEFAULT_WALLPAPER;
|
||||
}
|
||||
|
||||
public CompoundTag save() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
if (selectedWallpaper != null) {
|
||||
tag.putString(SELECTED_WALLPAPER_TAG, selectedWallpaper.toString());
|
||||
}
|
||||
tag.put(UNLOCKED_WALLPAPERS_TAG, saveIds(unlockedWallpaperIds));
|
||||
tag.put(INSTALLED_PROGRAMS_TAG, saveIds(installedProgramIds));
|
||||
ListTag positions = new ListTag();
|
||||
for (Map.Entry<ResourceLocation, IconPosition> entry : iconPositions.entrySet()) {
|
||||
CompoundTag position = new CompoundTag();
|
||||
position.putString(ID_TAG, entry.getKey().toString());
|
||||
position.putInt(X_TAG, entry.getValue().x());
|
||||
position.putInt(Y_TAG, entry.getValue().y());
|
||||
positions.add(position);
|
||||
}
|
||||
tag.put(ICON_POSITIONS_TAG, positions);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public void load(CompoundTag tag) {
|
||||
clear();
|
||||
loadIds(tag.getList(UNLOCKED_WALLPAPERS_TAG, 8), unlockedWallpaperIds, MAX_UNLOCKED_WALLPAPERS);
|
||||
ResourceLocation storedSelection = parseId(tag.getString(SELECTED_WALLPAPER_TAG));
|
||||
selectedWallpaper = storedSelection != null && unlockedWallpaperIds.contains(storedSelection) ? storedSelection : DEFAULT_WALLPAPER;
|
||||
loadIds(tag.getList(INSTALLED_PROGRAMS_TAG, 8), installedProgramIds, MAX_INSTALLED_PROGRAMS);
|
||||
ListTag positions = tag.getList(ICON_POSITIONS_TAG, 10);
|
||||
for (int index = 0; index < positions.size() && iconPositions.size() < MAX_ICON_POSITIONS; index++) {
|
||||
CompoundTag stored = positions.getCompound(index);
|
||||
ResourceLocation id = parseId(stored.getString(ID_TAG));
|
||||
int x = stored.getInt(X_TAG);
|
||||
int y = stored.getInt(Y_TAG);
|
||||
if (id != null && x >= MIN_ICON_COORDINATE && x <= MAX_ICON_COORDINATE && y >= MIN_ICON_COORDINATE && y <= MAX_ICON_COORDINATE) {
|
||||
iconPositions.putIfAbsent(id, new IconPosition(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ListTag saveIds(Collection<ResourceLocation> ids) {
|
||||
ListTag list = new ListTag();
|
||||
for (ResourceLocation id : ids) {
|
||||
list.add(StringTag.valueOf(id.toString()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void loadIds(ListTag list, Set<ResourceLocation> destination, int limit) {
|
||||
for (int index = 0; index < list.size() && destination.size() < limit; index++) {
|
||||
ResourceLocation id = parseId(list.getString(index));
|
||||
if (id != null) {
|
||||
destination.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ResourceLocation parseId(String value) {
|
||||
if (value == null || value.isBlank() || value.length() > MAX_ID_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return ResourceLocation.parse(value);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean validId(ResourceLocation id) {
|
||||
return id != null && id.toString().length() <= MAX_ID_LENGTH;
|
||||
}
|
||||
|
||||
public record IconPosition(int x, int y) {
|
||||
}
|
||||
}
|
||||
-220
@@ -1,220 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer;
|
||||
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ComputerFileSystem {
|
||||
public static final int MAX_FILE_SIZE = 16 * 1024;
|
||||
public static final int MAX_TOTAL_SIZE = 64 * 1024;
|
||||
public static final int MAX_NODES = 128;
|
||||
public static final int MAX_PATH_LENGTH = 64;
|
||||
public static final String ROOT = "/";
|
||||
public static final String DOCUMENTS = "/documents";
|
||||
public static final String PICTURES = "/pictures";
|
||||
public static final String SYSTEM = "/system";
|
||||
private static final String FILES_TAG = "Files";
|
||||
private static final String PATH_TAG = "Path";
|
||||
private static final String TYPE_TAG = "Type";
|
||||
private static final String CONTENTS_TAG = "Contents";
|
||||
private final Map<String, ComputerFile> nodes = new LinkedHashMap<>();
|
||||
private int totalBytes;
|
||||
|
||||
public ComputerFileSystem() {
|
||||
reset();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
nodes.clear();
|
||||
nodes.put(ROOT, ComputerFile.directory(ROOT));
|
||||
nodes.put(DOCUMENTS, ComputerFile.directory(DOCUMENTS));
|
||||
nodes.put(PICTURES, ComputerFile.directory(PICTURES));
|
||||
nodes.put(SYSTEM, ComputerFile.directory(SYSTEM));
|
||||
totalBytes = 0;
|
||||
}
|
||||
|
||||
public ComputerFile get(String path) {
|
||||
return nodes.get(normalize(path));
|
||||
}
|
||||
|
||||
public List<ComputerFile> list() {
|
||||
return List.copyOf(nodes.values());
|
||||
}
|
||||
|
||||
public List<ComputerFile> listTextFiles() {
|
||||
return nodes.values().stream().filter(file -> file.type() == ComputerFile.Type.TEXT).toList();
|
||||
}
|
||||
|
||||
public Result createDirectory(String path) {
|
||||
String normalized = normalize(path);
|
||||
if (normalized.isEmpty() || ROOT.equals(normalized) || isProtected(normalized) || nodes.containsKey(normalized) || nodes.size() >= MAX_NODES) return Result.failure("invalid_path");
|
||||
int slash = normalized.lastIndexOf('/');
|
||||
if (slash > 0 && !nodesDirectory(normalized.substring(0, slash))) return Result.failure("missing_directory");
|
||||
nodes.put(normalized, ComputerFile.directory(normalized));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public Result createTextFile(String path, String contents) {
|
||||
String normalized = normalize(path);
|
||||
Result validation = validateTextPath(normalized, contents);
|
||||
if (!validation.successful()) return validation;
|
||||
if (nodes.containsKey(normalized)) return Result.failure("already_exists");
|
||||
if (nodes.size() >= MAX_NODES) return Result.failure("node_limit");
|
||||
int size = contents.length();
|
||||
if (totalBytes + size > MAX_TOTAL_SIZE) return Result.failure("storage_limit");
|
||||
nodes.put(normalized, ComputerFile.text(normalized, contents));
|
||||
totalBytes += size;
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public Result writeTextFile(String path, String contents) {
|
||||
String normalized = normalize(path);
|
||||
Result validation = validateTextPath(normalized, contents);
|
||||
if (!validation.successful()) return validation;
|
||||
ComputerFile current = nodes.get(normalized);
|
||||
if (current == null) return Result.failure("not_found");
|
||||
if (current.type() != ComputerFile.Type.TEXT && current.type() != ComputerFile.Type.IMAGE) return Result.failure("not_file");
|
||||
int replacement = totalBytes - current.contents().length() + contents.length();
|
||||
if (replacement > MAX_TOTAL_SIZE) return Result.failure("storage_limit");
|
||||
nodes.put(normalized, ComputerFile.text(normalized, contents));
|
||||
totalBytes = replacement;
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public Result createOrWriteTextFile(String path, String contents) {
|
||||
return nodes.containsKey(normalize(path)) ? writeTextFile(path, contents) : createTextFile(path, contents);
|
||||
}
|
||||
|
||||
public Result move(String source, String destination) {
|
||||
String from = normalize(source);
|
||||
String to = normalize(destination);
|
||||
if (from.isEmpty() || to.isEmpty() || ROOT.equals(from) || ROOT.equals(to) || isProtected(from) || isProtected(to)) return Result.failure("invalid_path");
|
||||
ComputerFile file = nodes.get(from);
|
||||
if (file == null) return Result.failure("not_found");
|
||||
if (nodes.containsKey(to)) return Result.failure("already_exists");
|
||||
int slash = to.lastIndexOf('/');
|
||||
if (slash > 0 && !nodesDirectory(to.substring(0, slash))) return Result.failure("missing_directory");
|
||||
if (file.type() == ComputerFile.Type.DIRECTORY && to.startsWith(from + "/")) return Result.failure("invalid_path");
|
||||
if (file.type() == ComputerFile.Type.DIRECTORY) {
|
||||
List<String> descendants = nodes.keySet().stream()
|
||||
.filter(path -> path.startsWith(from + "/"))
|
||||
.toList();
|
||||
for (String descendant : descendants) {
|
||||
String movedPath = to + descendant.substring(from.length());
|
||||
if (nodes.containsKey(movedPath)) return Result.failure("already_exists");
|
||||
}
|
||||
Map<String, ComputerFile> moved = new LinkedHashMap<>();
|
||||
moved.put(to, ComputerFile.directory(to));
|
||||
for (String descendant : descendants) {
|
||||
ComputerFile child = nodes.get(descendant);
|
||||
String movedPath = to + descendant.substring(from.length());
|
||||
moved.put(movedPath, child.type() == ComputerFile.Type.DIRECTORY
|
||||
? ComputerFile.directory(movedPath)
|
||||
: ComputerFile.text(movedPath, child.contents()));
|
||||
}
|
||||
nodes.keySet().removeIf(path -> path.equals(from) || path.startsWith(from + "/"));
|
||||
nodes.putAll(moved);
|
||||
} else {
|
||||
nodes.remove(from);
|
||||
nodes.put(to, ComputerFile.text(to, file.contents()));
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public Result delete(String path) {
|
||||
String normalized = normalize(path);
|
||||
if (isProtected(normalized)) return Result.failure("protected");
|
||||
ComputerFile file = nodes.get(normalized);
|
||||
if (file == null) return Result.failure("not_found");
|
||||
if (file.type() == ComputerFile.Type.DIRECTORY && nodes.keySet().stream().anyMatch(key -> !key.equals(normalized) && key.startsWith(normalized + "/"))) {
|
||||
return Result.failure("not_empty");
|
||||
}
|
||||
nodes.remove(normalized);
|
||||
totalBytes -= file.contents().length();
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public static String normalize(String path) {
|
||||
if (path == null || path.isBlank()) return "";
|
||||
String value = path.trim().replace('\\', '/');
|
||||
if (!value.startsWith("/")) value = "/" + value;
|
||||
while (value.contains("//")) value = value.replace("//", "/");
|
||||
if (value.length() > 1 && value.endsWith("/")) value = value.substring(0, value.length() - 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
public static boolean isProtected(String path) {
|
||||
String normalized = normalize(path);
|
||||
return normalized.equals(SYSTEM) || normalized.startsWith(SYSTEM + "/");
|
||||
}
|
||||
|
||||
public void save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
ListTag files = new ListTag();
|
||||
for (ComputerFile file : nodes.values()) {
|
||||
if (ROOT.equals(file.path())) continue;
|
||||
CompoundTag stored = new CompoundTag();
|
||||
stored.putString(PATH_TAG, file.path());
|
||||
stored.putString(TYPE_TAG, file.type().name());
|
||||
if (file.type() == ComputerFile.Type.TEXT || file.type() == ComputerFile.Type.IMAGE) stored.putString(CONTENTS_TAG, file.contents());
|
||||
files.add(stored);
|
||||
}
|
||||
tag.put(FILES_TAG, files);
|
||||
}
|
||||
|
||||
public void load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
reset();
|
||||
ListTag files = tag.getList(FILES_TAG, 10);
|
||||
for (int index = 0; index < files.size(); index++) {
|
||||
CompoundTag stored = files.getCompound(index);
|
||||
String path = normalize(stored.getString(PATH_TAG));
|
||||
if (path.isEmpty() || nodes.containsKey(path) || path.length() > MAX_PATH_LENGTH || isProtected(path)) continue;
|
||||
String type = stored.getString(TYPE_TAG);
|
||||
if ("DIRECTORY".equals(type)) {
|
||||
if (nodes.size() < MAX_NODES) nodes.put(path, ComputerFile.directory(path));
|
||||
} else if ("TEXT".equals(type) || "IMAGE".equals(type)) {
|
||||
String contents = stored.getString(CONTENTS_TAG);
|
||||
if (contents.length() <= MAX_FILE_SIZE && nodes.size() < MAX_NODES && totalBytes + contents.length() <= MAX_TOTAL_SIZE) {
|
||||
nodes.put(path, "IMAGE".equals(type) ? ComputerFile.image(path, contents) : ComputerFile.text(path, contents));
|
||||
totalBytes += contents.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Result validateTextPath(String path, String contents) {
|
||||
if (path.isEmpty() || path.length() > MAX_PATH_LENGTH || ROOT.equals(path) || isProtected(path)) return Result.failure("invalid_path");
|
||||
if (contents == null || contents.length() > MAX_FILE_SIZE) return Result.failure("file_limit");
|
||||
if (path.contains("..") || path.indexOf('\0') >= 0 || path.indexOf('\n') >= 0 || path.indexOf('\r') >= 0) return Result.failure("invalid_path");
|
||||
int slash = path.lastIndexOf('/');
|
||||
if (slash > 0 && !nodesDirectory(path.substring(0, slash))) return Result.failure("missing_directory");
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private boolean nodesDirectory(String path) {
|
||||
ComputerFile parent = nodes.get(path);
|
||||
return parent != null && parent.type() == ComputerFile.Type.DIRECTORY;
|
||||
}
|
||||
|
||||
public int totalBytes() {
|
||||
return totalBytes;
|
||||
}
|
||||
|
||||
public record Result(boolean successful, String error) {
|
||||
public static Result ok() { return new Result(true, ""); }
|
||||
public static Result failure(String error) { return new Result(false, error); }
|
||||
}
|
||||
|
||||
public record ComputerFile(String path, Type type, String contents) {
|
||||
public static ComputerFile directory(String path) { return new ComputerFile(path, Type.DIRECTORY, ""); }
|
||||
public static ComputerFile text(String path, String contents) { return path.endsWith(".antpaint") ? image(path, contents) : new ComputerFile(path, Type.TEXT, contents); }
|
||||
public static ComputerFile image(String path, String contents) { return new ComputerFile(path, Type.IMAGE, contents); }
|
||||
public enum Type { DIRECTORY, TEXT, IMAGE }
|
||||
}
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class AntPaintCanvas {
|
||||
public static final int WIDTH = 32;
|
||||
public static final int HEIGHT = 24;
|
||||
public static final int PIXEL_COUNT = WIDTH * HEIGHT;
|
||||
public static final int PACKED_LENGTH = (PIXEL_COUNT + 7) / 8;
|
||||
|
||||
private final boolean[] pixels;
|
||||
|
||||
public AntPaintCanvas() {
|
||||
this.pixels = new boolean[PIXEL_COUNT];
|
||||
}
|
||||
|
||||
private AntPaintCanvas(boolean[] pixels) {
|
||||
this.pixels = pixels;
|
||||
}
|
||||
|
||||
public boolean get(int x, int y) {
|
||||
return pixels[index(x, y)];
|
||||
}
|
||||
|
||||
public void set(int x, int y, boolean value) {
|
||||
pixels[index(x, y)] = value;
|
||||
}
|
||||
|
||||
public boolean draw(int x, int y) {
|
||||
return setIfChanged(x, y, true);
|
||||
}
|
||||
|
||||
public boolean erase(int x, int y) {
|
||||
return setIfChanged(x, y, false);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
Arrays.fill(pixels, false);
|
||||
}
|
||||
|
||||
public int floodFill(int x, int y, boolean value) {
|
||||
int start = index(x, y);
|
||||
boolean original = pixels[start];
|
||||
if (original == value) {
|
||||
return 0;
|
||||
}
|
||||
ArrayDeque<Integer> queue = new ArrayDeque<>();
|
||||
boolean[] visited = new boolean[PIXEL_COUNT];
|
||||
queue.add(start);
|
||||
int changed = 0;
|
||||
while (!queue.isEmpty()) {
|
||||
int current = queue.removeFirst();
|
||||
if (visited[current] || pixels[current] != original) {
|
||||
continue;
|
||||
}
|
||||
visited[current] = true;
|
||||
pixels[current] = value;
|
||||
changed++;
|
||||
int px = current % WIDTH;
|
||||
int py = current / WIDTH;
|
||||
if (px > 0) queue.add(current - 1);
|
||||
if (px + 1 < WIDTH) queue.add(current + 1);
|
||||
if (py > 0) queue.add(current - WIDTH);
|
||||
if (py + 1 < HEIGHT) queue.add(current + WIDTH);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public byte[] pack() {
|
||||
byte[] packed = new byte[PACKED_LENGTH];
|
||||
for (int i = 0; i < PIXEL_COUNT; i++) {
|
||||
if (pixels[i]) {
|
||||
packed[i / 8] |= (byte) (1 << (i % 8));
|
||||
}
|
||||
}
|
||||
return packed;
|
||||
}
|
||||
|
||||
public static AntPaintCanvas unpack(byte[] packed) {
|
||||
if (packed == null || packed.length != PACKED_LENGTH) {
|
||||
throw new IllegalArgumentException("Invalid AntPaint pixel data length");
|
||||
}
|
||||
boolean[] pixels = new boolean[PIXEL_COUNT];
|
||||
for (int i = 0; i < PIXEL_COUNT; i++) {
|
||||
pixels[i] = (packed[i / 8] & (1 << (i % 8))) != 0;
|
||||
}
|
||||
return new AntPaintCanvas(pixels);
|
||||
}
|
||||
|
||||
public AntPaintCanvas copy() {
|
||||
return new AntPaintCanvas(Arrays.copyOf(pixels, pixels.length));
|
||||
}
|
||||
|
||||
public boolean[] copyPixels() {
|
||||
return Arrays.copyOf(pixels, pixels.length);
|
||||
}
|
||||
|
||||
private boolean setIfChanged(int x, int y, boolean value) {
|
||||
int index = index(x, y);
|
||||
if (pixels[index] == value) {
|
||||
return false;
|
||||
}
|
||||
pixels[index] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int index(int x, int y) {
|
||||
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) {
|
||||
throw new IndexOutOfBoundsException("AntPaint pixel outside canvas");
|
||||
}
|
||||
return y * WIDTH + x;
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AntPaintFile(String fileId, String filename, AntPaintCanvas canvas, long createdTime, long modifiedTime) {
|
||||
public AntPaintFile {
|
||||
Objects.requireNonNull(fileId, "fileId");
|
||||
filename = AntPaintFilename.normalize(filename);
|
||||
Objects.requireNonNull(canvas, "canvas");
|
||||
if (fileId.isBlank()) {
|
||||
throw new IllegalArgumentException("fileId cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
public AntPaintFile copy() {
|
||||
return new AntPaintFile(fileId, filename, canvas.copy(), createdTime, modifiedTime);
|
||||
}
|
||||
|
||||
public AntPaintFile withCanvas(AntPaintCanvas value, long modified) {
|
||||
return new AntPaintFile(fileId, filename, value, createdTime, modified);
|
||||
}
|
||||
|
||||
public AntPaintFile withFilename(String value, long modified) {
|
||||
return new AntPaintFile(fileId, value, canvas, createdTime, modified);
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class AntPaintFileCodec {
|
||||
public static final int VERSION = 1;
|
||||
public static final int MAX_ENCODED_BYTES = 1024;
|
||||
private static final int MAGIC = 0x41505431;
|
||||
|
||||
private AntPaintFileCodec() {
|
||||
}
|
||||
|
||||
public static byte[] encode(AntPaintFile file) {
|
||||
try {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(MAX_ENCODED_BYTES);
|
||||
DataOutputStream data = new DataOutputStream(output);
|
||||
data.writeInt(MAGIC);
|
||||
data.writeByte(VERSION);
|
||||
writeString(data, file.fileId());
|
||||
writeString(data, file.filename());
|
||||
data.writeLong(file.createdTime());
|
||||
data.writeLong(file.modifiedTime());
|
||||
data.writeByte(AntPaintCanvas.WIDTH);
|
||||
data.writeByte(AntPaintCanvas.HEIGHT);
|
||||
data.write(file.canvas().pack());
|
||||
data.flush();
|
||||
byte[] encoded = output.toByteArray();
|
||||
if (encoded.length > MAX_ENCODED_BYTES) {
|
||||
throw new IllegalArgumentException("AntPaint file exceeds encoded size limit");
|
||||
}
|
||||
return encoded;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Unable to encode AntPaint file", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static AntPaintFile decode(byte[] encoded) {
|
||||
if (encoded == null || encoded.length > MAX_ENCODED_BYTES) {
|
||||
throw new IllegalArgumentException("Invalid AntPaint file size");
|
||||
}
|
||||
try {
|
||||
DataInputStream data = new DataInputStream(new ByteArrayInputStream(encoded));
|
||||
if (data.readInt() != MAGIC || data.readUnsignedByte() != VERSION) {
|
||||
throw new IllegalArgumentException("Unsupported AntPaint file format");
|
||||
}
|
||||
String fileId = readString(data, 128);
|
||||
String filename = readString(data, AntPaintFilename.MAX_LENGTH + AntPaintFilename.EXTENSION.length());
|
||||
long created = data.readLong();
|
||||
long modified = data.readLong();
|
||||
if (data.readUnsignedByte() != AntPaintCanvas.WIDTH || data.readUnsignedByte() != AntPaintCanvas.HEIGHT) {
|
||||
throw new IllegalArgumentException("Unsupported AntPaint canvas size");
|
||||
}
|
||||
byte[] pixels = data.readNBytes(AntPaintCanvas.PACKED_LENGTH);
|
||||
if (pixels.length != AntPaintCanvas.PACKED_LENGTH || data.available() != 0) {
|
||||
throw new IllegalArgumentException("Invalid AntPaint pixel payload");
|
||||
}
|
||||
return new AntPaintFile(fileId, filename, AntPaintCanvas.unpack(pixels), created, modified);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalArgumentException("Invalid AntPaint file data", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeString(DataOutputStream data, String value) throws IOException {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
if (bytes.length > 128) {
|
||||
throw new IllegalArgumentException("AntPaint string exceeds size limit");
|
||||
}
|
||||
data.writeShort(bytes.length);
|
||||
data.write(bytes);
|
||||
}
|
||||
|
||||
private static String readString(DataInputStream data, int maxBytes) throws IOException {
|
||||
int length = data.readUnsignedShort();
|
||||
if (length > maxBytes) {
|
||||
throw new IllegalArgumentException("AntPaint string exceeds size limit");
|
||||
}
|
||||
byte[] bytes = new byte[length];
|
||||
data.readFully(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class AntPaintFilename {
|
||||
public static final int MAX_LENGTH = 32;
|
||||
public static final String EXTENSION = ".antpaint";
|
||||
private static final Pattern SAFE_NAME = Pattern.compile("[A-Za-z0-9 _.-]+");
|
||||
|
||||
private AntPaintFilename() {
|
||||
}
|
||||
|
||||
public static boolean isValid(String filename) {
|
||||
if (filename == null || filename.isBlank() || filename.length() > MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
if (filename.equals(".") || filename.equals("..") || filename.contains("..")) {
|
||||
return false;
|
||||
}
|
||||
return SAFE_NAME.matcher(filename).matches();
|
||||
}
|
||||
|
||||
public static String normalize(String filename) {
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("Filename cannot be null");
|
||||
}
|
||||
String value = filename.trim();
|
||||
if (value.toLowerCase(Locale.ROOT).endsWith(EXTENSION)) {
|
||||
value = value.substring(0, value.length() - EXTENSION.length());
|
||||
}
|
||||
value = value.trim();
|
||||
if (!isValid(value)) {
|
||||
throw new IllegalArgumentException("Invalid AntPaint filename");
|
||||
}
|
||||
return value + EXTENSION;
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
public final class AntPaintHistory {
|
||||
public static final int MAX_STATES = 32;
|
||||
|
||||
private final ArrayDeque<AntPaintCanvas> undo = new ArrayDeque<>();
|
||||
private final ArrayDeque<AntPaintCanvas> redo = new ArrayDeque<>();
|
||||
|
||||
public void record(AntPaintCanvas canvas) {
|
||||
undo.addLast(canvas.copy());
|
||||
while (undo.size() > MAX_STATES) {
|
||||
undo.removeFirst();
|
||||
}
|
||||
redo.clear();
|
||||
}
|
||||
|
||||
public boolean canUndo() {
|
||||
return !undo.isEmpty();
|
||||
}
|
||||
|
||||
public boolean canRedo() {
|
||||
return !redo.isEmpty();
|
||||
}
|
||||
|
||||
public AntPaintCanvas undo(AntPaintCanvas current) {
|
||||
if (undo.isEmpty()) {
|
||||
return current;
|
||||
}
|
||||
redo.addLast(current.copy());
|
||||
return undo.removeLast().copy();
|
||||
}
|
||||
|
||||
public AntPaintCanvas redo(AntPaintCanvas current) {
|
||||
if (redo.isEmpty()) {
|
||||
return current;
|
||||
}
|
||||
undo.addLast(current.copy());
|
||||
return redo.removeLast().copy();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
undo.clear();
|
||||
redo.clear();
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AntPaintRequests {
|
||||
private AntPaintRequests() {
|
||||
}
|
||||
|
||||
public record Save(String computerId, AntPaintFile file) {
|
||||
public Save {
|
||||
Objects.requireNonNull(computerId, "computerId");
|
||||
Objects.requireNonNull(file, "file");
|
||||
}
|
||||
}
|
||||
|
||||
public record Load(String computerId, String fileId) {
|
||||
public Load {
|
||||
Objects.requireNonNull(computerId, "computerId");
|
||||
Objects.requireNonNull(fileId, "fileId");
|
||||
}
|
||||
}
|
||||
|
||||
public record List(String computerId) {
|
||||
public List {
|
||||
Objects.requireNonNull(computerId, "computerId");
|
||||
}
|
||||
}
|
||||
|
||||
public record Delete(String computerId, String fileId) {
|
||||
public Delete {
|
||||
Objects.requireNonNull(computerId, "computerId");
|
||||
Objects.requireNonNull(fileId, "fileId");
|
||||
}
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class AntPaintResults {
|
||||
private AntPaintResults() {
|
||||
}
|
||||
|
||||
public enum Code {
|
||||
SUCCESS,
|
||||
UNAUTHENTICATED,
|
||||
OUT_OF_RANGE,
|
||||
INVALID_FILENAME,
|
||||
INVALID_FILE,
|
||||
FILE_NOT_FOUND,
|
||||
DUPLICATE_FILE,
|
||||
STORAGE_FULL,
|
||||
PAYLOAD_TOO_LARGE,
|
||||
BUSY,
|
||||
UNKNOWN_ERROR
|
||||
}
|
||||
|
||||
public record Save(Code code, String fileId) {
|
||||
}
|
||||
|
||||
public record Load(Code code, AntPaintFile file) {
|
||||
}
|
||||
|
||||
public record List(Code code, java.util.List<AntPaintFileMetadata> files) {
|
||||
public List {
|
||||
files = files == null ? java.util.List.of() : java.util.List.copyOf(files);
|
||||
}
|
||||
}
|
||||
|
||||
public record Delete(Code code, String fileId) {
|
||||
}
|
||||
|
||||
public record AntPaintFileMetadata(String fileId, String filename, long modifiedTime) {
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.paint;
|
||||
|
||||
public enum AntPaintTool {
|
||||
PENCIL,
|
||||
ERASER,
|
||||
FILL
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.terminal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TerminalCommand(String name, List<String> arguments, String rawInput) {
|
||||
public TerminalCommand {
|
||||
name = name.toLowerCase(java.util.Locale.ROOT);
|
||||
arguments = List.copyOf(arguments);
|
||||
rawInput = rawInput == null ? "" : rawInput;
|
||||
}
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.terminal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class TerminalCommandParser {
|
||||
private static final int MAX_INPUT_LENGTH = 256;
|
||||
|
||||
private TerminalCommandParser() {
|
||||
}
|
||||
|
||||
public static ParseResult parse(String input) {
|
||||
if (input == null) {
|
||||
return ParseResult.error("ERROR: EMPTY INPUT");
|
||||
}
|
||||
if (input.length() > MAX_INPUT_LENGTH) {
|
||||
return ParseResult.error("ERROR: COMMAND TOO LONG");
|
||||
}
|
||||
String trimmed = input.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return ParseResult.error("ERROR: EMPTY INPUT");
|
||||
}
|
||||
List<String> tokens = new ArrayList<>();
|
||||
StringBuilder token = new StringBuilder();
|
||||
Character quote = null;
|
||||
boolean escaped = false;
|
||||
for (int index = 0; index < trimmed.length(); index++) {
|
||||
char character = trimmed.charAt(index);
|
||||
if (escaped) {
|
||||
token.append(character);
|
||||
escaped = false;
|
||||
} else if (character == '\\') {
|
||||
escaped = true;
|
||||
} else if (quote != null) {
|
||||
if (character == quote) {
|
||||
quote = null;
|
||||
} else {
|
||||
token.append(character);
|
||||
}
|
||||
} else if (character == '\'' || character == '"') {
|
||||
quote = character;
|
||||
} else if (Character.isWhitespace(character)) {
|
||||
if (!token.isEmpty()) {
|
||||
tokens.add(token.toString());
|
||||
token.setLength(0);
|
||||
}
|
||||
} else {
|
||||
token.append(character);
|
||||
}
|
||||
}
|
||||
if (escaped) {
|
||||
token.append('\\');
|
||||
}
|
||||
if (quote != null) {
|
||||
return ParseResult.error("ERROR: UNTERMINATED QUOTE");
|
||||
}
|
||||
if (!token.isEmpty()) {
|
||||
tokens.add(token.toString());
|
||||
}
|
||||
if (tokens.isEmpty()) {
|
||||
return ParseResult.error("ERROR: EMPTY INPUT");
|
||||
}
|
||||
return ParseResult.success(new TerminalCommand(tokens.get(0), tokens.subList(1, tokens.size()), input));
|
||||
}
|
||||
|
||||
public record ParseResult(TerminalCommand command, String error) {
|
||||
public boolean isSuccess() {
|
||||
return command != null;
|
||||
}
|
||||
|
||||
public static ParseResult success(TerminalCommand command) {
|
||||
return new ParseResult(command, null);
|
||||
}
|
||||
|
||||
public static ParseResult error(String message) {
|
||||
return new ParseResult(null, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.terminal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class TerminalCommandService {
|
||||
private static final int MAX_ARGUMENT_LENGTH = 128;
|
||||
private static final List<String> COMMANDS = List.of(
|
||||
"help", "clear", "pwd", "ls", "cd", "mkdir", "touch", "cat", "write", "rm", "rmdir", "mv", "open"
|
||||
);
|
||||
|
||||
public TerminalResult execute(TerminalFileSystem fileSystem, String workingDirectory, String input) {
|
||||
String currentDirectory = normalizeWorkingDirectory(fileSystem, workingDirectory);
|
||||
TerminalCommandParser.ParseResult parsed = TerminalCommandParser.parse(input);
|
||||
if (!parsed.isSuccess()) {
|
||||
return TerminalResult.error(currentDirectory, parsed.error());
|
||||
}
|
||||
TerminalCommand command = parsed.command();
|
||||
if (command.arguments().stream().anyMatch(argument -> argument.length() > MAX_ARGUMENT_LENGTH)) {
|
||||
return TerminalResult.error(currentDirectory, "ERROR: ARGUMENT TOO LONG");
|
||||
}
|
||||
return switch (command.name()) {
|
||||
case "help" -> help(currentDirectory, command);
|
||||
case "clear" -> requireArguments(currentDirectory, command, 0, TerminalResult.clear(currentDirectory));
|
||||
case "pwd" -> requireArguments(currentDirectory, command, 0, TerminalResult.success(currentDirectory, currentDirectory));
|
||||
case "ls" -> list(fileSystem, currentDirectory, command);
|
||||
case "cd" -> changeDirectory(fileSystem, currentDirectory, command);
|
||||
case "mkdir" -> createDirectory(fileSystem, currentDirectory, command);
|
||||
case "touch" -> createFile(fileSystem, currentDirectory, command);
|
||||
case "cat" -> readFile(fileSystem, currentDirectory, command);
|
||||
case "write" -> writeFile(fileSystem, currentDirectory, command);
|
||||
case "rm" -> removeFile(fileSystem, currentDirectory, command);
|
||||
case "rmdir" -> removeDirectory(fileSystem, currentDirectory, command);
|
||||
case "mv" -> move(fileSystem, currentDirectory, command);
|
||||
case "open" -> open(fileSystem, currentDirectory, command);
|
||||
default -> TerminalResult.error(currentDirectory, "ERROR: INVALID COMMAND: " + command.name());
|
||||
};
|
||||
}
|
||||
|
||||
private TerminalResult help(String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().isEmpty()) {
|
||||
return TerminalResult.success(workingDirectory,
|
||||
"AVAILABLE COMMANDS",
|
||||
"HELP CLEAR PWD LS CD MKDIR TOUCH CAT WRITE RM RMDIR MV OPEN");
|
||||
}
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "help [command]");
|
||||
}
|
||||
return switch (command.arguments().get(0).toLowerCase(Locale.ROOT)) {
|
||||
case "help" -> TerminalResult.success(workingDirectory, "HELP [COMMAND] // SHOW COMMAND HELP");
|
||||
case "clear" -> TerminalResult.success(workingDirectory, "CLEAR // CLEAR TERMINAL OUTPUT");
|
||||
case "pwd" -> TerminalResult.success(workingDirectory, "PWD // PRINT WORKING DIRECTORY");
|
||||
case "ls" -> TerminalResult.success(workingDirectory, "LS [PATH] // LIST DIRECTORY CONTENTS");
|
||||
case "cd" -> TerminalResult.success(workingDirectory, "CD [PATH] // CHANGE WORKING DIRECTORY");
|
||||
case "mkdir" -> TerminalResult.success(workingDirectory, "MKDIR <PATH> // CREATE DIRECTORY");
|
||||
case "touch" -> TerminalResult.success(workingDirectory, "TOUCH <PATH> // CREATE EMPTY TEXT FILE");
|
||||
case "cat" -> TerminalResult.success(workingDirectory, "CAT <PATH> // PRINT TEXT FILE");
|
||||
case "write" -> TerminalResult.success(workingDirectory, "WRITE <PATH> [TEXT] // REPLACE TEXT FILE CONTENTS");
|
||||
case "rm" -> TerminalResult.success(workingDirectory, "RM <PATH> // DELETE TEXT FILE");
|
||||
case "rmdir" -> TerminalResult.success(workingDirectory, "RMDIR <PATH> // DELETE EMPTY DIRECTORY");
|
||||
case "mv" -> TerminalResult.success(workingDirectory, "MV <SOURCE> <DESTINATION> // MOVE OR RENAME");
|
||||
case "open" -> TerminalResult.success(workingDirectory, "OPEN <PATH> // OPEN TEXT FILE IN ANTTEXT");
|
||||
default -> TerminalResult.error(workingDirectory, "ERROR: NO HELP FOR " + command.arguments().get(0));
|
||||
};
|
||||
}
|
||||
|
||||
private TerminalResult list(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() > 1) {
|
||||
return usage(workingDirectory, "ls [path]");
|
||||
}
|
||||
String path = command.arguments().isEmpty() ? workingDirectory : command.arguments().get(0);
|
||||
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, path, workingDirectory);
|
||||
if (entry.isEmpty()) {
|
||||
return missing(workingDirectory, path);
|
||||
}
|
||||
if (!entry.get().isDirectory()) {
|
||||
return TerminalResult.success(workingDirectory, entry.get().name());
|
||||
}
|
||||
List<TerminalFileSystem.Entry> entries = new ArrayList<>(fileSystem.list(path, workingDirectory));
|
||||
entries.sort(Comparator.comparing(TerminalFileSystem.Entry::isDirectory).reversed().thenComparing(TerminalFileSystem.Entry::name));
|
||||
if (entries.isEmpty()) {
|
||||
return TerminalResult.success(workingDirectory, "DIRECTORY EMPTY");
|
||||
}
|
||||
return new TerminalResult(TerminalResult.Status.SUCCESS, entries.stream()
|
||||
.map(value -> (value.isDirectory() ? "[DIR] " : " ") + value.name()).toList(), workingDirectory, false, null);
|
||||
}
|
||||
|
||||
private TerminalResult changeDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() > 1) {
|
||||
return usage(workingDirectory, "cd [path]");
|
||||
}
|
||||
String path = command.arguments().isEmpty() ? TerminalFileSystem.ROOT_PATH : command.arguments().get(0);
|
||||
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, path, workingDirectory);
|
||||
if (entry.isEmpty() || !entry.get().isDirectory()) {
|
||||
return missing(workingDirectory, path);
|
||||
}
|
||||
return TerminalResult.success(entry.get().path(), entry.get().path());
|
||||
}
|
||||
|
||||
private TerminalResult createDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "mkdir <path>");
|
||||
}
|
||||
return fileSystem.createDirectory(command.arguments().get(0), workingDirectory)
|
||||
? TerminalResult.success(workingDirectory)
|
||||
: TerminalResult.error(workingDirectory, "ERROR: CANNOT CREATE DIRECTORY");
|
||||
}
|
||||
|
||||
private TerminalResult createFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "touch <path>");
|
||||
}
|
||||
return fileSystem.createFile(command.arguments().get(0), workingDirectory)
|
||||
? TerminalResult.success(workingDirectory)
|
||||
: TerminalResult.error(workingDirectory, "ERROR: CANNOT CREATE FILE");
|
||||
}
|
||||
|
||||
private TerminalResult readFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "cat <path>");
|
||||
}
|
||||
Optional<String> contents = fileSystem.readText(command.arguments().get(0), workingDirectory);
|
||||
return contents.map(value -> TerminalResult.success(workingDirectory, value.split("\\R", -1)))
|
||||
.orElseGet(() -> missing(workingDirectory, command.arguments().get(0)));
|
||||
}
|
||||
|
||||
private TerminalResult writeFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() < 1 || command.arguments().size() > 2) {
|
||||
return usage(workingDirectory, "write <path> [text]");
|
||||
}
|
||||
String contents = command.arguments().size() == 2 ? command.arguments().get(1) : "";
|
||||
return fileSystem.writeText(command.arguments().get(0), workingDirectory, contents)
|
||||
? TerminalResult.success(workingDirectory)
|
||||
: TerminalResult.error(workingDirectory, "ERROR: CANNOT WRITE FILE");
|
||||
}
|
||||
|
||||
private TerminalResult removeFile(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "rm <path>");
|
||||
}
|
||||
return fileSystem.deleteFile(command.arguments().get(0), workingDirectory)
|
||||
? TerminalResult.success(workingDirectory)
|
||||
: TerminalResult.error(workingDirectory, "ERROR: CANNOT DELETE FILE");
|
||||
}
|
||||
|
||||
private TerminalResult removeDirectory(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "rmdir <path>");
|
||||
}
|
||||
return fileSystem.deleteEmptyDirectory(command.arguments().get(0), workingDirectory)
|
||||
? TerminalResult.success(workingDirectory)
|
||||
: TerminalResult.error(workingDirectory, "ERROR: DIRECTORY NOT EMPTY OR NOT FOUND");
|
||||
}
|
||||
|
||||
private TerminalResult move(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 2) {
|
||||
return usage(workingDirectory, "mv <source> <destination>");
|
||||
}
|
||||
return fileSystem.move(command.arguments().get(0), command.arguments().get(1), workingDirectory)
|
||||
? TerminalResult.success(workingDirectory)
|
||||
: TerminalResult.error(workingDirectory, "ERROR: CANNOT MOVE PATH");
|
||||
}
|
||||
|
||||
private TerminalResult open(TerminalFileSystem fileSystem, String workingDirectory, TerminalCommand command) {
|
||||
if (command.arguments().size() != 1) {
|
||||
return usage(workingDirectory, "open <path>");
|
||||
}
|
||||
Optional<TerminalFileSystem.Entry> entry = find(fileSystem, command.arguments().get(0), workingDirectory);
|
||||
if (entry.isEmpty() || entry.get().isDirectory()) {
|
||||
return missing(workingDirectory, command.arguments().get(0));
|
||||
}
|
||||
return TerminalResult.open(workingDirectory, entry.get().path());
|
||||
}
|
||||
|
||||
private Optional<TerminalFileSystem.Entry> find(TerminalFileSystem fileSystem, String path, String workingDirectory) {
|
||||
try {
|
||||
return fileSystem.find(path, workingDirectory);
|
||||
} catch (RuntimeException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeWorkingDirectory(TerminalFileSystem fileSystem, String workingDirectory) {
|
||||
try {
|
||||
String normalized = fileSystem.normalize(workingDirectory, TerminalFileSystem.ROOT_PATH);
|
||||
Optional<TerminalFileSystem.Entry> entry = fileSystem.find(normalized, TerminalFileSystem.ROOT_PATH);
|
||||
return entry.filter(TerminalFileSystem.Entry::isDirectory).map(TerminalFileSystem.Entry::path).orElse(TerminalFileSystem.ROOT_PATH);
|
||||
} catch (RuntimeException ignored) {
|
||||
return TerminalFileSystem.ROOT_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalResult missing(String workingDirectory, String path) {
|
||||
return TerminalResult.error(workingDirectory, "ERROR: PATH NOT FOUND: " + path);
|
||||
}
|
||||
|
||||
private TerminalResult usage(String workingDirectory, String usage) {
|
||||
return TerminalResult.error(workingDirectory, "USAGE: " + usage);
|
||||
}
|
||||
|
||||
private TerminalResult requireArguments(String workingDirectory, TerminalCommand command, int count, TerminalResult result) {
|
||||
return command.arguments().size() == count ? result : usage(workingDirectory, command.name() + " requires " + count + " arguments");
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.terminal;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TerminalFileSystem {
|
||||
String ROOT_PATH = "/";
|
||||
|
||||
Optional<Entry> find(String path, String workingDirectory);
|
||||
|
||||
List<Entry> list(String path, String workingDirectory);
|
||||
|
||||
boolean createDirectory(String path, String workingDirectory);
|
||||
|
||||
boolean createFile(String path, String workingDirectory);
|
||||
|
||||
Optional<String> readText(String path, String workingDirectory);
|
||||
|
||||
boolean writeText(String path, String workingDirectory, String contents);
|
||||
|
||||
boolean deleteFile(String path, String workingDirectory);
|
||||
|
||||
boolean deleteEmptyDirectory(String path, String workingDirectory);
|
||||
|
||||
boolean move(String source, String destination, String workingDirectory);
|
||||
|
||||
String normalize(String path, String workingDirectory);
|
||||
|
||||
record Entry(String name, String path, EntryType type) {
|
||||
public boolean isDirectory() {
|
||||
return type == EntryType.DIRECTORY;
|
||||
}
|
||||
}
|
||||
|
||||
enum EntryType {
|
||||
FILE,
|
||||
DIRECTORY
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.computer.terminal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TerminalResult(Status status, List<String> lines, String workingDirectory, boolean clearOutput, String openPath) {
|
||||
public TerminalResult {
|
||||
status = status == null ? Status.ERROR : status;
|
||||
lines = List.copyOf(lines);
|
||||
workingDirectory = workingDirectory == null || workingDirectory.isBlank() ? TerminalFileSystem.ROOT_PATH : workingDirectory;
|
||||
}
|
||||
|
||||
public static TerminalResult success(String workingDirectory, String... lines) {
|
||||
return new TerminalResult(Status.SUCCESS, List.of(lines), workingDirectory, false, null);
|
||||
}
|
||||
|
||||
public static TerminalResult error(String workingDirectory, String... lines) {
|
||||
return new TerminalResult(Status.ERROR, List.of(lines), workingDirectory, false, null);
|
||||
}
|
||||
|
||||
public static TerminalResult clear(String workingDirectory) {
|
||||
return new TerminalResult(Status.SUCCESS, List.of(), workingDirectory, true, null);
|
||||
}
|
||||
|
||||
public static TerminalResult open(String workingDirectory, String path) {
|
||||
return new TerminalResult(Status.OPEN, List.of("OPENING " + path), workingDirectory, false, path);
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
OPEN
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -38,6 +38,8 @@ public final class CreativeTabContents {
|
||||
}
|
||||
|
||||
private static final List<CatalogEntry> CATALOG = List.of(
|
||||
entry("bug_spray", "tools"),
|
||||
entry("ant_trap", "building_blocks"),
|
||||
entry("ouranwood_log", "building_blocks"),
|
||||
entry("ouranwood_wood", "building_blocks"),
|
||||
entry("mossy_ouranwood_log", "building_blocks"),
|
||||
@@ -395,8 +397,6 @@ public final class CreativeTabContents {
|
||||
entry("lucid_anchor", "tools_and_utilities"),
|
||||
entry("undertrial_spawner", "tools_and_utilities"),
|
||||
entry("undervault", "tools_and_utilities"),
|
||||
entry("computer", "functional_blocks"),
|
||||
entry("floppy_disk", "tools_and_utilities"),
|
||||
entry("undertrial_key", "tools_and_utilities"),
|
||||
entry("reverie_bottle", "tools_and_utilities"),
|
||||
entry("glimmer_bottle", "tools_and_utilities"),
|
||||
@@ -641,8 +641,8 @@ public final class CreativeTabContents {
|
||||
}
|
||||
for (String diskId : List.of("antarchy:introduction", "antarchy:chronosphere", "antarchy:toreterror_arena", "antarchy:high_fructose_corn_syrup",
|
||||
"antarchy:elythia", "antarchy:flying_squirrel", "antarchy:cavaryn", "antarchy:thoraxis", "antarchy:temporal_fields", "antarchy:bloodglass", "antarchy:vortex")) {
|
||||
ItemStack disk = new ItemStack(AntarchyObjects.FLOPPY_DISK.get());
|
||||
disk.set(AntarchyObjects.FLOPPY_DISK_COMPONENT.get(), ResourceLocation.parse(diskId));
|
||||
ItemStack disk = new ItemStack(com.craisinlord.antos.content.AntOSObjects.FLOPPY_DISK.get());
|
||||
disk.set(com.craisinlord.antos.content.AntOSObjects.FLOPPY_DISK_COMPONENT.get(), ResourceLocation.parse(diskId));
|
||||
output.accept(disk);
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -6,6 +6,7 @@ import com.craisinlord.antarchy.content.gravity.AntarchyGravityTransition;
|
||||
import net.minecraft.world.effect.MobEffect;
|
||||
import net.minecraft.world.effect.MobEffectCategory;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
|
||||
public final class InvertedMobEffect extends MobEffect {
|
||||
private static final AntarchyGravityTransition EFFECT_TRANSITION = new AntarchyGravityTransition(12);
|
||||
@@ -17,6 +18,14 @@ public final class InvertedMobEffect extends MobEffect {
|
||||
|
||||
@Override
|
||||
public boolean applyEffectTick(LivingEntity entity, int amplifier) {
|
||||
// Vex flight steering is authored around vanilla gravity and ground contact. Rotating its
|
||||
// gravity frame makes the native flight controller drive it into terrain.
|
||||
if (entity.getType() == EntityType.VEX) {
|
||||
if (!entity.level().isClientSide()) {
|
||||
AntarchyGravityApi.clearForcedGravity(entity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!entity.level().isClientSide()) {
|
||||
if (AntarchyGravityApi.getGravityDirection(entity) != AntarchyGravityDirection.UP || !AntarchyGravityApi.isGravityForced(entity)) {
|
||||
AntarchyGravityApi.setForcedGravityDirection(entity, AntarchyGravityDirection.UP, EFFECT_TRANSITION);
|
||||
|
||||
+18
@@ -2,6 +2,7 @@ package com.craisinlord.antarchy.content.entity;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import com.craisinlord.antarchy.config.AntarchySettings;
|
||||
import com.craisinlord.antarchy.content.AntarchySoundEvents;
|
||||
import com.craisinlord.antarchy.content.gravity.AntarchyGravityApi;
|
||||
import com.craisinlord.antarchy.content.gravity.AntarchyGravityDirection;
|
||||
import com.craisinlord.antarchy.content.gravity.AntarchyGravityRotationUtil;
|
||||
@@ -67,6 +68,14 @@ public class HoverboardEntity extends PathfinderMob implements GeoEntity {
|
||||
this.setPersistenceRequired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (!this.level().isClientSide && this.isVehicle() && this.tickCount % 100 == 0) {
|
||||
this.playSound(AntarchySoundEvents.HOVERBOARD_IDLE.get(), 0.55F, 1.0F);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBeAffected(MobEffectInstance effectInstance) {
|
||||
return false;
|
||||
@@ -139,12 +148,21 @@ public class HoverboardEntity extends PathfinderMob implements GeoEntity {
|
||||
if (!this.isVehicle() && !player.isSecondaryUseActive()) {
|
||||
if (!this.level().isClientSide) {
|
||||
player.startRiding(this);
|
||||
this.playSound(AntarchySoundEvents.HOVERBOARD_MOUNT.get(), 1.0F, 1.0F);
|
||||
}
|
||||
return InteractionResult.sidedSuccess(this.level().isClientSide);
|
||||
}
|
||||
return super.mobInteract(player, hand);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removePassenger(Entity passenger) {
|
||||
super.removePassenger(passenger);
|
||||
if (!this.level().isClientSide) {
|
||||
this.playSound(AntarchySoundEvents.HOVERBOARD_DISMOUNT.get(), 1.0F, 1.0F);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public LivingEntity getControllingPassenger() {
|
||||
|
||||
+56
-2
@@ -16,6 +16,7 @@ import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.GameRules;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
@@ -32,6 +33,8 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
|
||||
private static final int WOOD_REPATH_INTERVAL_TICKS = 6;
|
||||
private static final int WOOD_SEARCH_RADIUS_HORIZONTAL = 12;
|
||||
private static final int WOOD_SEARCH_RADIUS_VERTICAL = 4;
|
||||
private static final int LIGHT_FLEE_SEARCH_RADIUS = 6;
|
||||
private static final int LIGHT_FLEE_SEARCH_INTERVAL_TICKS = 10;
|
||||
private static final Vec3i[] IMMEDIATE_WOOD_OFFSETS = {
|
||||
new Vec3i(0, 1, 0),
|
||||
new Vec3i(0, 0, -1),
|
||||
@@ -48,6 +51,7 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
|
||||
private int nextWoodBiteTick;
|
||||
private int nextWoodSearchTick;
|
||||
private int nextWoodRepathTick;
|
||||
private int nextLightFleeSearchTick;
|
||||
|
||||
public TermiteEntity(EntityType<? extends BaseAntEntity> entityType, Level level) {
|
||||
super(entityType, level);
|
||||
@@ -113,6 +117,12 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
|
||||
|
||||
@Override
|
||||
protected boolean handlePriorityForaging() {
|
||||
if (this.isBlockLit(this.blockPosition())) {
|
||||
this.targetWoodPos = null;
|
||||
this.fleeBlockLight();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)
|
||||
|| !this.level().getGameRules().getBoolean(AntarchyGameRules.RULE_DO_TERMITE_GREIFING)) {
|
||||
this.targetWoodPos = null;
|
||||
@@ -142,6 +152,45 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void fleeBlockLight() {
|
||||
if (this.tickCount < this.nextLightFleeSearchTick && !this.getNavigation().isDone()) {
|
||||
return;
|
||||
}
|
||||
this.nextLightFleeSearchTick = this.tickCount + LIGHT_FLEE_SEARCH_INTERVAL_TICKS;
|
||||
|
||||
BlockPos origin = this.blockPosition();
|
||||
BlockPos bestPos = null;
|
||||
int bestLight = this.level().getBrightness(LightLayer.BLOCK, origin);
|
||||
double bestDistance = -1.0D;
|
||||
BlockPos.MutableBlockPos candidate = new BlockPos.MutableBlockPos();
|
||||
for (int dx = -LIGHT_FLEE_SEARCH_RADIUS; dx <= LIGHT_FLEE_SEARCH_RADIUS; dx++) {
|
||||
for (int dz = -LIGHT_FLEE_SEARCH_RADIUS; dz <= LIGHT_FLEE_SEARCH_RADIUS; dz++) {
|
||||
if (dx * dx + dz * dz > LIGHT_FLEE_SEARCH_RADIUS * LIGHT_FLEE_SEARCH_RADIUS) continue;
|
||||
for (int dy = -2; dy <= 2; dy++) {
|
||||
candidate.set(origin.getX() + dx, origin.getY() + dy, origin.getZ() + dz);
|
||||
if (!this.isWalkableStandPos(candidate)) continue;
|
||||
int light = this.level().getBrightness(LightLayer.BLOCK, candidate);
|
||||
double distance = candidate.distSqr(origin);
|
||||
if (light > bestLight || (light == bestLight && distance <= bestDistance)) continue;
|
||||
bestPos = candidate.immutable();
|
||||
bestLight = light;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPos == null) {
|
||||
this.getNavigation().stop();
|
||||
return;
|
||||
}
|
||||
Path path = this.getNavigation().createPath(bestPos, 0);
|
||||
if (path == null || !path.canReach()) {
|
||||
this.getNavigation().stop();
|
||||
return;
|
||||
}
|
||||
this.getNavigation().moveTo(bestPos.getX() + 0.5D, bestPos.getY(), bestPos.getZ() + 0.5D, 1.2D);
|
||||
}
|
||||
|
||||
private boolean tryEatNearbyWood() {
|
||||
if (this.tickCount < this.nextWoodBiteTick) {
|
||||
return false;
|
||||
@@ -151,7 +200,7 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
|
||||
for (Vec3i offset : IMMEDIATE_WOOD_OFFSETS) {
|
||||
BlockPos targetPos = basePos.offset(offset);
|
||||
BlockState targetState = this.level().getBlockState(targetPos);
|
||||
if (!this.isTermiteEdible(targetState)) {
|
||||
if (this.isBlockLit(targetPos) || !this.isTermiteEdible(targetState)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -244,7 +293,12 @@ public class TermiteEntity extends BaseAntEntity implements GeoEntity {
|
||||
}
|
||||
|
||||
private boolean isValidWoodTarget(BlockPos targetPos) {
|
||||
return this.isTermiteEdible(this.level().getBlockState(targetPos));
|
||||
return !this.isBlockLit(targetPos) && this.isTermiteEdible(this.level().getBlockState(targetPos));
|
||||
}
|
||||
|
||||
private boolean isBlockLit(BlockPos pos) {
|
||||
return AntarchySettings.termitesFearBlockLight()
|
||||
&& this.level().getBrightness(LightLayer.BLOCK, pos) > 0;
|
||||
}
|
||||
|
||||
private boolean isTermiteEdible(BlockState state) {
|
||||
|
||||
+35
-5
@@ -14,6 +14,8 @@ import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.network.protocol.game.ClientboundClearTitlesPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket;
|
||||
@@ -252,7 +254,7 @@ public class KingEntity extends RoyalBossEntity {
|
||||
return true;
|
||||
}
|
||||
LivingEntity target = this.getTarget();
|
||||
if (target == null || this.activeDecree != null || this.royalChargeOpeningDelayTicks > 0
|
||||
if (target == null || this.royalChargeOpeningDelayTicks > 0
|
||||
|| this.kingLandingStage != KingLandingStage.AERIAL
|
||||
|| !this.attackScheduler.ready("royal_charge", RoyalAttackLane.MOVEMENT)) {
|
||||
return false;
|
||||
@@ -543,6 +545,9 @@ public class KingEntity extends RoyalBossEntity {
|
||||
return;
|
||||
}
|
||||
this.tickRoyalBoundary((ServerLevel) this.level());
|
||||
if (this.tickCount % 10 == 0 && this.isRoyalFlying() && this.getDeltaMovement().horizontalDistanceSqr() > 0.01D) {
|
||||
this.clearLeavesAhead((ServerLevel) this.level());
|
||||
}
|
||||
this.tickComeNoCloserIndicator();
|
||||
this.tickRoyalPunishment();
|
||||
this.tickPendingExileAnimation();
|
||||
@@ -762,6 +767,28 @@ public class KingEntity extends RoyalBossEntity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clearLeavesAhead(ServerLevel level) {
|
||||
if (!level.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_MOBGRIEFING)) {
|
||||
return;
|
||||
}
|
||||
Vec3 movement = this.getDeltaMovement().normalize().scale(1.5D);
|
||||
AABB forward = this.getBoundingBox().move(movement).inflate(0.2D, 0.0D, 0.2D);
|
||||
BlockPos min = BlockPos.containing(forward.minX, forward.minY, forward.minZ);
|
||||
BlockPos max = BlockPos.containing(forward.maxX, forward.maxY, forward.maxZ);
|
||||
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
|
||||
for (int y = min.getY(); y <= max.getY(); y++) {
|
||||
for (int x = min.getX(); x <= max.getX(); x++) {
|
||||
for (int z = min.getZ(); z <= max.getZ(); z++) {
|
||||
cursor.set(x, y, z);
|
||||
if (level.getBlockState(cursor).is(BlockTags.LEAVES)) {
|
||||
level.destroyBlock(cursor, true, this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickKingCombatMovement(LivingEntity target) {
|
||||
if (this.royalChargeActive) {
|
||||
this.getNavigation().stop();
|
||||
@@ -1102,7 +1129,9 @@ public class KingEntity extends RoyalBossEntity {
|
||||
|
||||
public RoyalDecree.Evaluation evaluateCloseQuarters(LivingEntity target) {
|
||||
UUID targetId = target.getUUID();
|
||||
double distanceSquared = target.distanceToSqr(this);
|
||||
double dx = target.getX() - this.getX();
|
||||
double dz = target.getZ() - this.getZ();
|
||||
double distanceSquared = dx * dx + dz * dz;
|
||||
if (distanceSquared <= CLOSE_QUARTERS_RADIUS * CLOSE_QUARTERS_RADIUS) {
|
||||
this.closeQuartersEntered.add(targetId);
|
||||
this.closeQuartersOutsideTicks.remove(targetId);
|
||||
@@ -1719,8 +1748,8 @@ public class KingEntity extends RoyalBossEntity {
|
||||
}
|
||||
@Override public void onActive(int elapsedTicks) {
|
||||
Vec3 origin = KingEntity.this.headAnchor(KingEntity.this.royalHead(RoyalHead.Slot.LEFT));
|
||||
Vec3 predicted = target.position().add(target.getDeltaMovement().scale(10.0D));
|
||||
Vec3 direction = predicted.add(0.0D, target.getBbHeight() * 0.5D, 0.0D).subtract(origin).normalize();
|
||||
Vec3 direction = RoyalElementalProjectileEntity.aimDirection(
|
||||
origin, target, RoyalBeamElement.FIRE);
|
||||
RoyalElementalProjectileEntity fireball = RoyalElementalProjectileEntity.create(level, KingEntity.this,
|
||||
RoyalBeamElement.FIRE, origin, direction);
|
||||
level.addFreshEntity(fireball);
|
||||
@@ -1739,7 +1768,8 @@ public class KingEntity extends RoyalBossEntity {
|
||||
}
|
||||
@Override public void onActive(int elapsedTicks) {
|
||||
Vec3 origin = KingEntity.this.headAnchor(KingEntity.this.royalHead(RoyalHead.Slot.RIGHT));
|
||||
Vec3 direction = target.getEyePosition().subtract(origin).normalize();
|
||||
Vec3 direction = RoyalElementalProjectileEntity.aimDirection(
|
||||
origin, target, RoyalBeamElement.ICE);
|
||||
RoyalElementalProjectileEntity iceball = RoyalElementalProjectileEntity.create(level, KingEntity.this,
|
||||
RoyalBeamElement.ICE, origin, direction);
|
||||
level.addFreshEntity(iceball);
|
||||
|
||||
+54
-7
@@ -10,6 +10,8 @@ import com.craisinlord.antarchy.content.gravity.AntarchyGravityApi;
|
||||
import com.craisinlord.antarchy.content.gravity.AntarchyGravityDirection;
|
||||
import com.craisinlord.antarchy.content.gravity.AntarchyGravityRotationUtil;
|
||||
import com.craisinlord.antarchy.content.gravity.AntarchyGravityTransition;
|
||||
import com.craisinlord.antarchy.content.time.TimeDilationApi;
|
||||
import com.craisinlord.antarchy.content.time.TimeDilationFieldEntity;
|
||||
import com.craisinlord.antarchy.content.worldgen.thoraxis.ThoraxisUndersideManager;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
@@ -73,6 +75,9 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
private static final double CRUSHING_GRAVITY_RADIUS = 18.0D;
|
||||
private static final double CRUSHING_GRAVITY_STRENGTH = 0.12D;
|
||||
private static final int ACCELERATION_COOLDOWN = 500;
|
||||
private static final int QUEEN_CHRONOSPHERE_COOLDOWN = 700;
|
||||
private static final int QUEEN_CHRONOSPHERE_DURATION = 200;
|
||||
private static final double QUEEN_CHRONOSPHERE_RADIUS = 20.0D;
|
||||
private static final int ACCELERATION_DURATION = 140;
|
||||
private static final int BLACK_HOLE_COOLDOWN = 340;
|
||||
private static final double QUEEN_BITE_COOLDOWN_MULTIPLIER = 1.5D;
|
||||
@@ -83,10 +88,10 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
private static final int ACCELERATION_TELEGRAPH_TICKS = 32;
|
||||
private static final int LANDING_TELEGRAPH_TICKS = 30;
|
||||
private static final int LANDING_IMPACT_TICKS = 18;
|
||||
private static final int GROUND_COMBAT_TICKS = 140;
|
||||
private static final int GROUND_COMBAT_TICKS = 160;
|
||||
private static final int TAKEOFF_TICKS = 30;
|
||||
private static final int LANDING_COOLDOWN_MIN = 360;
|
||||
private static final int LANDING_COOLDOWN_VARIANCE = 240;
|
||||
private static final int LANDING_COOLDOWN_MIN = 300;
|
||||
private static final int LANDING_COOLDOWN_VARIANCE = 120;
|
||||
private static final int BLACK_HOLE_FOLLOW_UP_DELAY = 22;
|
||||
private static final int CRUSHING_LANDING_DELAY = 40;
|
||||
private static final int MOMENTUM_VOLLEY_DELAY = MOMENTUM_LOCK_DURATION + 5;
|
||||
@@ -99,12 +104,14 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
private int momentumLockCooldownTicks;
|
||||
private int crushingGravityCooldownTicks;
|
||||
private int accelerationCooldownTicks;
|
||||
private int queenChronosphereCooldownTicks;
|
||||
private int blackHoleCooldownTicks;
|
||||
private int gravityStompTelegraphTicks;
|
||||
private int blackHoleTelegraphTicks;
|
||||
private int momentumLockTelegraphTicks;
|
||||
private int crushingGravityTelegraphTicks;
|
||||
private int accelerationTelegraphTicks;
|
||||
private int queenChronosphereTelegraphTicks;
|
||||
@Nullable
|
||||
private Vec3 pendingBlackHoleAnchor;
|
||||
private int queenLandingCooldownTicks = 300;
|
||||
@@ -238,6 +245,11 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
return 1.55D;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double combatStandoffDistance() {
|
||||
return Math.min(super.combatStandoffDistance(), 14.0D);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int maxConcurrentHeadAttacks(Phase phase) {
|
||||
return switch (phase) {
|
||||
@@ -851,6 +863,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
if (this.momentumLockCooldownTicks > 0) this.momentumLockCooldownTicks--;
|
||||
if (this.crushingGravityCooldownTicks > 0) this.crushingGravityCooldownTicks--;
|
||||
if (this.accelerationCooldownTicks > 0) this.accelerationCooldownTicks--;
|
||||
if (this.queenChronosphereCooldownTicks > 0) this.queenChronosphereCooldownTicks--;
|
||||
if (this.blackHoleCooldownTicks > 0) this.blackHoleCooldownTicks--;
|
||||
|
||||
if (!(this.level() instanceof ServerLevel level)) {
|
||||
@@ -871,7 +884,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
String[] candidates = switch (phase) {
|
||||
case ONE -> new String[] {"gravity_stomp", "black_hole"};
|
||||
case TWO -> new String[] {"gravity_stomp", "black_hole", "momentum_lock", "crushing_gravity"};
|
||||
case THREE -> new String[] {"gravity_stomp", "black_hole", "momentum_lock", "crushing_gravity", "acceleration"};
|
||||
case THREE -> new String[] {"gravity_stomp", "black_hole", "momentum_lock", "crushing_gravity", "acceleration", "queen_chronosphere"};
|
||||
};
|
||||
String forcedAttack = this.pendingPhaseSignature == Phase.TWO ? "momentum_lock"
|
||||
: this.pendingPhaseSignature == Phase.THREE ? "acceleration" : null;
|
||||
@@ -888,12 +901,14 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
case "black_hole" -> BLACK_HOLE_COOLDOWN;
|
||||
case "momentum_lock" -> MOMENTUM_LOCK_COOLDOWN;
|
||||
case "crushing_gravity" -> CRUSHING_GRAVITY_COOLDOWN;
|
||||
case "queen_chronosphere" -> QUEEN_CHRONOSPHERE_COOLDOWN;
|
||||
default -> ACCELERATION_COOLDOWN;
|
||||
};
|
||||
RoyalAttackLane lane = "black_hole".equals(selected) ? RoyalAttackLane.CENTER_HEAD : RoyalAttackLane.HAZARD;
|
||||
int actionWindup = this.queenAbilityWindup(selected);
|
||||
int actionAnimationTicks = "momentum_lock".equals(selected)
|
||||
|| "crushing_gravity".equals(selected) || "acceleration".equals(selected) ? 55 : 25;
|
||||
|| "crushing_gravity".equals(selected) || "acceleration".equals(selected)
|
||||
|| "queen_chronosphere".equals(selected) ? 55 : 25;
|
||||
int actionRecovery = this.animationRecovery(actionAnimationTicks, actionWindup, 1, 14);
|
||||
if (!this.beginRoyalAttack(selected, lane, this.cooldown(cooldown),
|
||||
actionWindup, 1, actionRecovery, new com.craisinlord.antarchy.content.entity.royal.attack.RoyalAttackScheduler.Action() {
|
||||
@@ -902,7 +917,8 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
QueenEntity.this.triggerAnim("body_action", "stomp");
|
||||
} else if ("momentum_lock".equals(selected)
|
||||
|| "crushing_gravity".equals(selected)
|
||||
|| "acceleration".equals(selected)) {
|
||||
|| "acceleration".equals(selected)
|
||||
|| "queen_chronosphere".equals(selected)) {
|
||||
QueenEntity.this.triggerAnim("wing_action", "wing_gust");
|
||||
} else if ("black_hole".equals(selected)) {
|
||||
QueenEntity.this.royalHead(RoyalHead.Slot.CENTER).startShoot();
|
||||
@@ -965,6 +981,13 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
this.startRoyalAcceleration();
|
||||
this.queueQueenFollowUp(QueenFollowUp.ACCELERATION_VOLLEY, ACCELERATION_VOLLEY_DELAY);
|
||||
}
|
||||
case "queen_chronosphere" -> {
|
||||
this.queenChronosphereCooldownTicks = cooldown;
|
||||
TimeDilationFieldEntity field = TimeDilationApi.createField(level, this.position(),
|
||||
QUEEN_CHRONOSPHERE_RADIUS, 0.08D, QUEEN_CHRONOSPHERE_DURATION, this.getUUID());
|
||||
field.configureQueenChronosphere(this);
|
||||
this.startRoyalRecovery(35);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,6 +1021,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
case "momentum_lock" -> !this.royalEffects.active("momentum_lock") && this.momentumLockCooldownTicks <= 0;
|
||||
case "crushing_gravity" -> phase != Phase.ONE && !this.royalEffects.active("crushing_gravity") && this.crushingGravityCooldownTicks <= 0;
|
||||
case "acceleration" -> phase == Phase.THREE && !this.royalEffects.active("acceleration") && this.accelerationCooldownTicks <= 0;
|
||||
case "queen_chronosphere" -> phase == Phase.THREE && this.queenChronosphereCooldownTicks <= 0;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
@@ -1013,6 +1037,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
};
|
||||
case THREE -> switch (id) {
|
||||
case "acceleration" -> 7;
|
||||
case "queen_chronosphere" -> 6;
|
||||
case "black_hole", "crushing_gravity" -> 5;
|
||||
case "gravity_stomp" -> 4;
|
||||
case "momentum_lock" -> 2;
|
||||
@@ -1084,6 +1109,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
case "momentum_lock" -> MOMENTUM_LOCK_TELEGRAPH_TICKS;
|
||||
case "crushing_gravity" -> CRUSHING_GRAVITY_TELEGRAPH_TICKS;
|
||||
case "acceleration" -> ACCELERATION_TELEGRAPH_TICKS;
|
||||
case "queen_chronosphere" -> 40;
|
||||
default -> 10;
|
||||
};
|
||||
}
|
||||
@@ -1111,6 +1137,10 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
this.accelerationTelegraphTicks = ACCELERATION_TELEGRAPH_TICKS;
|
||||
this.playRoyalSound(AntarchySoundEvents.QUEEN_ROAR.get(), 1.22F);
|
||||
}
|
||||
case "queen_chronosphere" -> {
|
||||
this.queenChronosphereTelegraphTicks = 40;
|
||||
this.playRoyalSound(AntarchySoundEvents.QUEEN_ROAR.get(), 0.85F);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,13 +1151,14 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
case "momentum_lock" -> this.momentumLockTelegraphTicks = 0;
|
||||
case "crushing_gravity" -> this.crushingGravityTelegraphTicks = 0;
|
||||
case "acceleration" -> this.accelerationTelegraphTicks = 0;
|
||||
case "queen_chronosphere" -> this.queenChronosphereTelegraphTicks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isQueenAbilityTelegraphActive() {
|
||||
return this.gravityStompTelegraphTicks > 0 || this.blackHoleTelegraphTicks > 0
|
||||
|| this.momentumLockTelegraphTicks > 0 || this.crushingGravityTelegraphTicks > 0
|
||||
|| this.accelerationTelegraphTicks > 0;
|
||||
|| this.accelerationTelegraphTicks > 0 || this.queenChronosphereTelegraphTicks > 0;
|
||||
}
|
||||
|
||||
private void clearQueenAbilityTelegraphs() {
|
||||
@@ -1136,6 +1167,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
this.momentumLockTelegraphTicks = 0;
|
||||
this.crushingGravityTelegraphTicks = 0;
|
||||
this.accelerationTelegraphTicks = 0;
|
||||
this.queenChronosphereTelegraphTicks = 0;
|
||||
this.pendingBlackHoleAnchor = null;
|
||||
}
|
||||
|
||||
@@ -1150,6 +1182,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
this.tickMomentumLockTelegraph(level);
|
||||
this.tickCrushingGravityTelegraph(level);
|
||||
this.tickAccelerationTelegraph(level);
|
||||
this.tickQueenChronosphereTelegraph(level);
|
||||
}
|
||||
|
||||
private void tickGravityStompTelegraph(ServerLevel level) {
|
||||
@@ -1235,6 +1268,18 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
this.accelerationTelegraphTicks--;
|
||||
}
|
||||
|
||||
private void tickQueenChronosphereTelegraph(ServerLevel level) {
|
||||
if (this.queenChronosphereTelegraphTicks <= 0) return;
|
||||
int elapsed = 40 - this.queenChronosphereTelegraphTicks;
|
||||
Vec3 center = this.position().add(0.0D, this.getBbHeight() * 0.5D, 0.0D);
|
||||
if (this.tickCount % 2 == 0) {
|
||||
double radius = Mth.lerp(elapsed / 40.0D, 3.0D, QUEEN_CHRONOSPHERE_RADIUS);
|
||||
this.spawnQueenTelegraphRing(level, center, radius, 96, ParticleTypes.REVERSE_PORTAL);
|
||||
}
|
||||
this.playQueenTelegraphBeat(level, this.queenChronosphereTelegraphTicks, 40, 0.5F);
|
||||
this.queenChronosphereTelegraphTicks--;
|
||||
}
|
||||
|
||||
private void spawnQueenTelegraphSpokes(ServerLevel level, Vec3 center, double radius) {
|
||||
int spokes = 10;
|
||||
int segments = 6;
|
||||
@@ -1524,6 +1569,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
tag.putInt("MomentumLockCooldownTicks", this.momentumLockCooldownTicks);
|
||||
tag.putInt("CrushingGravityCooldownTicks", this.crushingGravityCooldownTicks);
|
||||
tag.putInt("AccelerationCooldownTicks", this.accelerationCooldownTicks);
|
||||
tag.putInt("QueenChronosphereCooldownTicks", this.queenChronosphereCooldownTicks);
|
||||
tag.putInt("BlackHoleCooldownTicks", this.blackHoleCooldownTicks);
|
||||
tag.putInt("QueenLandingCooldownTicks", this.queenLandingCooldownTicks);
|
||||
tag.putString("QueuedQueenFollowUp", this.queuedQueenFollowUp.name());
|
||||
@@ -1547,6 +1593,7 @@ public class QueenEntity extends RoyalBossEntity {
|
||||
this.momentumLockCooldownTicks = Math.max(0, tag.getInt("MomentumLockCooldownTicks"));
|
||||
this.crushingGravityCooldownTicks = Math.max(0, tag.getInt("CrushingGravityCooldownTicks"));
|
||||
this.accelerationCooldownTicks = Math.max(0, tag.getInt("AccelerationCooldownTicks"));
|
||||
this.queenChronosphereCooldownTicks = Math.max(0, tag.getInt("QueenChronosphereCooldownTicks"));
|
||||
this.blackHoleCooldownTicks = Math.max(0, tag.getInt("BlackHoleCooldownTicks"));
|
||||
this.queenLandingCooldownTicks = tag.contains("QueenLandingCooldownTicks")
|
||||
? Math.max(0, tag.getInt("QueenLandingCooldownTicks")) : 300;
|
||||
|
||||
+6
-2
@@ -850,8 +850,7 @@ public abstract class RoyalBossEntity extends Monster implements GeoEntity, Mult
|
||||
Vec3 awayFromTarget = horizontalDistance > 1.0E-4D
|
||||
? new Vec3(-dx / horizontalDistance, 0.0D, -dz / horizontalDistance)
|
||||
: this.getViewVector(1.0F).multiply(-1.0D, 0.0D, -1.0D).normalize();
|
||||
double standoff = Math.max(this.getBbWidth() * 0.55D,
|
||||
BEAM_MUZZLE_FORWARD - this.biteReach() * 0.65D + 1.0D);
|
||||
double standoff = this.combatStandoffDistance();
|
||||
Vec3 wanted = target.position().add(awayFromTarget.scale(standoff));
|
||||
boolean atStandoff = this.position().multiply(1.0D, 0.0D, 1.0D)
|
||||
.distanceToSqr(wanted.multiply(1.0D, 0.0D, 1.0D)) <= 4.0D;
|
||||
@@ -908,6 +907,11 @@ public abstract class RoyalBossEntity extends Monster implements GeoEntity, Mult
|
||||
return false;
|
||||
}
|
||||
|
||||
protected double combatStandoffDistance() {
|
||||
return Math.max(this.getBbWidth() * 0.55D,
|
||||
BEAM_MUZZLE_FORWARD - this.biteReach() * 0.65D + 1.0D);
|
||||
}
|
||||
|
||||
private void tickCombatLocomotionMode(LivingEntity target) {
|
||||
if (!this.isFlyingBoss() || this.managesOwnCombatLocomotion()) {
|
||||
return;
|
||||
|
||||
+36
-3
@@ -42,6 +42,11 @@ public class RoyalElementalProjectileEntity extends ThrowableProjectile implemen
|
||||
private static final int ICEBALL = 1;
|
||||
private static final int DREAM_FIREBALL = 2;
|
||||
private static final int MAX_LIFETIME = 100;
|
||||
private static final float FIREBALL_SPEED = 1.5F;
|
||||
private static final float ICEBALL_SPEED = 1.2F;
|
||||
private static final float DREAM_FIREBALL_SPEED = 1.05F;
|
||||
private static final double PROJECTILE_INERTIA = 0.985D;
|
||||
private static final float DREAM_FIREBALL_INERTIA = 0.96F;
|
||||
private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this);
|
||||
|
||||
public RoyalElementalProjectileEntity(EntityType<? extends RoyalElementalProjectileEntity> type, Level level) {
|
||||
@@ -57,10 +62,38 @@ public class RoyalElementalProjectileEntity extends ThrowableProjectile implemen
|
||||
projectile.setPos(position.x, position.y, position.z);
|
||||
projectile.setAttackType(element == RoyalBeamElement.ICE ? ICEBALL
|
||||
: element == RoyalBeamElement.DREAM_FIRE ? DREAM_FIREBALL : FIREBALL);
|
||||
projectile.shoot(direction.x, direction.y, direction.z, element == RoyalBeamElement.ICE ? 0.8F : 1.05F, 0.0F);
|
||||
float speed = element == RoyalBeamElement.ICE ? ICEBALL_SPEED
|
||||
: element == RoyalBeamElement.DREAM_FIRE ? DREAM_FIREBALL_SPEED : FIREBALL_SPEED;
|
||||
projectile.shoot(direction.x, direction.y, direction.z, speed, 0.0F);
|
||||
return projectile;
|
||||
}
|
||||
|
||||
/** Solves a discrete ballistic lead against the projectile's drag and gravity. */
|
||||
public static Vec3 aimDirection(Vec3 origin, LivingEntity target, RoyalBeamElement element) {
|
||||
double speed = element == RoyalBeamElement.ICE ? ICEBALL_SPEED : FIREBALL_SPEED;
|
||||
double gravity = element == RoyalBeamElement.ICE ? 0.006D : 0.018D;
|
||||
double inertia = PROJECTILE_INERTIA;
|
||||
Vec3 bestDirection = target.getEyePosition().subtract(origin).normalize();
|
||||
double bestError = Double.MAX_VALUE;
|
||||
|
||||
for (int ticks = 1; ticks < MAX_LIFETIME; ticks++) {
|
||||
double dragDistance = (1.0D - Math.pow(inertia, ticks)) / (1.0D - inertia);
|
||||
Vec3 predictedTarget = target.getEyePosition().add(target.getDeltaMovement().scale(ticks));
|
||||
Vec3 displacement = predictedTarget.subtract(origin);
|
||||
double gravityCompensation = gravity * (ticks - dragDistance) / (1.0D - inertia);
|
||||
Vec3 initialVelocity = new Vec3(
|
||||
displacement.x / dragDistance,
|
||||
(displacement.y + gravityCompensation) / dragDistance,
|
||||
displacement.z / dragDistance);
|
||||
double error = Math.abs(initialVelocity.length() - speed);
|
||||
if (error < bestError) {
|
||||
bestError = error;
|
||||
bestDirection = initialVelocity.normalize();
|
||||
}
|
||||
}
|
||||
return bestDirection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void defineSynchedData(SynchedEntityData.Builder builder) {
|
||||
builder.define(ATTACK_TYPE, FIREBALL);
|
||||
@@ -80,11 +113,11 @@ public class RoyalElementalProjectileEntity extends ThrowableProjectile implemen
|
||||
|
||||
@Override
|
||||
protected double getDefaultGravity() {
|
||||
return this.isIceball() ? 0.012D : 0.018D;
|
||||
return this.isIceball() ? 0.006D : 0.018D;
|
||||
}
|
||||
|
||||
protected float getInertia() {
|
||||
return this.isIceball() ? 0.985F : 0.96F;
|
||||
return this.isDreamFireball() ? DREAM_FIREBALL_INERTIA : (float) PROJECTILE_INERTIA;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+54
-15
@@ -10,7 +10,7 @@ import com.craisinlord.antarchy.content.entity.royal.beam.RoyalBeamTerrainMode;
|
||||
import com.craisinlord.antarchy.content.effect.RoyalEffectEligibility;
|
||||
import com.craisinlord.antarchy.content.effect.RoyalEffectHooks;
|
||||
import com.craisinlord.antarchy.content.time.TimeDilationApi;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.syncher.EntityDataAccessor;
|
||||
@@ -34,6 +34,7 @@ import net.minecraft.world.entity.MoverType;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.entity.TamableAnimal;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.ai.control.FlyingMoveControl;
|
||||
import net.minecraft.world.entity.ai.control.MoveControl;
|
||||
@@ -71,6 +72,7 @@ import software.bernie.geckolib.animation.RawAnimation;
|
||||
import software.bernie.geckolib.util.GeckoLibUtil;
|
||||
|
||||
public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntity, FlyingAnimal {
|
||||
private static final ResourceLocation GROWTH_HEALTH_MODIFIER = ResourceLocation.fromNamespaceAndPath("antarchy", "royal_mount_growth_health");
|
||||
public static final int ANIM_IDLE = 0;
|
||||
public static final int ANIM_WALK = 1;
|
||||
public static final int ANIM_FLY = 2;
|
||||
@@ -309,6 +311,9 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
|
||||
this.navigation = flying ? this.flyingNavigation : this.groundNavigation;
|
||||
this.setNoGravity(flying);
|
||||
}
|
||||
if (GROWTH_PROGRESS.equals(key)) {
|
||||
this.refreshDimensions();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFlying(boolean flying) {
|
||||
@@ -350,11 +355,30 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
|
||||
public void setGrowthProgress(float progress) {
|
||||
float clamped = Mth.clamp(progress, 0.0F, 1.0F);
|
||||
if (this.getGrowthProgress() != clamped) {
|
||||
float oldHealth = this.getHealth();
|
||||
this.entityData.set(GROWTH_PROGRESS, clamped);
|
||||
var maxHealth = this.getAttribute(Attributes.MAX_HEALTH);
|
||||
if (maxHealth != null) {
|
||||
maxHealth.removeModifier(GROWTH_HEALTH_MODIFIER);
|
||||
if (clamped > 0.0F) {
|
||||
maxHealth.addTransientModifier(new AttributeModifier(GROWTH_HEALTH_MODIFIER,
|
||||
this.getGrowthScale() - 1.0D, AttributeModifier.Operation.ADD_MULTIPLIED_BASE));
|
||||
}
|
||||
this.setHealth(Math.min(oldHealth, this.getMaxHealth()));
|
||||
}
|
||||
this.refreshDimensions();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hurt(DamageSource source, float amount) {
|
||||
Entity attacker = source.getEntity();
|
||||
if (attacker != null && this.hasPassenger(attacker)) {
|
||||
return false;
|
||||
}
|
||||
return super.hurt(source, amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityDimensions getDefaultDimensions(Pose pose) {
|
||||
return super.getDefaultDimensions(pose).scale(this.getGrowthScale());
|
||||
@@ -369,19 +393,37 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
|
||||
public InteractionResult mobInteract(Player player, InteractionHand hand) {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
|
||||
if (this.isFood(stack)) {
|
||||
if (stack.is(Items.GOLDEN_APPLE) && !this.isTame()) {
|
||||
if (!this.level().isClientSide) {
|
||||
if (!player.getAbilities().instabuild) {
|
||||
stack.shrink(1);
|
||||
}
|
||||
this.tame(player);
|
||||
this.heal(8.0F);
|
||||
this.level().broadcastEntityEvent(this, (byte) 7);
|
||||
}
|
||||
return InteractionResult.sidedSuccess(this.level().isClientSide);
|
||||
}
|
||||
|
||||
if (stack.is(Items.ENCHANTED_GOLDEN_APPLE) && this.isTame() && this.isOwnedBy(player)) {
|
||||
if (!this.level().isClientSide) {
|
||||
if (!player.getAbilities().instabuild) {
|
||||
stack.shrink(1);
|
||||
}
|
||||
this.heal(8.0F);
|
||||
this.setGrowthProgress(this.getGrowthProgress() + (1.0F - this.getGrowthProgress()) * 0.1F);
|
||||
if (!this.isTame() && this.random.nextInt(3) == 0) {
|
||||
this.tame(player);
|
||||
this.level().broadcastEntityEvent(this, (byte) 7);
|
||||
} else {
|
||||
this.level().broadcastEntityEvent(this, (byte) 6);
|
||||
this.level().broadcastEntityEvent(this, (byte) 6);
|
||||
}
|
||||
return InteractionResult.sidedSuccess(this.level().isClientSide);
|
||||
}
|
||||
|
||||
if (this.isFood(stack)) {
|
||||
if (!this.level().isClientSide && this.isTame() && this.isOwnedBy(player) && this.getHealth() < this.getMaxHealth()) {
|
||||
if (!player.getAbilities().instabuild) {
|
||||
stack.shrink(1);
|
||||
}
|
||||
this.heal(8.0F);
|
||||
this.level().broadcastEntityEvent(this, (byte) 6);
|
||||
}
|
||||
return InteractionResult.sidedSuccess(this.level().isClientSide);
|
||||
}
|
||||
@@ -399,12 +441,7 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
|
||||
}
|
||||
|
||||
if (stack.isEmpty() && player.isSecondaryUseActive()) {
|
||||
if (this.isSaddled()) {
|
||||
if (!this.level().isClientSide) {
|
||||
this.setSaddled(false);
|
||||
this.spawnAtLocation(new ItemStack(Items.SADDLE));
|
||||
}
|
||||
} else if (!this.level().isClientSide) {
|
||||
if (!this.level().isClientSide) {
|
||||
this.setOrderedToSit(!this.isOrderedToSit());
|
||||
this.setInSittingPose(this.isOrderedToSit());
|
||||
this.navigation.stop();
|
||||
@@ -762,8 +799,10 @@ public abstract class RoyalMountEntity extends TamableAnimal implements GeoEntit
|
||||
if (!this.hasPassenger(passenger)) {
|
||||
return;
|
||||
}
|
||||
double yOffset = (this.isFlying() ? 1.55D : 1.35D) * this.getGrowthScale() - 1.0D;
|
||||
Vec3 back = this.getLookAngle().scale(-0.35D * this.getGrowthScale());
|
||||
// Seat near the back of the torso, outside the scaled body instead of inside its growing hitbox.
|
||||
double yOffset = Math.max(this.getBbHeight() * 0.82D, this.getBbHeight() - passenger.getBbHeight() * 0.65D);
|
||||
double backDistance = this.getBbWidth() * 0.5D + passenger.getBbWidth() * 0.5D + 0.15D;
|
||||
Vec3 back = this.getLookAngle().scale(-backDistance);
|
||||
moveFunction.accept(passenger, this.getX() + back.x, this.getY() + yOffset, this.getZ() + back.z);
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -11,9 +11,14 @@ public final class StandYourGroundDecree implements RoyalDecree {
|
||||
private final Map<UUID, Vec3> anchors = new HashMap<>();
|
||||
public String translationKey() { return "decree.antarchy.stand_your_ground"; }
|
||||
public void apply(ServerLevel level, KingEntity king, LivingEntity target) {
|
||||
Vec3 anchor = this.anchors.computeIfAbsent(target.getUUID(), ignored -> target.position());
|
||||
this.anchors.computeIfAbsent(target.getUUID(), ignored -> target.position());
|
||||
}
|
||||
@Override public Evaluation evaluate(ServerLevel level, KingEntity king, LivingEntity target) {
|
||||
if (target instanceof net.minecraft.server.level.ServerPlayer player && player.isFallFlying()) {
|
||||
// Gliding carries unavoidable momentum; begin measuring again from the landing point.
|
||||
this.anchors.put(target.getUUID(), target.position());
|
||||
return Evaluation.COMPLIANT;
|
||||
}
|
||||
Vec3 anchor = this.anchors.computeIfAbsent(target.getUUID(), ignored -> target.position());
|
||||
return target.position().distanceToSqr(anchor) > ALLOWED_DRIFT * ALLOWED_DRIFT
|
||||
? Evaluation.VIOLATED : Evaluation.COMPLIANT;
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
package com.craisinlord.antarchy.content.entity.trades;
|
||||
|
||||
import com.craisinlord.antarchy.content.AntarchyObjects;
|
||||
import com.craisinlord.antos.content.AntOSObjects;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -118,8 +118,8 @@ public final class ComputerScientistTradeManager extends SimpleJsonResourceReloa
|
||||
ResourceLocation diskId = diskType.equals("random_general")
|
||||
? GENERAL_DISKS.get(random.nextInt(GENERAL_DISKS.size()))
|
||||
: ResourceLocation.parse(diskType);
|
||||
ItemStack disk = new ItemStack(AntarchyObjects.FLOPPY_DISK.get(), count);
|
||||
disk.set(AntarchyObjects.FLOPPY_DISK_COMPONENT.get(), diskId);
|
||||
ItemStack disk = new ItemStack(AntOSObjects.FLOPPY_DISK.get(), count);
|
||||
disk.set(AntOSObjects.FLOPPY_DISK_COMPONENT.get(), diskId);
|
||||
return disk;
|
||||
}
|
||||
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.craisinlord.antarchy.content.entity.trades;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.entity.npc.VillagerTrades;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.trading.ItemCost;
|
||||
import net.minecraft.world.item.trading.MerchantOffer;
|
||||
|
||||
public final class PestControlTradeManager extends SimpleJsonResourceReloadListener {
|
||||
private static final Gson GSON = new Gson();
|
||||
private static volatile List<Entry> trades = List.of();
|
||||
|
||||
public PestControlTradeManager() {
|
||||
super(GSON, "pest_control_trades");
|
||||
}
|
||||
|
||||
public static int[] slotsPerLevel() {
|
||||
return new int[] {2, 2, 2, 1, 1};
|
||||
}
|
||||
|
||||
public static VillagerTrades.ItemListing listing(int level, int index) {
|
||||
return (villager, random) -> trades.stream().filter(entry -> entry.level == level && entry.index == index)
|
||||
.findFirst().map(entry -> entry.offer(random)).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Map<ResourceLocation, JsonElement> resources, ResourceManager manager, ProfilerFiller profiler) {
|
||||
List<Entry> parsed = new ArrayList<>();
|
||||
for (JsonElement resource : resources.values()) {
|
||||
if (!resource.isJsonObject()) continue;
|
||||
JsonObject root = resource.getAsJsonObject();
|
||||
if (root.has("trades")) {
|
||||
for (JsonElement element : root.getAsJsonArray("trades")) if (element.isJsonObject()) parsed.add(Entry.parse(element.getAsJsonObject()));
|
||||
} else parsed.add(Entry.parse(root));
|
||||
}
|
||||
trades = List.copyOf(parsed);
|
||||
}
|
||||
|
||||
private record Entry(int level, int index, String buy, int buyCount, String sell, int sellCount,
|
||||
String secondBuy, int secondBuyCount, int maxUses, int xp) {
|
||||
private MerchantOffer offer(RandomSource random) {
|
||||
var buyItem = BuiltInRegistries.ITEM.get(ResourceLocation.parse(buy));
|
||||
var sellItem = BuiltInRegistries.ITEM.get(ResourceLocation.parse(sell));
|
||||
var secondCost = secondBuy == null ? java.util.Optional.<ItemCost>empty()
|
||||
: java.util.Optional.of(new ItemCost(BuiltInRegistries.ITEM.get(ResourceLocation.parse(secondBuy)), secondBuyCount));
|
||||
return new MerchantOffer(new ItemCost(buyItem, buyCount), secondCost, new ItemStack(sellItem, sellCount), maxUses, xp, 0.05F);
|
||||
}
|
||||
|
||||
private static Entry parse(JsonObject json) {
|
||||
JsonObject buy = json.getAsJsonObject("buy");
|
||||
JsonObject sell = json.getAsJsonObject("sell");
|
||||
JsonObject second = json.has("second_buy") ? json.getAsJsonObject("second_buy") : null;
|
||||
return new Entry(json.get("level").getAsInt(), json.get("index").getAsInt(), buy.get("item").getAsString(),
|
||||
count(buy), sell.get("item").getAsString(), count(sell), second == null ? null : second.get("item").getAsString(),
|
||||
second == null ? 0 : count(second), json.has("max_uses") ? json.get("max_uses").getAsInt() : 12,
|
||||
json.has("villager_xp") ? json.get("villager_xp").getAsInt() : 5);
|
||||
}
|
||||
|
||||
private static int count(JsonObject stack) {
|
||||
if (stack.has("min") && stack.has("max")) return stack.get("min").getAsInt();
|
||||
return stack.has("count") ? stack.get("count").getAsInt() : stack.get("min").getAsInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
-290
@@ -1,290 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.guide;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import com.craisinlord.antarchy.config.AntarchySettings;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ComputerGuideData extends SimplePreparableReloadListener<ComputerGuideData.LoadedData> {
|
||||
private static final ComputerGuideData INSTANCE = new ComputerGuideData();
|
||||
private static final ResourceLocation INTRODUCTION = ResourceLocation.fromNamespaceAndPath("antarchy", "introduction");
|
||||
private static volatile Map<ResourceLocation, Entry> entries = Map.of();
|
||||
private static volatile Map<ResourceLocation, Disk> disks = Map.of();
|
||||
|
||||
private ComputerGuideData() {
|
||||
}
|
||||
|
||||
public static ComputerGuideData instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static Entry entry(ResourceLocation id) {
|
||||
return entries.get(id);
|
||||
}
|
||||
|
||||
public static Disk disk(ResourceLocation id) {
|
||||
return disks.get(id);
|
||||
}
|
||||
|
||||
public static List<Entry> entriesFor(List<ResourceLocation> diskIds) {
|
||||
if (AntarchySettings.unlockAllArchives()) {
|
||||
Map<ResourceLocation, Entry> result = new LinkedHashMap<>();
|
||||
result.put(INTRODUCTION, introduction());
|
||||
result.putAll(entries);
|
||||
return result.values().stream().sorted(Comparator.comparing(entry -> entry.titleKey().toString())).toList();
|
||||
}
|
||||
Map<ResourceLocation, Entry> result = new LinkedHashMap<>();
|
||||
for (ResourceLocation diskId : diskIds) {
|
||||
if (INTRODUCTION.equals(diskId)) {
|
||||
result.put(INTRODUCTION, introduction());
|
||||
continue;
|
||||
}
|
||||
Disk disk = disks.get(diskId);
|
||||
if (disk == null) {
|
||||
continue;
|
||||
}
|
||||
for (ResourceLocation entryId : disk.entries()) {
|
||||
Entry entry = entries.get(entryId);
|
||||
if (entry != null) {
|
||||
result.putIfAbsent(entryId, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.values().stream().sorted(Comparator.comparing(entry -> entry.titleKey().toString())).toList();
|
||||
}
|
||||
|
||||
private static Entry introduction() {
|
||||
return new Entry(INTRODUCTION, "article", "general", "guide.antarchy.entry.introduction.title",
|
||||
"guide.antarchy.entry.introduction.subtitle", List.of("guide.antarchy.entry.introduction.description"),
|
||||
"", "", "", "", "", "", "", 0, "", "", "", 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
/** Encodes the server data-pack view so remote clients do not need a duplicate asset copy. */
|
||||
public static String encodeNetworkSnapshot() {
|
||||
JsonObject root = new JsonObject();
|
||||
JsonArray entryArray = new JsonArray();
|
||||
entries.values().stream().sorted(Comparator.comparing(entry -> entry.id().toString())).forEach(entry -> {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("id", entry.id().toString());
|
||||
object.addProperty("type", entry.type());
|
||||
object.addProperty("category", entry.category());
|
||||
object.addProperty("title", entry.titleKey());
|
||||
object.addProperty("subtitle", entry.subtitleKey());
|
||||
JsonArray descriptions = new JsonArray();
|
||||
entry.descriptionKeys().forEach(descriptions::add);
|
||||
object.add("description", descriptions);
|
||||
object.addProperty("item", entry.itemId());
|
||||
object.addProperty("entity", entry.entityId());
|
||||
object.addProperty("enchantment", entry.enchantmentId());
|
||||
object.addProperty("recipe", entry.recipeId());
|
||||
object.addProperty("structure", entry.structureId());
|
||||
object.addProperty("structure_tag", entry.structureTagId());
|
||||
object.addProperty("dimension", entry.dimensionId());
|
||||
object.addProperty("search_radius", entry.searchRadius());
|
||||
object.addProperty("cover_item", entry.coverItemId());
|
||||
object.addProperty("cover_entity", entry.coverEntityId());
|
||||
object.addProperty("cover_potion", entry.coverPotionId());
|
||||
object.addProperty("rotation", entry.rotation());
|
||||
object.addProperty("render_scale", entry.renderScale());
|
||||
entryArray.add(object);
|
||||
});
|
||||
root.add("entries", entryArray);
|
||||
|
||||
JsonArray diskArray = new JsonArray();
|
||||
disks.values().stream().sorted(Comparator.comparing(disk -> disk.id().toString())).forEach(disk -> {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("id", disk.id().toString());
|
||||
object.addProperty("category", disk.category());
|
||||
object.addProperty("title", disk.titleKey());
|
||||
JsonArray diskEntries = new JsonArray();
|
||||
disk.entries().forEach(id -> diskEntries.add(id.toString()));
|
||||
object.add("entries", diskEntries);
|
||||
object.addProperty("wallpaper", disk.wallpaper());
|
||||
diskArray.add(object);
|
||||
});
|
||||
root.add("disks", diskArray);
|
||||
return root.toString();
|
||||
}
|
||||
|
||||
/** Applies the authoritative server snapshot on a multiplayer client. */
|
||||
public static void applyNetworkSnapshot(String json) {
|
||||
try {
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
Map<ResourceLocation, Entry> loadedEntries = new HashMap<>();
|
||||
Map<ResourceLocation, Disk> loadedDisks = new HashMap<>();
|
||||
JsonArray entryArray = root.getAsJsonArray("entries");
|
||||
if (entryArray != null) {
|
||||
for (JsonElement element : entryArray) {
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
parseEntry(ResourceLocation.parse(string(object, "id", "")), object, loadedEntries);
|
||||
}
|
||||
}
|
||||
JsonArray diskArray = root.getAsJsonArray("disks");
|
||||
if (diskArray != null) {
|
||||
for (JsonElement element : diskArray) {
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
parseDisk(ResourceLocation.parse(string(object, "id", "")), object, loadedDisks);
|
||||
}
|
||||
}
|
||||
entries = Map.copyOf(loadedEntries);
|
||||
disks = Map.copyOf(loadedDisks);
|
||||
Antarchy.LOGGER.info("Loaded {} computer archive entries and {} disks from the server", entries.size(), disks.size());
|
||||
} catch (RuntimeException exception) {
|
||||
Antarchy.LOGGER.error("Failed to decode computer archive data from the server", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LoadedData prepare(ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
Map<ResourceLocation, Entry> loadedEntries = new HashMap<>();
|
||||
Map<ResourceLocation, Disk> loadedDisks = new HashMap<>();
|
||||
for (Map.Entry<ResourceLocation, net.minecraft.server.packs.resources.Resource> resource : resourceManager.listResources("computer/entry", path -> path.getPath().endsWith(".json")).entrySet()) {
|
||||
ResourceLocation id = resourceId(resource.getKey(), "computer/entry");
|
||||
try {
|
||||
JsonElement json = JsonParser.parseReader(resource.getValue().openAsReader());
|
||||
parseEntry(id, json, loadedEntries);
|
||||
} catch (Exception exception) {
|
||||
Antarchy.LOGGER.error("Failed to load computer archive entry {}", resource.getKey(), exception);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<ResourceLocation, net.minecraft.server.packs.resources.Resource> resource : resourceManager.listResources("computer/disk", path -> path.getPath().endsWith(".json")).entrySet()) {
|
||||
ResourceLocation id = resourceId(resource.getKey(), "computer/disk");
|
||||
try {
|
||||
JsonElement json = JsonParser.parseReader(resource.getValue().openAsReader());
|
||||
parseDisk(id, json, loadedDisks);
|
||||
} catch (Exception exception) {
|
||||
Antarchy.LOGGER.error("Failed to load computer archive disk {}", resource.getKey(), exception);
|
||||
}
|
||||
}
|
||||
return new LoadedData(Map.copyOf(loadedEntries), Map.copyOf(loadedDisks));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(LoadedData data, ResourceManager resourceManager, ProfilerFiller profiler) {
|
||||
entries = data.entries();
|
||||
disks = data.disks();
|
||||
}
|
||||
|
||||
private static ResourceLocation resourceId(ResourceLocation path, String directory) {
|
||||
String prefix = directory + "/";
|
||||
String value = path.getPath().startsWith(prefix) ? path.getPath().substring(prefix.length()) : path.getPath();
|
||||
if (value.endsWith(".json")) {
|
||||
value = value.substring(0, value.length() - 5);
|
||||
}
|
||||
return ResourceLocation.fromNamespaceAndPath(path.getNamespace(), value);
|
||||
}
|
||||
|
||||
private static void parseEntry(ResourceLocation id, JsonElement element, Map<ResourceLocation, Entry> target) {
|
||||
if (!element.isJsonObject()) {
|
||||
return;
|
||||
}
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
String type = string(object, "type", "article");
|
||||
String category = string(object, "category", "general");
|
||||
String title = string(object, "title", "guide.antarchy.missing_title");
|
||||
String subtitle = string(object, "subtitle", "");
|
||||
List<String> description = strings(object.get("description"));
|
||||
String item = string(object, "item", "");
|
||||
String entity = string(object, "entity", "");
|
||||
String enchantment = string(object, "enchantment", "");
|
||||
String recipe = string(object, "recipe", "");
|
||||
String structure = string(object, "structure", "");
|
||||
String structureTag = string(object, "structure_tag", "");
|
||||
String dimension = string(object, "dimension", "");
|
||||
int searchRadius = object.has("search_radius") && object.get("search_radius").isJsonPrimitive()
|
||||
? Math.max(0, Math.min(1000, object.get("search_radius").getAsInt())) : 0;
|
||||
String coverItem = string(object, "cover_item", "");
|
||||
String coverEntity = string(object, "cover_entity", "");
|
||||
String coverPotion = string(object, "cover_potion", "");
|
||||
float rotation = object.has("rotation") && object.get("rotation").isJsonPrimitive()
|
||||
? object.get("rotation").getAsFloat() : 0.0F;
|
||||
float renderScale = object.has("render_scale") && object.get("render_scale").isJsonPrimitive()
|
||||
? object.get("render_scale").getAsFloat() : 1.0F;
|
||||
renderScale = Math.max(0.1F, Math.min(3.0F, renderScale));
|
||||
target.put(id, new Entry(id, type, category, title, subtitle, description, item, entity, enchantment, recipe, structure, structureTag, dimension, searchRadius,
|
||||
coverItem, coverEntity, coverPotion, rotation, renderScale));
|
||||
}
|
||||
|
||||
private static void parseDisk(ResourceLocation id, JsonElement element, Map<ResourceLocation, Disk> target) {
|
||||
if (!element.isJsonObject()) {
|
||||
return;
|
||||
}
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
String category = string(object, "category", "general");
|
||||
String title = string(object, "title", "guide.antarchy.missing_disk_title");
|
||||
List<ResourceLocation> entryIds = new ArrayList<>();
|
||||
JsonElement entriesElement = object.get("entries");
|
||||
if (entriesElement != null && entriesElement.isJsonArray()) {
|
||||
for (JsonElement entry : entriesElement.getAsJsonArray()) {
|
||||
if (entry.isJsonPrimitive() && entry.getAsJsonPrimitive().isString()) {
|
||||
try {
|
||||
entryIds.add(ResourceLocation.parse(entry.getAsString()));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String wallpaper = string(object, "wallpaper", "");
|
||||
target.put(id, new Disk(id, category, title, List.copyOf(entryIds), wallpaper));
|
||||
}
|
||||
|
||||
private static String string(JsonObject object, String key, String fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element != null && element.isJsonPrimitive() ? element.getAsString() : fallback;
|
||||
}
|
||||
|
||||
private static List<String> strings(JsonElement element) {
|
||||
if (element == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (element.isJsonPrimitive()) {
|
||||
return List.of(element.getAsString());
|
||||
}
|
||||
if (!element.isJsonArray()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> values = new ArrayList<>();
|
||||
JsonArray array = element.getAsJsonArray();
|
||||
for (JsonElement value : array) {
|
||||
if (value.isJsonPrimitive()) {
|
||||
values.add(value.getAsString());
|
||||
} else if (value.isJsonObject()) {
|
||||
JsonObject render = value.getAsJsonObject();
|
||||
String item = string(render, "item", "");
|
||||
String entity = string(render, "entity", "");
|
||||
String enchantment = string(render, "enchantment", "");
|
||||
String recipe = string(render, "recipe", "");
|
||||
if (!item.isBlank()) values.add("@item:" + item);
|
||||
else if (!entity.isBlank()) values.add("@entity:" + entity);
|
||||
else if (!enchantment.isBlank()) values.add("@enchantment:" + enchantment);
|
||||
else if (!recipe.isBlank()) values.add("@recipe:" + recipe);
|
||||
}
|
||||
}
|
||||
return List.copyOf(values);
|
||||
}
|
||||
|
||||
public record Entry(ResourceLocation id, String type, String category, String titleKey, String subtitleKey,
|
||||
List<String> descriptionKeys, String itemId, String entityId, String enchantmentId,
|
||||
String recipeId, String structureId, String structureTagId, String dimensionId, int searchRadius, String coverItemId,
|
||||
String coverEntityId, String coverPotionId, float rotation, float renderScale) {
|
||||
}
|
||||
|
||||
public record Disk(ResourceLocation id, String category, String titleKey, List<ResourceLocation> entries, String wallpaper) {
|
||||
}
|
||||
|
||||
record LoadedData(Map<ResourceLocation, Entry> entries, Map<ResourceLocation, Disk> disks) {
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -110,8 +110,7 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
|
||||
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (player.isShiftKeyDown()) {
|
||||
cycleMode(level, player, stack);
|
||||
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
|
||||
return InteractionResultHolder.pass(stack);
|
||||
}
|
||||
|
||||
BossMode mode = getMode(stack);
|
||||
@@ -390,8 +389,8 @@ public class BigBerthaItem extends SwordItem implements GeoItem {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean tryCycleModeWhileCoolingDown(Level level, Player player, ItemStack stack) {
|
||||
if (player.getMainHandItem() != stack || !player.isShiftKeyDown() || !player.getCooldowns().isOnCooldown(this)) {
|
||||
public boolean tryCycleModeFromInput(Level level, Player player, ItemStack stack) {
|
||||
if (player.getMainHandItem() != stack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.craisinlord.antarchy.content.item;
|
||||
|
||||
import com.craisinlord.antarchy.content.AntarchyObjects;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResultHolder;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.tags.EntityTypeTags;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import com.craisinlord.antarchy.content.entity.ant.BaseAntEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class BugSprayItem extends Item {
|
||||
private static final TagKey<net.minecraft.world.entity.EntityType<?>> ANARCHY_ARTHROPODS = TagKey.create(
|
||||
net.minecraft.core.registries.Registries.ENTITY_TYPE, ResourceLocation.fromNamespaceAndPath("antarchy", "arthropods"));
|
||||
|
||||
public BugSprayItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (!level.isClientSide && level instanceof ServerLevel serverLevel) {
|
||||
Vec3 look = player.getLookAngle().normalize();
|
||||
Vec3 origin = player.getEyePosition().add(look.scale(0.6D));
|
||||
var sprayParticle = AntarchyObjects.BUG_SPRAY_PARTICLE.get();
|
||||
serverLevel.sendParticles(sprayParticle, origin.x, origin.y, origin.z, 18,
|
||||
look.x * 0.45D, look.y * 0.45D, look.z * 0.45D, 0.06D);
|
||||
AABB bounds = new AABB(origin, origin.add(look.scale(7.0D))).inflate(2.2D);
|
||||
for (Entity entity : level.getEntities(player, bounds, candidate -> candidate instanceof Mob mob
|
||||
&& (mob.getType().is(EntityTypeTags.SENSITIVE_TO_BANE_OF_ARTHROPODS)
|
||||
|| mob.getType().is(ANARCHY_ARTHROPODS) || mob instanceof BaseAntEntity))) {
|
||||
if (!(entity instanceof Mob mob)) continue;
|
||||
Vec3 toward = mob.position().subtract(origin);
|
||||
double distance = toward.length();
|
||||
if (distance > 9.0D || distance < 0.001D || toward.normalize().dot(look) < 0.55D) continue;
|
||||
Vec3 flee = mob.position().subtract(player.position()).normalize();
|
||||
mob.getNavigation().moveTo(mob.getX() + flee.x * 6.0D, mob.getY(), mob.getZ() + flee.z * 6.0D, 1.25D);
|
||||
mob.setTarget(null);
|
||||
mob.setLastHurtByMob(null);
|
||||
mob.setDeltaMovement(mob.getDeltaMovement().add(flee.scale(0.55D)).add(0.0D, 0.15D, 0.0D));
|
||||
mob.hurtMarked = true;
|
||||
serverLevel.sendParticles(sprayParticle, mob.getX(), mob.getY() + mob.getBbHeight() * 0.5D, mob.getZ(), 5, 0.12D, 0.12D, 0.12D, 0.02D);
|
||||
}
|
||||
level.playSound(null, player.blockPosition(), SoundEvents.FIRE_EXTINGUISH, SoundSource.PLAYERS, 0.7F, 1.2F);
|
||||
stack.shrink(1);
|
||||
}
|
||||
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.item;
|
||||
|
||||
import com.craisinlord.antarchy.content.block.ComputerBlock;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
|
||||
public final class ComputerItem extends BlockItem {
|
||||
public ComputerItem(ComputerBlock block, Properties properties) {
|
||||
super(block, properties);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.item;
|
||||
|
||||
import com.craisinlord.antarchy.content.AntarchyObjects;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class FloppyDiskItem extends Item {
|
||||
public FloppyDiskItem(Properties properties) {
|
||||
super(properties.stacksTo(1));
|
||||
}
|
||||
|
||||
public static float categoryModelProperty(ItemStack stack) {
|
||||
ResourceLocation id = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
|
||||
if (id == null) return 0.0F;
|
||||
var disk = com.craisinlord.antarchy.content.guide.ComputerGuideData.disk(id);
|
||||
if (disk == null) return 0.0F;
|
||||
return switch (disk.category()) {
|
||||
case "elythia" -> 1.0F;
|
||||
case "cavaryn" -> 2.0F;
|
||||
case "thoraxis" -> 3.0F;
|
||||
default -> 0.0F;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
ResourceLocation id = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
|
||||
if (id != null) {
|
||||
var disk = com.craisinlord.antarchy.content.guide.ComputerGuideData.disk(id);
|
||||
if (disk != null) {
|
||||
return Component.translatable(disk.titleKey());
|
||||
}
|
||||
}
|
||||
return Component.translatable("item.antarchy.floppy_disk");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, List<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.add(Component.translatable("item.antarchy.floppy_disk.tooltip"));
|
||||
ResourceLocation id = stack.get(AntarchyObjects.FLOPPY_DISK_COMPONENT.get());
|
||||
if (id != null) {
|
||||
var disk = com.craisinlord.antarchy.content.guide.ComputerGuideData.disk(id);
|
||||
if (disk != null) {
|
||||
tooltip.add(Component.translatable("guide.antarchy.category." + disk.category()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-9
@@ -2,6 +2,7 @@ package com.craisinlord.antarchy.content.item;
|
||||
|
||||
import com.craisinlord.antarchy.config.AntarchySettings;
|
||||
import com.craisinlord.antarchy.content.effect.CommandedEntityAccess;
|
||||
import com.craisinlord.antarchy.content.AntarchySoundEvents;
|
||||
import com.craisinlord.antarchy.content.entity.royal.RoyalIceSpikeEntity;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -126,21 +127,23 @@ public final class RoyalGuardianSwordAbilities {
|
||||
if (forward.lengthSqr() < 1.0E-5D) forward = new Vec3(0.0D, 0.0D, 1.0D);
|
||||
forward = forward.normalize();
|
||||
Vec3 side = new Vec3(-forward.z, 0.0D, forward.x);
|
||||
for (int distanceStep = 1; distanceStep <= 4; distanceStep++) {
|
||||
double distance = 2.5D + distanceStep * 2.7D;
|
||||
for (int distanceStep = 1; distanceStep <= 5; distanceStep++) {
|
||||
double distance = distanceStep * 4.0D;
|
||||
for (int lane = -1; lane <= 1; lane++) {
|
||||
Vec3 point = primary.position().add(forward.scale(distance)).add(side.scale(lane * 1.8D));
|
||||
BlockPos surface = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, BlockPos.containing(point));
|
||||
Vec3 spawn = new Vec3(point.x, surface.getY() + 0.1D, point.z);
|
||||
Vec3 point = owner.position().add(forward.scale(distance))
|
||||
.add(side.scale(lane * (distanceStep % 2 == 0 ? 2.0D : 0.0D)));
|
||||
int groundY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
net.minecraft.util.Mth.floor(point.x), net.minecraft.util.Mth.floor(point.z));
|
||||
Vec3 spawn = new Vec3(point.x, groundY + 0.1D, point.z);
|
||||
RoyalIceSpikeEntity spike = RoyalIceSpikeEntity.create(level, spawn, owner,
|
||||
(float) AntarchySettings.royalGuardianSwordFrostSpikeDamage(), 1.55D, 100, 60, 1);
|
||||
(float) AntarchySettings.royalGuardianSwordFrostSpikeDamage(), 2.2D, 100, 0, 0);
|
||||
level.addFreshEntity(spike);
|
||||
}
|
||||
}
|
||||
Vec3 center = primary.position().add(0.0D, 0.8D, 0.0D);
|
||||
level.sendParticles(ParticleTypes.SNOWFLAKE, center.x, center.y, center.z, 70, 1.3D, 1.1D, 1.3D, 0.1D);
|
||||
level.sendParticles(ParticleTypes.SNOWFLAKE, owner.getX(), owner.getY() + 1.0D, owner.getZ(), 18, 0.7D, 0.5D, 0.7D, 0.04D);
|
||||
level.playSound(null, primary.blockPosition(), SoundEvents.GLASS_BREAK, SoundSource.PLAYERS, 1.0F, 0.6F);
|
||||
level.playSound(null, primary.blockPosition(), AntarchySoundEvents.KING_ICE_SPIKES.get(), SoundSource.PLAYERS, 0.8F, 0.95F);
|
||||
}
|
||||
|
||||
private static void chainLightning(ServerLevel level, Player owner, LivingEntity primary) {
|
||||
@@ -187,9 +190,10 @@ public final class RoyalGuardianSwordAbilities {
|
||||
double length = direction.length();
|
||||
if (length > 0.001D) {
|
||||
Vec3 step = direction.scale(1.0D / length);
|
||||
for (double d = 0.0D; d < length; d += 1.2D) {
|
||||
for (double d = 0.0D; d < length; d += 0.35D) {
|
||||
Vec3 point = start.add(step.scale(d));
|
||||
level.sendParticles(STORM_GOLD, point.x, point.y, point.z, 2, 0.08D, 0.08D, 0.08D, 0.04D);
|
||||
level.sendParticles(ParticleTypes.ELECTRIC_SPARK, point.x, point.y, point.z, 1, 0.025D, 0.025D, 0.025D, 0.0D);
|
||||
level.sendParticles(STORM_GOLD, point.x, point.y, point.z, 1, 0.025D, 0.025D, 0.025D, 0.0D);
|
||||
}
|
||||
}
|
||||
level.sendParticles(ParticleTypes.ELECTRIC_SPARK, end.x, end.y, end.z, 18, 0.25D, 0.4D, 0.25D, 0.08D);
|
||||
|
||||
+4
-3
@@ -77,11 +77,11 @@ public class RoyalGuardianSwordItem extends SwordItem implements GeoItem {
|
||||
public InteractionResultHolder<ItemStack> use(net.minecraft.world.level.Level level, Player player, InteractionHand hand) {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (!player.isShiftKeyDown()) return InteractionResultHolder.pass(stack);
|
||||
cycleMode(level, player, stack);
|
||||
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
|
||||
return InteractionResultHolder.pass(stack);
|
||||
}
|
||||
|
||||
private void cycleMode(net.minecraft.world.level.Level level, Player player, ItemStack stack) {
|
||||
public boolean tryCycleModeFromInput(net.minecraft.world.level.Level level, Player player, ItemStack stack) {
|
||||
if (player.getMainHandItem() != stack) return false;
|
||||
Mode next = getMode(stack).next();
|
||||
setMode(stack, next);
|
||||
if (!level.isClientSide) {
|
||||
@@ -89,6 +89,7 @@ public class RoyalGuardianSwordItem extends SwordItem implements GeoItem {
|
||||
Component.translatable(next.translationKey)), true);
|
||||
level.playSound(null, player.blockPosition(), SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 0.8F, 0.8F + next.ordinal() * 0.2F);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void setMode(ItemStack stack, Mode mode) {
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.config.AntarchySettings;
|
||||
import com.craisinlord.antarchy.content.computer.AntarchyGameSavedData;
|
||||
import com.craisinlord.antarchy.content.computer.blockle.BlockleAnswers;
|
||||
import com.craisinlord.antarchy.content.computer.blockle.BlockleDictionary;
|
||||
import com.craisinlord.antarchy.content.computer.blockle.BlockleGame;
|
||||
import java.util.function.BiConsumer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Handles Antarchy-specific game progress sent by AntOS game extensions. */
|
||||
public final class AntarchyGameNetworkHandler {
|
||||
private static BiConsumer<ServerPlayer, AntarchyGameResultPayload> resultSender = (player, result) -> { };
|
||||
|
||||
private AntarchyGameNetworkHandler() { }
|
||||
|
||||
public static void setResultSender(BiConsumer<ServerPlayer, AntarchyGameResultPayload> sender) {
|
||||
resultSender = sender;
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntarchyGamePayload payload) {
|
||||
BlockPos pos = payload.pos();
|
||||
if (payload.value().length() > 65536 || player.level().isClientSide || !player.serverLevel().hasChunkAt(pos)
|
||||
|| player.distanceToSqr(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D) > 64.0D) return;
|
||||
|
||||
var blockEntity = player.serverLevel().getBlockEntity(pos);
|
||||
if (!(blockEntity instanceof com.craisinlord.antos.content.block.entity.ComputerBlockEntity computer)
|
||||
|| !computer.canUseFileSystem(player)) {
|
||||
send(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
String key = AntarchyGameSavedData.key(player.serverLevel().dimension().location(), pos);
|
||||
AntarchyGameSavedData.GameState state = AntarchyGameSavedData.get(player.server, key);
|
||||
switch (payload.action()) {
|
||||
case AntarchyGamePayload.BASILISK_STATE -> basilisk(player, payload, state);
|
||||
case AntarchyGamePayload.ANTMAN_STATE -> antman(player, payload, state);
|
||||
case AntarchyGamePayload.BLOCKLE_STATE -> blockle(player, computer, payload, state, false);
|
||||
case AntarchyGamePayload.BLOCKLE_GUESS -> blockle(player, computer, payload, state, true);
|
||||
default -> send(player, payload, false, "", "invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private static void basilisk(ServerPlayer player, AntarchyGamePayload payload, AntarchyGameSavedData.GameState state) {
|
||||
if (!payload.value().isBlank()) {
|
||||
Integer score = parseScore(payload.value());
|
||||
if (score == null) { send(player, payload, false, "", "invalid_score"); return; }
|
||||
state.basiliskScore = score;
|
||||
state.basiliskHighScore = Math.max(state.basiliskHighScore, score);
|
||||
AntarchyGameSavedData.changed(player.server);
|
||||
}
|
||||
send(player, payload, true, state.basiliskScore + "\0" + state.basiliskHighScore, "");
|
||||
}
|
||||
|
||||
private static void antman(ServerPlayer player, AntarchyGamePayload payload, AntarchyGameSavedData.GameState state) {
|
||||
if (!payload.value().isBlank()) {
|
||||
Integer score = parseScore(payload.value());
|
||||
if (score == null) { send(player, payload, false, "", "invalid_score"); return; }
|
||||
state.antmanScore = score;
|
||||
state.antmanHighScore = Math.max(state.antmanHighScore, score);
|
||||
AntarchyGameSavedData.changed(player.server);
|
||||
}
|
||||
send(player, payload, true, state.antmanScore + "\0" + state.antmanHighScore, "");
|
||||
}
|
||||
|
||||
private static Integer parseScore(String value) {
|
||||
try { return Math.min(1_000_000, Math.max(0, Integer.parseInt(value))); }
|
||||
catch (NumberFormatException ignored) { return null; }
|
||||
}
|
||||
|
||||
private static void blockle(ServerPlayer player,
|
||||
com.craisinlord.antos.content.block.entity.ComputerBlockEntity computer,
|
||||
AntarchyGamePayload payload, AntarchyGameSavedData.GameState state, boolean submit) {
|
||||
ResourceLocation disk = ResourceLocation.fromNamespaceAndPath("antarchy", "blockle_game");
|
||||
if (!AntarchySettings.unlockAllArchives() && !computer.diskIds().contains(disk)) {
|
||||
send(player, payload, false, "", "not_installed"); return;
|
||||
}
|
||||
ServerLevel overworld = player.server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) { send(player, payload, false, "", "overworld_unavailable"); return; }
|
||||
long day = overworld.getDayTime() / 24000L;
|
||||
if (state.blockleDay != day) {
|
||||
state.blockleDay = day;
|
||||
state.blockleGuesses.clear();
|
||||
AntarchyGameSavedData.changed(player.server);
|
||||
}
|
||||
BlockleAnswers.Answer answer = BlockleAnswers.answerForDay(day);
|
||||
if (answer == null) { send(player, payload, false, "", "answers_unavailable"); return; }
|
||||
if (submit) {
|
||||
String guess = payload.value().toLowerCase(java.util.Locale.ROOT);
|
||||
if (guess.length() != 5 || !guess.chars().allMatch(value -> value >= 'a' && value <= 'z') || !BlockleDictionary.contains(guess)) {
|
||||
send(player, payload, false, "", "invalid_word"); return;
|
||||
}
|
||||
if (state.blockleGuesses.size() >= 6 || state.blockleGuesses.contains(answer.word())) {
|
||||
send(player, payload, false, "", "game_over"); return;
|
||||
}
|
||||
state.blockleGuesses.add(guess);
|
||||
AntarchyGameSavedData.changed(player.server);
|
||||
}
|
||||
send(player, payload, true, encodeBlockle(day, answer, state.blockleGuesses), "");
|
||||
}
|
||||
|
||||
private static String encodeBlockle(long day, BlockleAnswers.Answer answer, java.util.List<String> guesses) {
|
||||
StringBuilder result = new StringBuilder(Long.toString(day));
|
||||
boolean solved = false;
|
||||
for (String guess : guesses) {
|
||||
result.append('|').append(guess).append(',').append(BlockleGame.evaluate(answer.word(), guess));
|
||||
if (guess.equals(answer.word())) solved = true;
|
||||
}
|
||||
if (solved || guesses.size() >= 6) result.append('|').append(solved ? "SOLVED" : "FAILED").append('|').append(answer.word()).append('|').append(answer.itemId());
|
||||
else result.append('|').append("PLAYING");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static void send(ServerPlayer player, AntarchyGamePayload payload, boolean success, String data, String error) {
|
||||
resultSender.accept(player, new AntarchyGameResultPayload(payload.pos(), success,
|
||||
payload.action() + "\0" + error + "\0" + data));
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Small Antarchy game-progress bridge over the AntOS computer session. */
|
||||
public final class AntarchyGameNetworking {
|
||||
private static Consumer<AntarchyGamePayload> sender = payload -> { };
|
||||
private AntarchyGameNetworking() { }
|
||||
|
||||
public static void setSender(Consumer<AntarchyGamePayload> sender) { AntarchyGameNetworking.sender = sender; }
|
||||
public static void requestBasiliskState(BlockPos pos) { send(pos, AntarchyGamePayload.BASILISK_STATE, ""); }
|
||||
public static void saveBasiliskScore(BlockPos pos, int score) { send(pos, AntarchyGamePayload.BASILISK_STATE, Integer.toString(Math.max(0, score))); }
|
||||
public static void requestAntmanState(BlockPos pos) { send(pos, AntarchyGamePayload.ANTMAN_STATE, ""); }
|
||||
public static void saveAntmanScore(BlockPos pos, int score) { send(pos, AntarchyGamePayload.ANTMAN_STATE, Integer.toString(Math.max(0, score))); }
|
||||
public static void requestBlockleState(BlockPos pos) { send(pos, AntarchyGamePayload.BLOCKLE_STATE, ""); }
|
||||
public static void submitBlockleGuess(BlockPos pos, String guess) { send(pos, AntarchyGamePayload.BLOCKLE_GUESS, guess); }
|
||||
|
||||
private static void send(BlockPos pos, int action, String value) { sender.accept(new AntarchyGamePayload(pos, action, value)); }
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntarchyGamePayload(BlockPos pos, int action, String value) implements CustomPacketPayload {
|
||||
public static final int BASILISK_STATE = 16;
|
||||
public static final int ANTMAN_STATE = 17;
|
||||
public static final int BLOCKLE_STATE = 18;
|
||||
public static final int BLOCKLE_GUESS = 19;
|
||||
public static final Type<AntarchyGamePayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antarchy_game"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntarchyGamePayload> STREAM_CODEC = StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC, AntarchyGamePayload::pos,
|
||||
ByteBufCodecs.VAR_INT, AntarchyGamePayload::action,
|
||||
ByteBufCodecs.stringUtf8(65536), AntarchyGamePayload::value,
|
||||
AntarchyGamePayload::new
|
||||
);
|
||||
|
||||
public AntarchyGamePayload(BlockPos pos, int action) {
|
||||
this(pos, action, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<AntarchyGamePayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntarchyGameResultPayload(BlockPos pos, boolean success, String data) implements CustomPacketPayload {
|
||||
public static final Type<AntarchyGameResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antarchy_game_result"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntarchyGameResultPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC, AntarchyGameResultPayload::pos,
|
||||
ByteBufCodecs.BOOL, AntarchyGameResultPayload::success,
|
||||
ByteBufCodecs.stringUtf8(65536), AntarchyGameResultPayload::data,
|
||||
AntarchyGameResultPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<AntarchyGameResultPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailDeletePayload(BlockPos pos, String messageId, boolean sent) implements CustomPacketPayload {
|
||||
public static final Type<AntmailDeletePayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_delete"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailDeletePayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailDeletePayload::pos, ByteBufCodecs.stringUtf8(64), AntmailDeletePayload::messageId, ByteBufCodecs.BOOL, AntmailDeletePayload::sent, AntmailDeletePayload::new);
|
||||
@Override public Type<AntmailDeletePayload> type() { return TYPE; }
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailDraftPayload(BlockPos pos, int action, String data) implements CustomPacketPayload {
|
||||
public static final int SAVE = 0;
|
||||
public static final int DELETE = 1;
|
||||
public static final Type<AntmailDraftPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_draft"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailDraftPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailDraftPayload::pos, ByteBufCodecs.VAR_INT, AntmailDraftPayload::action, ByteBufCodecs.stringUtf8(65536), AntmailDraftPayload::data, AntmailDraftPayload::new);
|
||||
@Override public Type<AntmailDraftPayload> type() { return TYPE; }
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailMessageRequestPayload(BlockPos pos, String messageId) implements CustomPacketPayload {
|
||||
public static final Type<AntmailMessageRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_message_request"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailMessageRequestPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC, AntmailMessageRequestPayload::pos,
|
||||
ByteBufCodecs.stringUtf8(64), AntmailMessageRequestPayload::messageId,
|
||||
AntmailMessageRequestPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<AntmailMessageRequestPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailAttachment;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailDebug;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailDraft;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailWire;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class AntmailNetworking {
|
||||
private static Consumer<Object> sender = payload -> { };
|
||||
private static volatile boolean senderConfigured;
|
||||
private static final AtomicBoolean senderErrorLogged = new AtomicBoolean();
|
||||
|
||||
private AntmailNetworking() {
|
||||
}
|
||||
|
||||
public static void setSender(Consumer<Object> sender) {
|
||||
AntmailNetworking.sender = sender;
|
||||
senderConfigured = sender != null;
|
||||
}
|
||||
|
||||
public static void setup(BlockPos pos, String username) {
|
||||
AntmailDebug.log("C2S setup request pos=" + pos + " usernameLength=" + username.length());
|
||||
sendPayload(new AntmailSetupPayload(pos, username));
|
||||
}
|
||||
|
||||
public static void requestState(BlockPos pos) {
|
||||
requestState(pos, AntmailStateRequestPayload.INBOX, 0, 0L);
|
||||
}
|
||||
|
||||
public static void requestState(BlockPos pos, int folder, int page) {
|
||||
requestState(pos, folder, page, 0L);
|
||||
}
|
||||
|
||||
public static void requestState(BlockPos pos, int folder, int page, long knownVersion) {
|
||||
AntmailDebug.log("C2S state request pos=" + pos + " folder=" + folder + " page=" + page + " knownVersion=" + knownVersion);
|
||||
sendPayload(new AntmailStateRequestPayload(pos, folder, page, knownVersion));
|
||||
}
|
||||
|
||||
public static void requestMessage(BlockPos pos, UUID messageId) {
|
||||
sendPayload(new AntmailMessageRequestPayload(pos, messageId.toString()));
|
||||
}
|
||||
|
||||
public static void send(BlockPos pos, String recipient, String subject, String body, List<AntmailAttachment> attachments) {
|
||||
sendPayload(new AntmailSendPayload(pos, recipient, subject, body, AntmailWire.encodeAttachments(attachments)));
|
||||
}
|
||||
|
||||
public static void markRead(BlockPos pos, UUID messageId) {
|
||||
sendPayload(new AntmailReadPayload(pos, messageId.toString(), true));
|
||||
}
|
||||
|
||||
public static void markUnread(BlockPos pos, UUID messageId) {
|
||||
sendPayload(new AntmailReadPayload(pos, messageId.toString(), false));
|
||||
}
|
||||
|
||||
public static void delete(BlockPos pos, UUID messageId, boolean sent) {
|
||||
sendPayload(new AntmailDeletePayload(pos, messageId.toString(), sent));
|
||||
}
|
||||
|
||||
public static void saveDraft(BlockPos pos, AntmailDraft draft) {
|
||||
sendPayload(new AntmailDraftPayload(pos, AntmailDraftPayload.SAVE, AntmailWire.encodeTag(draft.toTag())));
|
||||
}
|
||||
|
||||
public static void deleteDraft(BlockPos pos, UUID draftId) {
|
||||
sendPayload(new AntmailDraftPayload(pos, AntmailDraftPayload.DELETE, draftId.toString()));
|
||||
}
|
||||
|
||||
public static void retry(BlockPos pos, UUID messageId) {
|
||||
sendPayload(new AntmailRetryPayload(pos, messageId.toString()));
|
||||
}
|
||||
|
||||
private static void sendPayload(Object payload) {
|
||||
if (!senderConfigured) {
|
||||
if (senderErrorLogged.compareAndSet(false, true)) AntmailDebug.error("Network sender is not configured; dropping " + payload.getClass().getSimpleName(), new IllegalStateException("AntmailNetworking.setSender was not called"));
|
||||
}
|
||||
else sender.accept(payload);
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class AntmailPayloadTypes {
|
||||
private AntmailPayloadTypes() {
|
||||
}
|
||||
|
||||
public static List<CustomPacketPayload.Type<?>> all() {
|
||||
return List.of(
|
||||
AntmailSetupPayload.TYPE,
|
||||
AntmailStateRequestPayload.TYPE,
|
||||
AntmailMessageRequestPayload.TYPE,
|
||||
AntmailSendPayload.TYPE,
|
||||
AntmailReadPayload.TYPE,
|
||||
AntmailDeletePayload.TYPE,
|
||||
AntmailDraftPayload.TYPE,
|
||||
AntmailRetryPayload.TYPE,
|
||||
AntmailResultPayload.TYPE
|
||||
);
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailReadPayload(BlockPos pos, String messageId, boolean read) implements CustomPacketPayload {
|
||||
public static final Type<AntmailReadPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_read"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailReadPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailReadPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailReadPayload::messageId, ByteBufCodecs.BOOL, AntmailReadPayload::read, AntmailReadPayload::new);
|
||||
@Override public Type<AntmailReadPayload> type() { return TYPE; }
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailResultPayload(BlockPos pos, int status, String address, String messageId, String detail, String data, long version) implements CustomPacketPayload {
|
||||
public static final Type<AntmailResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_result"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailResultPayload> STREAM_CODEC = StreamCodec.of(
|
||||
(buf, payload) -> {
|
||||
BlockPos.STREAM_CODEC.encode(buf, payload.pos());
|
||||
ByteBufCodecs.VAR_INT.encode(buf, payload.status());
|
||||
ByteBufCodecs.stringUtf8(64).encode(buf, payload.address());
|
||||
ByteBufCodecs.stringUtf8(64).encode(buf, payload.messageId());
|
||||
ByteBufCodecs.stringUtf8(128).encode(buf, payload.detail());
|
||||
ByteBufCodecs.stringUtf8(65536).encode(buf, payload.data());
|
||||
ByteBufCodecs.VAR_LONG.encode(buf, payload.version());
|
||||
},
|
||||
buf -> new AntmailResultPayload(
|
||||
BlockPos.STREAM_CODEC.decode(buf),
|
||||
ByteBufCodecs.VAR_INT.decode(buf),
|
||||
ByteBufCodecs.stringUtf8(64).decode(buf),
|
||||
ByteBufCodecs.stringUtf8(64).decode(buf),
|
||||
ByteBufCodecs.stringUtf8(128).decode(buf),
|
||||
ByteBufCodecs.stringUtf8(65536).decode(buf),
|
||||
ByteBufCodecs.VAR_LONG.decode(buf)
|
||||
));
|
||||
@Override public Type<AntmailResultPayload> type() { return TYPE; }
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailRetryPayload(BlockPos pos, String messageId) implements CustomPacketPayload {
|
||||
public static final Type<AntmailRetryPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_retry"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailRetryPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailRetryPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailRetryPayload::messageId, AntmailRetryPayload::new);
|
||||
@Override public Type<AntmailRetryPayload> type() { return TYPE; }
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailSendPayload(BlockPos pos, String recipient, String subject, String body, String attachments) implements CustomPacketPayload {
|
||||
public static final Type<AntmailSendPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_send"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailSendPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSendPayload::pos, ByteBufCodecs.stringUtf8(64), AntmailSendPayload::recipient, ByteBufCodecs.stringUtf8(64), AntmailSendPayload::subject, ByteBufCodecs.stringUtf8(16384), AntmailSendPayload::body, ByteBufCodecs.stringUtf8(65536), AntmailSendPayload::attachments, AntmailSendPayload::new);
|
||||
@Override public Type<AntmailSendPayload> type() { return TYPE; }
|
||||
}
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailAddress;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailDebug;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailAttachment;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailDeliveryResult;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailDraft;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailMessage;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailMailbox;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailServerData;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailValidation;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailWire;
|
||||
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public final class AntmailServerHandler {
|
||||
private static BiConsumer<ServerPlayer, AntmailResultPayload> resultSender = (player, payload) -> { };
|
||||
private static volatile boolean resultSenderConfigured;
|
||||
|
||||
private AntmailServerHandler() {
|
||||
}
|
||||
|
||||
public static void setResultSender(BiConsumer<ServerPlayer, AntmailResultPayload> sender) {
|
||||
resultSender = sender;
|
||||
resultSenderConfigured = sender != null;
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailSetupPayload payload) {
|
||||
AntmailDebug.log("C2S setup received player=" + player.getGameProfile().getName() + " pos=" + payload.pos()
|
||||
+ " usernameLength=" + payload.username().length());
|
||||
ComputerBlockEntity computer = computer(player, payload.pos());
|
||||
if (computer == null || !computer.canUseFileSystem(player)) {
|
||||
AntmailDebug.log("setup rejected: computer missing, out of reach, unauthenticated, or owned by another user");
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "registration_failed:unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailServerData.Registration registration = data.register(player.server, player.serverLevel(), payload.pos(), payload.username());
|
||||
if (registration.valid()) data.setLastUsedAddress(player, registration.address());
|
||||
String mailbox = registration.valid() ? AntmailWire.encodeTag(data.mailboxOrCreate(registration.address()).toTag(false)) : "";
|
||||
String detail = registration.valid() ? "registration_success" : "registration_failed:" + registration.reason();
|
||||
result(player, payload.pos(), registration.valid() ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), registration.valid() ? registration.address().fullAddress() : "", "", detail, mailbox);
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailStateRequestPayload payload) {
|
||||
AntmailDebug.log("state request received player=" + player.getGameProfile().getName() + " pos=" + payload.pos()
|
||||
+ " folder=" + payload.folder() + " page=" + payload.page() + " knownVersion=" + payload.knownVersion());
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
AntmailDebug.log("state request rejected: computer missing, out of reach, unauthenticated, or owned by another user");
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
data.drainAddress(player.server, player.serverLevel(), payload.pos());
|
||||
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (address == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", "", "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
AntmailMailbox mailbox = data.mailboxOrCreate(address);
|
||||
if (payload.knownVersion() == data.mailboxVersion(address)) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "unchanged", "");
|
||||
return;
|
||||
}
|
||||
int folder = payload.folder() < AntmailStateRequestPayload.INBOX || payload.folder() > AntmailStateRequestPayload.DRAFTS
|
||||
? AntmailStateRequestPayload.INBOX : payload.folder();
|
||||
int page = Math.max(0, Math.min(payload.page(), AntmailValidation.MAX_MAILBOX_MESSAGES / AntmailMailbox.PAGE_SIZE));
|
||||
try {
|
||||
String encoded = AntmailWire.encodeTag(mailbox.toPageTag(folder, page));
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "", encoded);
|
||||
} catch (RuntimeException exception) {
|
||||
AntmailDebug.error("Failed to encode state for " + address.fullAddress() + " at " + payload.pos(), exception);
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), "", "state_encode_failed", "");
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailMessageRequestPayload payload) {
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (address == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
UUID id = UUID.fromString(payload.messageId());
|
||||
AntmailMailbox mailbox = data.mailboxOrCreate(address);
|
||||
AntmailMessage message = mailbox.findInbox(id);
|
||||
if (message == null) message = mailbox.findSent(id);
|
||||
if (message == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "not_found", "");
|
||||
return;
|
||||
}
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), payload.messageId(), "message_detail", AntmailWire.encodeTag(message.toTag()));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "invalid_id", "");
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailSendPayload payload) {
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress sender = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (sender == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AntmailAddress recipient = AntmailAddress.parse(payload.recipient());
|
||||
List<AntmailAttachment> attachments = AntmailWire.decodeAttachments(payload.attachments());
|
||||
AntmailMessage message = AntmailMessage.create(sender, recipient, payload.subject(), payload.body(), player.serverLevel().getGameTime(), attachments);
|
||||
AntmailDeliveryResult delivery = data.deliver(player.server, message);
|
||||
result(player, payload.pos(), delivery.status().ordinal(), sender.fullAddress(), message.id().toString(), delivery.detail(), AntmailWire.encodeTag(data.mailboxOrCreate(sender).toTag(false)));
|
||||
} catch (RuntimeException exception) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal(), "", "", "invalid_message", "");
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailReadPayload payload) {
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (address == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
UUID id = UUID.fromString(payload.messageId());
|
||||
AntmailMailbox mailbox = data.mailboxOrCreate(address);
|
||||
boolean changed = payload.read() ? mailbox.markRead(id) : markUnread(mailbox, id);
|
||||
if (changed) data.touchMailbox(address);
|
||||
result(player, payload.pos(), changed ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), changed ? "" : "not_found", AntmailWire.encodeTag(mailbox.toTag(false)));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "invalid_id", "");
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailDeletePayload payload) {
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (address == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
UUID id = UUID.fromString(payload.messageId());
|
||||
AntmailMailbox mailbox = data.mailboxOrCreate(address);
|
||||
boolean changed = payload.sent() ? mailbox.removeSent(id) : mailbox.removeInbox(id);
|
||||
if (changed) data.touchMailbox(address);
|
||||
result(player, payload.pos(), changed ? AntmailDeliveryResult.Status.DELIVERED.ordinal() : AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), changed ? "" : "not_found", AntmailWire.encodeTag(mailbox.toTag(false)));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), address.fullAddress(), payload.messageId(), "invalid_id", "");
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailDraftPayload payload) {
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", "", "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress address = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (address == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", "", "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
AntmailMailbox mailbox = data.mailboxOrCreate(address);
|
||||
try {
|
||||
if (payload.action() == AntmailDraftPayload.SAVE) {
|
||||
AntmailDraft draft = AntmailDraft.fromTag(AntmailWire.decodeTag(payload.data()));
|
||||
AntmailDraft stored = new AntmailDraft(draft.id(), address, draft.recipient(), draft.subject(), draft.body(), draft.attachments());
|
||||
if (!mailbox.saveDraft(stored)) throw new IllegalArgumentException("draft_limit");
|
||||
} else if (payload.action() == AntmailDraftPayload.DELETE) {
|
||||
if (!mailbox.removeDraft(UUID.fromString(payload.data()))) throw new IllegalArgumentException("not_found");
|
||||
} else {
|
||||
throw new IllegalArgumentException("invalid_action");
|
||||
}
|
||||
data.touchMailbox(address);
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.DELIVERED.ordinal(), address.fullAddress(), "", "draft_saved", AntmailWire.encodeTag(mailbox.toTag(false)));
|
||||
} catch (RuntimeException exception) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal(), address.fullAddress(), "", "draft_failed", AntmailWire.encodeTag(mailbox.toTag(false)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, AntmailRetryPayload payload) {
|
||||
ComputerBlockEntity computer = authorizedComputer(player, payload.pos());
|
||||
if (computer == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.FAILED.ordinal(), "", payload.messageId(), "unauthorized", "");
|
||||
return;
|
||||
}
|
||||
AntmailServerData data = AntmailServerData.access(player.server);
|
||||
AntmailAddress sender = data.addressAt(player.serverLevel().dimension().location(), payload.pos());
|
||||
if (sender == null) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.ADDRESS_NOT_FOUND.ordinal(), "", payload.messageId(), "unconfigured", "");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AntmailDeliveryResult delivery = data.retry(player.server, sender, UUID.fromString(payload.messageId()));
|
||||
AntmailMailbox mailbox = data.mailboxOrCreate(sender);
|
||||
result(player, payload.pos(), delivery == null ? AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal() : delivery.status().ordinal(), sender.fullAddress(), payload.messageId(), delivery == null ? "not_found" : delivery.detail(), AntmailWire.encodeTag(mailbox.toTag(false)));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
result(player, payload.pos(), AntmailDeliveryResult.Status.MESSAGE_INVALID.ordinal(), sender.fullAddress(), payload.messageId(), "invalid_id", "");
|
||||
}
|
||||
}
|
||||
|
||||
private static ComputerBlockEntity authorizedComputer(ServerPlayer player, BlockPos pos) {
|
||||
ComputerBlockEntity computer = computer(player, pos);
|
||||
return computer != null && computer.canUseFileSystem(player) ? computer : null;
|
||||
}
|
||||
|
||||
private static boolean markUnread(AntmailMailbox mailbox, UUID id) {
|
||||
for (com.craisinlord.antarchy.content.antmail.AntmailMessage message : mailbox.inbox()) {
|
||||
if (message.id().equals(id)) {
|
||||
message.markUnread();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ComputerBlockEntity computer(ServerPlayer player, BlockPos pos) {
|
||||
if (!player.serverLevel().hasChunkAt(pos) || player.distanceToSqr(pos.getX() + .5D, pos.getY() + .5D, pos.getZ() + .5D) > 64D) return null;
|
||||
BlockEntity blockEntity = player.serverLevel().getBlockEntity(pos);
|
||||
return blockEntity instanceof ComputerBlockEntity computer ? computer : null;
|
||||
}
|
||||
|
||||
private static void result(ServerPlayer player, BlockPos pos, int status, String address, String messageId, String detail, String data) {
|
||||
long version = 0L;
|
||||
try {
|
||||
if (!address.isBlank()) version = AntmailServerData.access(player.server).mailboxVersion(AntmailAddress.parse(address));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
AntmailDebug.log("sending result player=" + player.getGameProfile().getName() + " pos=" + pos + " status=" + status
|
||||
+ " detail=" + detail + " addressPresent=" + !address.isBlank() + " dataChars=" + data.length() + " version=" + version);
|
||||
if (!resultSenderConfigured) {
|
||||
AntmailDebug.error("Result sender is not configured; dropping response at " + pos, new IllegalStateException("AntmailServerHandler.setResultSender was not called"));
|
||||
return;
|
||||
}
|
||||
resultSender.accept(player, new AntmailResultPayload(pos, status, address, messageId, detail, data, version));
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailSetupPayload(BlockPos pos, String username) implements CustomPacketPayload {
|
||||
public static final Type<AntmailSetupPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_setup"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailSetupPayload> STREAM_CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, AntmailSetupPayload::pos, ByteBufCodecs.stringUtf8(32), AntmailSetupPayload::username, AntmailSetupPayload::new);
|
||||
@Override public Type<AntmailSetupPayload> type() { return TYPE; }
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record AntmailStateRequestPayload(BlockPos pos, int folder, int page, long knownVersion) implements CustomPacketPayload {
|
||||
public static final int INBOX = 0;
|
||||
public static final int SENT = 1;
|
||||
public static final int DRAFTS = 2;
|
||||
public static final Type<AntmailStateRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "antmail_state_request"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AntmailStateRequestPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC, AntmailStateRequestPayload::pos,
|
||||
ByteBufCodecs.VAR_INT, AntmailStateRequestPayload::folder,
|
||||
ByteBufCodecs.VAR_INT, AntmailStateRequestPayload::page,
|
||||
ByteBufCodecs.VAR_LONG, AntmailStateRequestPayload::knownVersion,
|
||||
AntmailStateRequestPayload::new);
|
||||
@Override public Type<AntmailStateRequestPayload> type() { return TYPE; }
|
||||
}
|
||||
-455
@@ -1,455 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.content.block.entity.ComputerBlockEntity;
|
||||
import com.craisinlord.antarchy.content.computer.ComputerFileSystem;
|
||||
import com.craisinlord.antarchy.content.computer.terminal.TerminalCommandService;
|
||||
import com.craisinlord.antarchy.content.computer.terminal.TerminalFileSystem;
|
||||
import com.craisinlord.antarchy.content.computer.terminal.TerminalResult;
|
||||
import com.craisinlord.antarchy.config.AntarchySettings;
|
||||
import com.craisinlord.antarchy.content.antmail.AntmailWire;
|
||||
import com.craisinlord.antarchy.content.computer.blockle.BlockleAnswers;
|
||||
import com.craisinlord.antarchy.content.computer.blockle.BlockleDictionary;
|
||||
import com.craisinlord.antarchy.content.computer.blockle.BlockleGame;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public final class ComputerAccessHandler {
|
||||
private static BiConsumer<ServerPlayer, ComputerAccessResultPayload> resultSender = (player, result) -> {
|
||||
};
|
||||
|
||||
private ComputerAccessHandler() {
|
||||
}
|
||||
|
||||
public static void setResultSender(BiConsumer<ServerPlayer, ComputerAccessResultPayload> sender) {
|
||||
resultSender = sender;
|
||||
}
|
||||
|
||||
public static void handle(ServerPlayer player, ComputerAccessPayload payload) {
|
||||
if (payload.value().length() > 65536 || player.level().isClientSide || !player.serverLevel().hasChunkAt(payload.pos()) ||
|
||||
player.distanceToSqr(payload.pos().getX() + 0.5D, payload.pos().getY() + 0.5D, payload.pos().getZ() + 0.5D) > 64.0D) {
|
||||
return;
|
||||
}
|
||||
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
|
||||
if (!(blockEntity instanceof ComputerBlockEntity computer)) {
|
||||
send(player, payload, ComputerAccessResultPayload.INVALID);
|
||||
return;
|
||||
}
|
||||
switch (payload.action()) {
|
||||
case ComputerAccessPayload.OPEN -> open(player, computer, payload);
|
||||
case ComputerAccessPayload.SETUP -> setup(player, computer, payload);
|
||||
case ComputerAccessPayload.LOGIN -> login(player, computer, payload);
|
||||
case ComputerAccessPayload.LOGOUT -> {
|
||||
computer.logout();
|
||||
send(player, payload, ComputerAccessResultPayload.READY);
|
||||
}
|
||||
case ComputerAccessPayload.CLOSE -> {
|
||||
computer.releaseUser(player);
|
||||
send(player, payload, computer.isAuthenticated() ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.READY);
|
||||
}
|
||||
case ComputerAccessPayload.CHANGE_PASSWORD -> {
|
||||
boolean changed = computer.setPassword(player, payload.value());
|
||||
send(player, payload, changed ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID_PASSWORD);
|
||||
}
|
||||
case ComputerAccessPayload.EJECT -> eject(player, computer, payload);
|
||||
case ComputerAccessPayload.FILE_LIST -> fileList(player, computer, payload);
|
||||
case ComputerAccessPayload.FILE_OPEN -> fileOpen(player, computer, payload);
|
||||
case ComputerAccessPayload.FILE_CREATE -> fileCreate(player, computer, payload);
|
||||
case ComputerAccessPayload.FILE_SAVE -> fileSave(player, computer, payload);
|
||||
case ComputerAccessPayload.FILE_DELETE -> fileDelete(player, computer, payload);
|
||||
case ComputerAccessPayload.FILE_MOVE -> fileMove(player, computer, payload);
|
||||
case ComputerAccessPayload.TERMINAL_COMMAND -> terminalCommand(player, computer, payload);
|
||||
case ComputerAccessPayload.DESKTOP_STATE -> desktopState(player, computer, payload);
|
||||
case ComputerAccessPayload.DESKTOP_WALLPAPER -> desktopWallpaper(player, computer, payload);
|
||||
case ComputerAccessPayload.BASILISK_STATE -> basiliskState(player, computer, payload);
|
||||
case ComputerAccessPayload.ANTMAN_STATE -> antmanState(player, computer, payload);
|
||||
case ComputerAccessPayload.BLOCKLE_STATE -> blockleState(player, computer, payload, false);
|
||||
case ComputerAccessPayload.BLOCKLE_GUESS -> blockleState(player, computer, payload, true);
|
||||
case ComputerAccessPayload.LOCATE_STRUCTURE -> locateStructure(player, computer, payload);
|
||||
default -> send(player, payload, ComputerAccessResultPayload.INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
private static void open(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (computer.hasActiveUser() && !computer.isAuthenticatedBy(player)) {
|
||||
send(player, payload, ComputerAccessResultPayload.BUSY);
|
||||
sendArchive(player, computer, payload);
|
||||
return;
|
||||
}
|
||||
if (!computer.hasPassword()) {
|
||||
computer.claimUser(player);
|
||||
send(player, payload, ComputerAccessResultPayload.SETUP_REQUIRED);
|
||||
} else if (computer.isAuthenticated()) {
|
||||
computer.claimUser(player);
|
||||
send(player, payload, ComputerAccessResultPayload.SUCCESS);
|
||||
} else {
|
||||
send(player, payload, ComputerAccessResultPayload.READY);
|
||||
}
|
||||
sendArchive(player, computer, payload);
|
||||
}
|
||||
|
||||
private static void sendArchive(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), ComputerAccessResultPayload.SUCCESS,
|
||||
computer.hasPassword(), computer.isAuthenticated(), ComputerAccessPayload.ARCHIVE_STATE + "\0\0"
|
||||
+ com.craisinlord.antarchy.content.guide.ComputerGuideData.encodeNetworkSnapshot()));
|
||||
}
|
||||
|
||||
private static void setup(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.hasPassword() && computer.initializePassword(player, payload.value())) {
|
||||
send(player, payload, ComputerAccessResultPayload.SUCCESS);
|
||||
} else {
|
||||
send(player, payload, computer.hasActiveUser() ? ComputerAccessResultPayload.BUSY : ComputerAccessResultPayload.INVALID_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private static void login(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (computer.hasActiveUser() && !computer.isAuthenticatedBy(player)) {
|
||||
send(player, payload, ComputerAccessResultPayload.BUSY);
|
||||
} else if (computer.authenticate(player, payload.value())) {
|
||||
com.craisinlord.antarchy.content.antmail.AntmailServerData data = com.craisinlord.antarchy.content.antmail.AntmailServerData.access(player.server);
|
||||
net.minecraft.resources.ResourceLocation dimension = player.serverLevel().dimension().location();
|
||||
com.craisinlord.antarchy.content.antmail.AntmailAddress address = data.addressAt(dimension, computer.getBlockPos());
|
||||
if (address != null) data.setLastUsedAddress(player, address);
|
||||
send(player, payload, ComputerAccessResultPayload.SUCCESS);
|
||||
} else {
|
||||
send(player, payload, ComputerAccessResultPayload.INVALID_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private static void eject(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
try {
|
||||
ResourceLocation diskId = ResourceLocation.parse(payload.value());
|
||||
boolean ejected = computer.ejectOne(player, diskId);
|
||||
send(player, payload, ejected ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID);
|
||||
} catch (Exception ignored) {
|
||||
send(player, payload, ComputerAccessResultPayload.INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
private static void fileList(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
StringBuilder data = new StringBuilder();
|
||||
for (ComputerFileSystem.ComputerFile file : computer.fileSystem().list()) {
|
||||
if (!data.isEmpty()) data.append('\n');
|
||||
data.append(file.type().name()).append('\t').append(file.path()).append('\t').append(file.contents().length());
|
||||
}
|
||||
sendFile(player, payload, true, data.toString(), "");
|
||||
}
|
||||
|
||||
private static void fileOpen(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
ComputerFileSystem.ComputerFile file = computer.fileSystem().get(payload.value());
|
||||
if (file == null) {
|
||||
sendFile(player, payload, false, "", "not_found");
|
||||
} else if (file.type() != ComputerFileSystem.ComputerFile.Type.TEXT && file.type() != ComputerFileSystem.ComputerFile.Type.IMAGE) {
|
||||
sendFile(player, payload, false, "", "not_file");
|
||||
} else {
|
||||
sendFile(player, payload, true, file.path() + "\0" + file.contents(), "");
|
||||
}
|
||||
}
|
||||
|
||||
private static void fileCreate(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
String[] request = splitFileRequest(payload.value());
|
||||
if (request == null) {
|
||||
sendFile(player, payload, false, "", "invalid_request");
|
||||
return;
|
||||
}
|
||||
ComputerFileSystem.Result result = computer.fileSystem().createTextFile(request[0], request[1]);
|
||||
if (result.successful()) computer.setChanged();
|
||||
sendFile(player, payload, result.successful(), request[0], result.error());
|
||||
}
|
||||
|
||||
private static void fileSave(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
String[] request = splitFileRequest(payload.value());
|
||||
if (request == null) {
|
||||
sendFile(player, payload, false, "", "invalid_request");
|
||||
return;
|
||||
}
|
||||
ComputerFileSystem.Result result = computer.fileSystem().writeTextFile(request[0], request[1]);
|
||||
if (result.successful()) computer.setChanged();
|
||||
sendFile(player, payload, result.successful(), request[0], result.error());
|
||||
}
|
||||
|
||||
private static void fileDelete(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
ComputerFileSystem.Result result = computer.fileSystem().delete(payload.value());
|
||||
if (result.successful()) computer.setChanged();
|
||||
sendFile(player, payload, result.successful(), payload.value(), result.error());
|
||||
}
|
||||
|
||||
private static void fileMove(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
String[] request = splitFileRequest(payload.value());
|
||||
if (request == null) {
|
||||
sendFile(player, payload, false, "", "invalid_request");
|
||||
return;
|
||||
}
|
||||
ComputerFileSystem.Result result = computer.fileSystem().move(request[0], request[1]);
|
||||
if (result.successful()) computer.setChanged();
|
||||
sendFile(player, payload, result.successful(), request[1], result.error());
|
||||
}
|
||||
|
||||
private static void terminalCommand(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
String[] request = splitFileRequest(payload.value());
|
||||
if (request == null) {
|
||||
sendTerminal(player, payload, TerminalResult.error("/", "ERROR: INVALID REQUEST"));
|
||||
return;
|
||||
}
|
||||
TerminalResult result = new TerminalCommandService().execute(new ComputerTerminalFileSystem(computer.fileSystem()), request[0], request[1]);
|
||||
if (result.status() != TerminalResult.Status.SUCCESS || !result.lines().isEmpty() || result.clearOutput() || result.openPath() != null) computer.setChanged();
|
||||
sendTerminal(player, payload, result);
|
||||
}
|
||||
|
||||
private static void sendTerminal(ServerPlayer player, ComputerAccessPayload payload, TerminalResult result) {
|
||||
net.minecraft.nbt.CompoundTag tag = new net.minecraft.nbt.CompoundTag();
|
||||
tag.putString("Directory", result.workingDirectory());
|
||||
tag.putBoolean("Clear", result.clearOutput());
|
||||
tag.putString("Open", result.openPath() == null ? "" : result.openPath());
|
||||
net.minecraft.nbt.ListTag lines = new net.minecraft.nbt.ListTag();
|
||||
for (String line : result.lines()) lines.add(net.minecraft.nbt.StringTag.valueOf(line));
|
||||
tag.put("Lines", lines);
|
||||
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), result.status() == TerminalResult.Status.ERROR ? ComputerAccessResultPayload.INVALID : ComputerAccessResultPayload.SUCCESS,
|
||||
true, true, payload.action() + "\0" + AntmailWire.encodeTag(tag)));
|
||||
}
|
||||
|
||||
private static final class ComputerTerminalFileSystem implements TerminalFileSystem {
|
||||
private final ComputerFileSystem fileSystem;
|
||||
|
||||
private ComputerTerminalFileSystem(ComputerFileSystem fileSystem) { this.fileSystem = fileSystem; }
|
||||
|
||||
@Override public java.util.Optional<Entry> find(String path, String workingDirectory) {
|
||||
ComputerFileSystem.ComputerFile file = fileSystem.get(resolve(path, workingDirectory));
|
||||
return file == null ? java.util.Optional.empty() : java.util.Optional.of(new Entry(file.path(), file.path(), file.type() == ComputerFileSystem.ComputerFile.Type.DIRECTORY ? EntryType.DIRECTORY : EntryType.FILE));
|
||||
}
|
||||
@Override public java.util.List<Entry> list(String path, String workingDirectory) {
|
||||
String directory = resolve(path, workingDirectory);
|
||||
return fileSystem.list().stream().filter(file -> {
|
||||
String parent = file.path().lastIndexOf('/') <= 0 ? "/" : file.path().substring(0, file.path().lastIndexOf('/'));
|
||||
return parent.equals(directory) && !file.path().equals(directory);
|
||||
}).map(file -> new Entry(file.path().substring(file.path().lastIndexOf('/') + 1), file.path(), file.type() == ComputerFileSystem.ComputerFile.Type.DIRECTORY ? EntryType.DIRECTORY : EntryType.FILE)).toList();
|
||||
}
|
||||
@Override public boolean createDirectory(String path, String workingDirectory) { return fileSystem.createDirectory(resolve(path, workingDirectory)).successful(); }
|
||||
@Override public boolean createFile(String path, String workingDirectory) { return fileSystem.createTextFile(resolve(path, workingDirectory), "").successful(); }
|
||||
@Override public java.util.Optional<String> readText(String path, String workingDirectory) { ComputerFileSystem.ComputerFile file = fileSystem.get(resolve(path, workingDirectory)); return file != null && file.type() == ComputerFileSystem.ComputerFile.Type.TEXT ? java.util.Optional.of(file.contents()) : java.util.Optional.empty(); }
|
||||
@Override public boolean writeText(String path, String workingDirectory, String contents) { return fileSystem.createOrWriteTextFile(resolve(path, workingDirectory), contents).successful(); }
|
||||
@Override public boolean deleteFile(String path, String workingDirectory) { return fileSystem.delete(resolve(path, workingDirectory)).successful(); }
|
||||
@Override public boolean deleteEmptyDirectory(String path, String workingDirectory) { return fileSystem.delete(resolve(path, workingDirectory)).successful(); }
|
||||
@Override public boolean move(String source, String destination, String workingDirectory) { return fileSystem.move(resolve(source, workingDirectory), resolve(destination, workingDirectory)).successful(); }
|
||||
@Override public String normalize(String path, String workingDirectory) { return resolve(path, workingDirectory); }
|
||||
|
||||
private String resolve(String path, String workingDirectory) {
|
||||
if (path == null || path.isBlank()) return ComputerFileSystem.normalize(workingDirectory);
|
||||
return ComputerFileSystem.normalize(path.startsWith("/") ? path : ComputerFileSystem.normalize(workingDirectory) + "/" + path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void desktopState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendDesktop(player, payload, false, "unauthorized");
|
||||
return;
|
||||
}
|
||||
sendDesktop(player, payload, true, AntmailWire.encodeTag(computer.desktopState().save()));
|
||||
}
|
||||
|
||||
private static void desktopWallpaper(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendDesktop(player, payload, false, "unauthorized");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
boolean changed = computer.selectWallpaper(ResourceLocation.parse(payload.value()));
|
||||
sendDesktop(player, payload, changed, changed ? "" : "wallpaper_locked");
|
||||
} catch (RuntimeException exception) {
|
||||
sendDesktop(player, payload, false, "invalid_wallpaper");
|
||||
}
|
||||
}
|
||||
|
||||
private static void basiliskState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
if (!payload.value().isBlank()) {
|
||||
try {
|
||||
computer.setBasiliskScore(Integer.parseInt(payload.value()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
sendFile(player, payload, false, "", "invalid_score");
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendFile(player, payload, true, computer.basiliskScore() + "\0" + computer.basiliskHighScore(), "");
|
||||
}
|
||||
|
||||
private static void antmanState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
if (!payload.value().isBlank()) {
|
||||
try {
|
||||
computer.setAntmanScore(Integer.parseInt(payload.value()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
sendFile(player, payload, false, "", "invalid_score");
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendFile(player, payload, true, computer.antmanScore() + "\0" + computer.antmanHighScore(), "");
|
||||
}
|
||||
|
||||
private static void blockleState(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload, boolean submit) {
|
||||
if (!computer.canUseFileSystem(player)) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
ResourceLocation disk = ResourceLocation.fromNamespaceAndPath("antarchy", "blockle_game");
|
||||
if (!AntarchySettings.unlockAllArchives() && !computer.diskIds().contains(disk)) {
|
||||
sendFile(player, payload, false, "", "not_installed");
|
||||
return;
|
||||
}
|
||||
ServerLevel overworld = player.server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) {
|
||||
sendFile(player, payload, false, "", "overworld_unavailable");
|
||||
return;
|
||||
}
|
||||
long day = overworld.getDayTime() / 24000L;
|
||||
computer.resetBlockle(day);
|
||||
BlockleAnswers.Answer answer = BlockleAnswers.answerForDay(day);
|
||||
if (answer == null) {
|
||||
sendFile(player, payload, false, "", "answers_unavailable");
|
||||
return;
|
||||
}
|
||||
if (submit) {
|
||||
String guess = payload.value() == null ? "" : payload.value().toLowerCase(java.util.Locale.ROOT);
|
||||
if (guess.length() != 5 || !guess.chars().allMatch(value -> value >= 'a' && value <= 'z')) {
|
||||
sendFile(player, payload, false, "", "invalid_word");
|
||||
return;
|
||||
}
|
||||
if (!BlockleDictionary.contains(guess)) {
|
||||
sendFile(player, payload, false, "", "invalid_word");
|
||||
return;
|
||||
}
|
||||
if (computer.blockleGuesses().size() >= 6 || computer.blockleGuesses().stream().anyMatch(value -> value.equals(answer.word()))) {
|
||||
sendFile(player, payload, false, "", "game_over");
|
||||
return;
|
||||
}
|
||||
computer.addBlockleGuess(guess);
|
||||
}
|
||||
sendFile(player, payload, true, encodeBlockle(day, answer, computer.blockleGuesses()), "");
|
||||
}
|
||||
|
||||
private static void locateStructure(ServerPlayer player, ComputerBlockEntity computer, ComputerAccessPayload payload) {
|
||||
ResourceLocation entryId;
|
||||
try {
|
||||
entryId = ResourceLocation.parse(payload.value());
|
||||
} catch (RuntimeException exception) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
var entry = com.craisinlord.antarchy.content.guide.ComputerGuideData.entry(entryId);
|
||||
boolean unlocked = com.craisinlord.antarchy.content.guide.ComputerGuideData.entriesFor(computer.diskIds()).stream()
|
||||
.anyMatch(candidate -> candidate.id().equals(entryId));
|
||||
if (!computer.canUseFileSystem(player) || !unlocked || entry == null || entry.structureId().isBlank()
|
||||
|| entry.structureTagId().isBlank() || entry.dimensionId().isBlank() || entry.searchRadius() <= 0) {
|
||||
sendFile(player, payload, false, "", "unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceLocation dimensionId;
|
||||
ResourceLocation structureTagId;
|
||||
try {
|
||||
dimensionId = ResourceLocation.parse(entry.dimensionId());
|
||||
structureTagId = ResourceLocation.parse(entry.structureTagId());
|
||||
} catch (RuntimeException exception) {
|
||||
sendFile(player, payload, false, "", "dimension_unavailable");
|
||||
return;
|
||||
}
|
||||
ServerLevel targetLevel = player.server.getLevel(net.minecraft.resources.ResourceKey.create(Registries.DIMENSION, dimensionId));
|
||||
if (targetLevel == null) {
|
||||
sendFile(player, payload, false, "", "dimension_unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
TagKey<Structure> structures = TagKey.create(Registries.STRUCTURE, structureTagId);
|
||||
net.minecraft.core.BlockPos origin = computer.getBlockPos();
|
||||
var nearest = targetLevel.findNearestMapStructure(structures, origin, entry.searchRadius(), false);
|
||||
if (nearest == null) {
|
||||
sendFile(player, payload, false, "", "not_found");
|
||||
return;
|
||||
}
|
||||
|
||||
sendFile(player, payload, true, nearest.getX() + ", " + nearest.getY() + ", " + nearest.getZ(), "");
|
||||
}
|
||||
|
||||
private static String encodeBlockle(long day, BlockleAnswers.Answer answer, java.util.List<String> guesses) {
|
||||
StringBuilder result = new StringBuilder(Long.toString(day));
|
||||
boolean solved = false;
|
||||
for (String guess : guesses) {
|
||||
String colors = BlockleGame.evaluate(answer.word(), guess);
|
||||
if (guess.equals(answer.word())) solved = true;
|
||||
result.append('|').append(guess).append(',').append(colors);
|
||||
}
|
||||
if (solved || guesses.size() >= 6) {
|
||||
result.append('|').append(solved ? "SOLVED" : "FAILED").append('|').append(answer.word()).append('|').append(answer.itemId());
|
||||
} else {
|
||||
result.append('|').append("PLAYING");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static void sendDesktop(ServerPlayer player, ComputerAccessPayload payload, boolean success, String data) {
|
||||
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
|
||||
boolean hasPassword = blockEntity instanceof ComputerBlockEntity computer && computer.hasPassword();
|
||||
boolean authenticated = blockEntity instanceof ComputerBlockEntity computer && computer.isAuthenticated();
|
||||
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), success ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID,
|
||||
hasPassword, authenticated, payload.action() + "\0\0" + data));
|
||||
}
|
||||
|
||||
private static String[] splitFileRequest(String value) {
|
||||
if (value == null) return null;
|
||||
int separator = value.indexOf('\0');
|
||||
if (separator < 0) return null;
|
||||
return new String[]{value.substring(0, separator), value.substring(separator + 1)};
|
||||
}
|
||||
|
||||
private static void send(ServerPlayer player, ComputerAccessPayload payload, int result) {
|
||||
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
|
||||
boolean hasPassword = blockEntity instanceof ComputerBlockEntity computer && computer.hasPassword();
|
||||
boolean authenticated = blockEntity instanceof ComputerBlockEntity computer && computer.isAuthenticated();
|
||||
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), result, hasPassword, authenticated));
|
||||
}
|
||||
|
||||
private static void sendFile(ServerPlayer player, ComputerAccessPayload payload, boolean success, String data, String error) {
|
||||
BlockEntity blockEntity = player.serverLevel().getBlockEntity(payload.pos());
|
||||
boolean hasPassword = blockEntity instanceof ComputerBlockEntity computer && computer.hasPassword();
|
||||
boolean authenticated = blockEntity instanceof ComputerBlockEntity computer && computer.isAuthenticated();
|
||||
resultSender.accept(player, new ComputerAccessResultPayload(payload.pos(), success ? ComputerAccessResultPayload.SUCCESS : ComputerAccessResultPayload.INVALID,
|
||||
hasPassword, authenticated, payload.action() + "\0" + error + "\0" + data));
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record ComputerAccessPayload(BlockPos pos, int action, String value) implements CustomPacketPayload {
|
||||
public static final int OPEN = 0;
|
||||
public static final int SETUP = 1;
|
||||
public static final int LOGIN = 2;
|
||||
public static final int LOGOUT = 3;
|
||||
public static final int CHANGE_PASSWORD = 4;
|
||||
public static final int EJECT = 5;
|
||||
public static final int CLOSE = 6;
|
||||
public static final int FILE_LIST = 7;
|
||||
public static final int FILE_OPEN = 8;
|
||||
public static final int FILE_CREATE = 9;
|
||||
public static final int FILE_SAVE = 10;
|
||||
public static final int DESKTOP_STATE = 11;
|
||||
public static final int DESKTOP_WALLPAPER = 12;
|
||||
public static final int FILE_DELETE = 13;
|
||||
public static final int FILE_MOVE = 14;
|
||||
public static final int TERMINAL_COMMAND = 15;
|
||||
public static final int BASILISK_STATE = 16;
|
||||
public static final int ANTMAN_STATE = 17;
|
||||
public static final int BLOCKLE_STATE = 18;
|
||||
public static final int BLOCKLE_GUESS = 19;
|
||||
public static final int ARCHIVE_STATE = 20;
|
||||
public static final int LOCATE_STRUCTURE = 21;
|
||||
public static final Type<ComputerAccessPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerAccessPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC, ComputerAccessPayload::pos,
|
||||
ByteBufCodecs.VAR_INT, ComputerAccessPayload::action,
|
||||
ByteBufCodecs.stringUtf8(65536), ComputerAccessPayload::value,
|
||||
ComputerAccessPayload::new
|
||||
);
|
||||
|
||||
public ComputerAccessPayload(BlockPos pos, int action) {
|
||||
this(pos, action, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<ComputerAccessPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package com.craisinlord.antarchy.content.network;
|
||||
|
||||
import com.craisinlord.antarchy.Antarchy;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public record ComputerAccessResultPayload(BlockPos pos, int result, boolean hasPassword, boolean authenticated, String data) implements CustomPacketPayload {
|
||||
public static final int SETUP_REQUIRED = 0;
|
||||
public static final int READY = 1;
|
||||
public static final int SUCCESS = 2;
|
||||
public static final int INVALID_PASSWORD = 3;
|
||||
public static final int BUSY = 4;
|
||||
public static final int INVALID = 5;
|
||||
public static final Type<ComputerAccessResultPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(Antarchy.MODID, "computer_access_result"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ComputerAccessResultPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
BlockPos.STREAM_CODEC, ComputerAccessResultPayload::pos,
|
||||
ByteBufCodecs.VAR_INT, ComputerAccessResultPayload::result,
|
||||
ByteBufCodecs.BOOL, ComputerAccessResultPayload::hasPassword,
|
||||
ByteBufCodecs.BOOL, ComputerAccessResultPayload::authenticated,
|
||||
ByteBufCodecs.stringUtf8(65536), ComputerAccessResultPayload::data,
|
||||
ComputerAccessResultPayload::new
|
||||
);
|
||||
|
||||
public ComputerAccessResultPayload(BlockPos pos, int result, boolean hasPassword, boolean authenticated) {
|
||||
this(pos, result, hasPassword, authenticated, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<ComputerAccessResultPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 262 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user