diff --git a/gradle.properties b/gradle.properties index ae5d5b0..b909e31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ loader_version=0.14.21 #Fabric api fabric_version=0.84.0+1.20.1 -mod_version = 1.0.0-beta.5 +mod_version = 1.0.0-beta.6 maven_group = io.github.foundationgames archives_base_name = phonos diff --git a/src/main/java/io/github/foundationgames/phonos/Phonos.java b/src/main/java/io/github/foundationgames/phonos/Phonos.java index 2bed42b..d98c34a 100644 --- a/src/main/java/io/github/foundationgames/phonos/Phonos.java +++ b/src/main/java/io/github/foundationgames/phonos/Phonos.java @@ -12,6 +12,7 @@ import io.github.foundationgames.phonos.sound.emitter.SoundEmitterStorage; import io.github.foundationgames.phonos.sound.stream.ServerOutgoingStreamHandler; import io.github.foundationgames.phonos.util.PhonosUtil; +import io.github.foundationgames.phonos.world.command.PhonosCommands; import io.github.foundationgames.phonos.world.sound.InputPlugPoint; import io.github.foundationgames.phonos.world.sound.data.SoundDataTypes; import net.fabricmc.api.ModInitializer; @@ -110,6 +111,7 @@ public void onInitialize() { }); RadioStorage.init(); + PhonosCommands.init(); } public static Identifier id(String path) { diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/SatelliteStationBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/SatelliteStationBlockEntity.java index fbddba8..30eab77 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/entity/SatelliteStationBlockEntity.java +++ b/src/main/java/io/github/foundationgames/phonos/block/entity/SatelliteStationBlockEntity.java @@ -213,7 +213,7 @@ protected int getComparatorOutput(int timer) { return 0; } - return MathHelper.clamp(Math.ceil(15 * timer / (float)this.playDuration), 0, 15); + return MathHelper.clamp((int) Math.ceil(15f * timer / this.playDuration), 0, 15); } public Vec3d launchpadPos() { diff --git a/src/main/java/io/github/foundationgames/phonos/sound/custom/ServerCustomAudio.java b/src/main/java/io/github/foundationgames/phonos/sound/custom/ServerCustomAudio.java index 790e50b..16678b4 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/custom/ServerCustomAudio.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/custom/ServerCustomAudio.java @@ -137,6 +137,8 @@ public static void save(Path folder) throws IOException { doNotDelete.add(id); } + Phonos.LOG.info("Saved all custom audio to /phonos/"); + try (var paths = Files.walk(folder, 1)) { for (var path : paths.toList()) { var filename = path.getFileName().toString(); @@ -147,6 +149,7 @@ public static void save(Path folder) throws IOException { long id = Long.parseUnsignedLong(hexStr, 16); if (!doNotDelete.contains(id)) { Files.deleteIfExists(path); + Phonos.LOG.info("Deleted unused custom audio with ID {}", Long.toHexString(id)); } } catch (NumberFormatException ignored) {} diff --git a/src/main/java/io/github/foundationgames/phonos/util/PhonosUtil.java b/src/main/java/io/github/foundationgames/phonos/util/PhonosUtil.java index 1dc4280..3d98a7a 100644 --- a/src/main/java/io/github/foundationgames/phonos/util/PhonosUtil.java +++ b/src/main/java/io/github/foundationgames/phonos/util/PhonosUtil.java @@ -31,6 +31,8 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Path; +import java.text.NumberFormat; +import java.util.Locale; import java.util.function.IntFunction; public enum PhonosUtil {; @@ -141,6 +143,26 @@ public static int readInt(InputStream stream) throws IOException { return r; } + public static String duration(int seconds) { + var fmt = NumberFormat.getInstance(Locale.ROOT); + fmt.setMinimumIntegerDigits(2); + + if (seconds < 60) { + return "0:" + fmt.format(seconds); + } + + int sec = seconds % 60; + int min = (seconds / 60) % 60; + + if (seconds < 3600) { + return min + ":" + fmt.format(sec); + } + + int hr = seconds / 3600; + + return hr + ":" + fmt.format(min) + ":" + fmt.format(sec); + } + public static Path getCustomSoundFolder(MinecraftServer server) { return server.getSavePath(WorldSavePath.ROOT).resolve("phonos"); } diff --git a/src/main/java/io/github/foundationgames/phonos/world/RadarPoints.java b/src/main/java/io/github/foundationgames/phonos/world/RadarPoints.java index 845c5bf..019c3ae 100644 --- a/src/main/java/io/github/foundationgames/phonos/world/RadarPoints.java +++ b/src/main/java/io/github/foundationgames/phonos/world/RadarPoints.java @@ -26,6 +26,10 @@ public void remove(int channel, BlockPos pos) { } } + public LongSet getPoints(int channel) { + return channelToSources.get(channel); + } + public static RadarPoints get(ServerWorld world) { return world.getPersistentStateManager().getOrCreate(RadarPoints::readNbt, RadarPoints::new, "phonos_radar_points"); } diff --git a/src/main/java/io/github/foundationgames/phonos/world/command/PhonosCommands.java b/src/main/java/io/github/foundationgames/phonos/world/command/PhonosCommands.java new file mode 100644 index 0000000..7c5051f --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/world/command/PhonosCommands.java @@ -0,0 +1,180 @@ +package io.github.foundationgames.phonos.world.command; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.github.foundationgames.phonos.Phonos; +import io.github.foundationgames.phonos.block.entity.SatelliteStationBlockEntity; +import io.github.foundationgames.phonos.radio.RadioStorage; +import io.github.foundationgames.phonos.sound.custom.ServerCustomAudio; +import io.github.foundationgames.phonos.util.PhonosUtil; +import io.github.foundationgames.phonos.world.RadarPoints; +import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.argument.BlockPosArgumentType; +import net.minecraft.command.argument.PosArgument; +import net.minecraft.command.argument.serialize.ConstantArgumentSerializer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.Text; +import net.minecraft.text.Texts; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.BlockPos; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class PhonosCommands { + public static void init() { + ArgumentTypeRegistry.registerArgumentType(Phonos.id("satellite"), + SatelliteArgumentType.class, ConstantArgumentSerializer.of(SatelliteArgumentType::new)); + + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + dispatcher.register(literal("phonos") + .then(literal("radar") + .requires(src -> src.hasPermissionLevel(2)) + .then(argument( + "channel", + IntegerArgumentType.integer(0, RadioStorage.CHANNEL_COUNT - 1)) + .executes(ctx -> radar( + ctx.getSource(), + ctx.getArgument("channel", Integer.class) + )) + ) + ) + .then(literal("satellite") + .then(literal("inspect").then(argument("pos", BlockPosArgumentType.blockPos()) + .executes(ctx -> satelliteInspect( + ctx.getSource(), + ctx.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(ctx.getSource()) + )) + ) + ) + .then(literal("list") + .requires(src -> src.hasPermissionLevel(2)) + .executes(ctx -> satelliteList( + ctx.getSource() + )) + ) + .then(literal("crash") + .requires(src -> src.hasPermissionLevel(4) && src.getPlayer() != null) + .then(argument("id", new SatelliteArgumentType()) + .executes(ctx -> satelliteCrash( + ctx.getSource(), + ctx.getArgument("id", Long.class) + )) + ) + ) + ) + ); + }); + } + + public static int satelliteInspect(ServerCommandSource source, BlockPos pos) { + var world = source.getWorld(); + + if (world.getBlockEntity(pos) instanceof SatelliteStationBlockEntity be) { + long id = be.streamId; + + if (!ServerCustomAudio.SAVED.containsKey(id)) { + source.sendError(Text.translatable("command.phonos.satellite.inspect.no_upload")); + + return 1; + } + + var aud = ServerCustomAudio.SAVED.get(id); + double sizeKB = (double)(aud.originalSize / 100) / 10D; + int duration = (int) Math.ceil((double) aud.originalSize / aud.sampleRate); + source.sendMessage(Text.translatable("command.phonos.satellite.entry", + Long.toHexString(id), + PhonosUtil.duration(duration), + sizeKB)); + + return 1; + } + + source.sendError(Text.translatable("command.phonos.satellite.inspect.invalid")); + return 1; + } + + public static int satelliteList(ServerCommandSource source) { + var set = ServerCustomAudio.SAVED.long2ObjectEntrySet(); + + if (set.isEmpty()) { + source.sendError(Text.translatable("command.phonos.satellite.list.none")); + + return 1; + } + + double totalSizeKB = 0; + + for (var entry : set) { + double sizeKB = (double)(entry.getValue().originalSize / 100) / 10D; + int duration = (int) Math.ceil((double) entry.getValue().originalSize / entry.getValue().sampleRate); + source.sendMessage(Text.translatable("command.phonos.satellite.entry", + Long.toHexString(entry.getLongKey()), + PhonosUtil.duration(duration), + sizeKB)); + totalSizeKB += entry.getValue().originalSize; + } + + totalSizeKB = (double)((int)totalSizeKB / 100) / 10D; + source.sendMessage(Text.translatable("command.phonos.satellite.list.info", set.size(), totalSizeKB)); + + return 1; + } + + public static int satelliteCrash(ServerCommandSource source, long id) throws CommandSyntaxException { + var idStr = Long.toHexString(id); + Phonos.LOG.info("Satellite {} was crashed via command by player {}.", idStr, source.getPlayerOrThrow()); + + ServerCustomAudio.deleteSaved(source.getServer(), id); + source.sendMessage(Text.translatable("command.phonos.satellite.crash", idStr)); + + return 1; + } + + public static int radar(ServerCommandSource source, int channel) { + var world = source.getWorld(); + var origin = source.getPosition(); + + var radar = RadarPoints.get(world); + var pos = new BlockPos.Mutable(); + + var result = new BlockPos.Mutable(); + double minSqDist = Double.POSITIVE_INFINITY; + + var points = radar.getPoints(channel); + if (points == null || points.size() == 0) { + source.sendError(Text.translatable("command.phonos.radar.none_found", channel)); + + return 1; + } + + for (long l : radar.getPoints(channel)) { + pos.set(l); + + double sqDist = origin.squaredDistanceTo(pos.getX(), pos.getY(), pos.getZ()); + if (sqDist < minSqDist) { + result.set(pos); + minSqDist = sqDist; + } + } + + sendCoordinates(source, "command.phonos.radar.success", result.up()); + + return 1; + } + + private static void sendCoordinates(ServerCommandSource source, String key, BlockPos pos) { + Text coords = Texts.bracketed( + Text.translatable("chat.coordinates", pos.getX(), pos.getY(), pos.getZ()) + ).styled(style -> + style.withColor(Formatting.GREEN) + .withClickEvent( + new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/tp @s " + pos.getX() + " " + pos.getY() + " " + pos.getZ())) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("chat.coordinates.tooltip")))); + + source.sendFeedback(() -> Text.translatable(key, coords), false); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/world/command/SatelliteArgumentType.java b/src/main/java/io/github/foundationgames/phonos/world/command/SatelliteArgumentType.java new file mode 100644 index 0000000..f6c9d08 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/world/command/SatelliteArgumentType.java @@ -0,0 +1,60 @@ +package io.github.foundationgames.phonos.world.command; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.github.foundationgames.phonos.sound.custom.ServerCustomAudio; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +import java.util.concurrent.CompletableFuture; + +public class SatelliteArgumentType implements ArgumentType { + public static final SimpleCommandExceptionType INVALID_ID_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("argument.phonos.idList.invalid")); + + public SatelliteArgumentType() { + } + + @Override + public Long parse(StringReader reader) throws CommandSyntaxException { + try { + var str = reader.readString(); + return Long.parseUnsignedLong(str, 16); + } catch (NumberFormatException ex) { + throw INVALID_ID_EXCEPTION.create(); + } + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + var input = builder.getInput().substring(builder.getStart()); + + if (context.getSource() instanceof ServerCommandSource) { + var savedIds = ServerCustomAudio.SAVED.keySet(); + + if (input.isBlank()) { + savedIds.forEach(l -> builder.suggest(Long.toHexString(l))); + + return builder.buildFuture(); + } + + builder.suggest(input); + + for (long id : savedIds) { + var idStr = Long.toHexString(id); + + if (idStr.startsWith(input)) { + builder.suggest(idStr); + } + } + + return builder.buildFuture(); + } + + return Suggestions.empty(); + } +} diff --git a/src/main/resources/assets/phonos/lang/en_us.json b/src/main/resources/assets/phonos/lang/en_us.json index e5c9595..5d330bc 100644 --- a/src/main/resources/assets/phonos/lang/en_us.json +++ b/src/main/resources/assets/phonos/lang/en_us.json @@ -23,6 +23,17 @@ "message.phonos.satellite_launching": "Launch in progress!", "message.phonos.no_satellite": "Place a satellite on the launch pad first!", + "command.phonos.radar.none_found": "Found no radio emitters on channel %s.", + "command.phonos.radar.success": "Nearest radio emitter located at %s.", + "command.phonos.satellite.inspect.no_upload": "This station has no orbiting satellite!", + "command.phonos.satellite.inspect.invalid": "This block is not a satellite station!", + "command.phonos.satellite.crash": "Crashed satellite with id %s.", + "command.phonos.satellite.entry": "ID: %s, Duration: %s, Size: %s KB", + "command.phonos.satellite.list.info": "Found %s orbiting, total size %s KB", + "command.phonos.satellite.list.none": "No satellites in orbit.", + + "argument.phonos.idList.invalid": "Invalid ID!", + "block.phonos.loudspeaker": "Loudspeaker", "block.phonos.electronic_jukebox": "Electronic Jukebox", "block.phonos.electronic_note_block": "Electronic Note Block",