From 2e7ace27fe48d633a38fbc7fac67406d30806a8b Mon Sep 17 00:00:00 2001 From: FoundationGames <43485105+FoundationGames@users.noreply.github.com> Date: Fri, 28 Jul 2023 23:47:59 -0700 Subject: [PATCH] Flesh out satellite audio mechanic - Add satellites, which are rockets mounted on top of stations - Uploading will launch the satellite rocket, crashing one in orbit deletes the audio - Configurable limit for maximum uploaded audio data (gamerule) - All phonos sounds are played in the records category (temporary) - Fixed some indexing issues when applying changes to cable networks - Server logs more detailed and frequent information about audio uploads --- gradle.properties | 2 +- .../github/foundationgames/phonos/Phonos.java | 6 + .../foundationgames/phonos/PhonosClient.java | 13 +- .../phonos/block/SatelliteStationBlock.java | 37 +- .../entity/SatelliteStationBlockEntity.java | 325 +++++++++++++++++- ...udioCableEndModel.java => BasicModel.java} | 8 +- .../block/CableOutputBlockEntityRenderer.java | 11 +- .../SatelliteStationBlockEntityRenderer.java | 102 ++++++ .../screen/CrashSatelliteStationScreen.java | 21 ++ ...java => LaunchSatelliteStationScreen.java} | 9 +- .../phonos/item/PhonosItems.java | 1 + .../phonos/network/ClientPayloadPackets.java | 35 +- .../phonos/network/PayloadPackets.java | 44 ++- .../sound/MultiSourceSoundInstance.java | 2 +- .../custom/ClientCustomAudioUploader.java | 26 +- .../sound/custom/ServerCustomAudio.java | 42 ++- .../sound/emitter/SoundEmitterTree.java | 5 +- .../phonos/sound/stream/AudioDataQueue.java | 11 +- .../phonos/sound/stream/QueueAudioStream.java | 3 - .../resources/assets/phonos/lang/en_us.json | 18 +- .../phonos/models/entity/satellite/main.json | 255 ++++++++++++++ .../assets/phonos/models/item/satellite.json | 6 + .../textures/entity/satellite/exhaust_1.png | Bin 0 -> 4325 bytes .../textures/entity/satellite/exhaust_2.png | Bin 0 -> 4322 bytes .../textures/entity/satellite/satellite.png | Bin 0 -> 5077 bytes .../assets/phonos/textures/item/satellite.png | Bin 0 -> 1066 bytes 26 files changed, 928 insertions(+), 54 deletions(-) rename src/main/java/io/github/foundationgames/phonos/client/model/{AudioCableEndModel.java => BasicModel.java} (70%) create mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/block/SatelliteStationBlockEntityRenderer.java create mode 100644 src/main/java/io/github/foundationgames/phonos/client/screen/CrashSatelliteStationScreen.java rename src/main/java/io/github/foundationgames/phonos/client/screen/{SatelliteStationScreen.java => LaunchSatelliteStationScreen.java} (92%) create mode 100644 src/main/resources/assets/phonos/models/entity/satellite/main.json create mode 100644 src/main/resources/assets/phonos/models/item/satellite.json create mode 100644 src/main/resources/assets/phonos/textures/entity/satellite/exhaust_1.png create mode 100644 src/main/resources/assets/phonos/textures/entity/satellite/exhaust_2.png create mode 100644 src/main/resources/assets/phonos/textures/entity/satellite/satellite.png create mode 100644 src/main/resources/assets/phonos/textures/item/satellite.png diff --git a/gradle.properties b/gradle.properties index d6854a0..7ef5cff 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.3 +mod_version = 1.0.0-beta.4 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 fdd9534..60ffa6f 100644 --- a/src/main/java/io/github/foundationgames/phonos/Phonos.java +++ b/src/main/java/io/github/foundationgames/phonos/Phonos.java @@ -18,11 +18,14 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerBlockEntityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.gamerule.v1.GameRuleFactory; +import net.fabricmc.fabric.api.gamerule.v1.GameRuleRegistry; import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.registry.Registries; import net.minecraft.registry.Registry; import net.minecraft.util.Identifier; +import net.minecraft.world.GameRules; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -34,6 +37,9 @@ public class Phonos implements ModInitializer { public static final ItemGroupQueue PHONOS_ITEMS = new ItemGroupQueue(id("phonos")); + public static final GameRules.Key PHONOS_UPLOAD_LIMIT_KB = GameRuleRegistry.register( + "phonosUploadLimitKB", GameRules.Category.MISC, GameRuleFactory.createIntRule(-1, -1)); + public static final Identifier STREAMED_SOUND = Phonos.id("streamed"); @Override diff --git a/src/main/java/io/github/foundationgames/phonos/PhonosClient.java b/src/main/java/io/github/foundationgames/phonos/PhonosClient.java index 889d046..d54b076 100644 --- a/src/main/java/io/github/foundationgames/phonos/PhonosClient.java +++ b/src/main/java/io/github/foundationgames/phonos/PhonosClient.java @@ -5,6 +5,7 @@ import io.github.foundationgames.phonos.client.render.block.CableOutputBlockEntityRenderer; import io.github.foundationgames.phonos.client.render.block.RadioLoudspeakerBlockEntityRenderer; import io.github.foundationgames.phonos.client.render.block.RadioTransceiverBlockEntityRenderer; +import io.github.foundationgames.phonos.client.render.block.SatelliteStationBlockEntityRenderer; import io.github.foundationgames.phonos.item.AudioCableItem; import io.github.foundationgames.phonos.item.PhonosItems; import io.github.foundationgames.phonos.network.ClientPayloadPackets; @@ -32,6 +33,7 @@ public class PhonosClient implements ClientModInitializer { public static final EntityModelLayer AUDIO_CABLE_END_LAYER = new EntityModelLayer(Phonos.id("audio_cable_end"), "main"); + public static final EntityModelLayer SATELLITE_LAYER = new EntityModelLayer(Phonos.id("satellite"), "main"); @Override public void onInitializeClient() { @@ -39,6 +41,7 @@ public void onInitializeClient() { ClientSoundStorage.initClient(); JsonEM.registerModelLayer(AUDIO_CABLE_END_LAYER); + JsonEM.registerModelLayer(SATELLITE_LAYER); BlockRenderLayerMap.INSTANCE.putBlock(PhonosBlocks.ELECTRONIC_NOTE_BLOCK, RenderLayer.getCutout()); BlockRenderLayerMap.INSTANCE.putBlock(PhonosBlocks.RADIO_TRANSCEIVER, RenderLayer.getCutout()); @@ -49,7 +52,7 @@ public void onInitializeClient() { BlockEntityRendererFactories.register(PhonosBlocks.CONNECTION_HUB_ENTITY, CableOutputBlockEntityRenderer::new); BlockEntityRendererFactories.register(PhonosBlocks.RADIO_TRANSCEIVER_ENTITY, RadioTransceiverBlockEntityRenderer::new); BlockEntityRendererFactories.register(PhonosBlocks.RADIO_LOUDSPEAKER_ENTITY, RadioLoudspeakerBlockEntityRenderer::new); - BlockEntityRendererFactories.register(PhonosBlocks.SATELLITE_STATION_ENTITY, CableOutputBlockEntityRenderer::new); + BlockEntityRendererFactories.register(PhonosBlocks.SATELLITE_STATION_ENTITY, SatelliteStationBlockEntityRenderer::new); ColorProviderRegistry.BLOCK.register((state, world, pos, tintIndex) -> world != null && pos != null && state != null ? @@ -98,12 +101,4 @@ public void onInitializeClient() { //ScreenRegistry.register(Phonos.RADIO_JUKEBOX_HANDLER, (gui, inventory, title) -> new RadioJukeboxScreen(gui, inventory.player)); } - - private static long seed(String s) { - if(s == null) return 0; - long l = 0; - for(char c : s.toCharArray()) - l = 31L * l + c; - return l; - } } diff --git a/src/main/java/io/github/foundationgames/phonos/block/SatelliteStationBlock.java b/src/main/java/io/github/foundationgames/phonos/block/SatelliteStationBlock.java index d898fc6..0dfbc5c 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/SatelliteStationBlock.java +++ b/src/main/java/io/github/foundationgames/phonos/block/SatelliteStationBlock.java @@ -1,7 +1,7 @@ package io.github.foundationgames.phonos.block; import io.github.foundationgames.phonos.block.entity.SatelliteStationBlockEntity; -import io.github.foundationgames.phonos.network.PayloadPackets; +import io.github.foundationgames.phonos.item.PhonosItems; import io.github.foundationgames.phonos.util.PhonosUtil; import net.minecraft.block.*; import net.minecraft.block.entity.BlockEntity; @@ -37,11 +37,28 @@ public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEnt var side = hit.getSide(); var facing = state.get(FACING); - if (side == Direction.DOWN || side == Direction.UP) { + if (side == Direction.DOWN) { return ActionResult.PASS; } if (player.canModifyBlocks() && world.getBlockEntity(pos) instanceof SatelliteStationBlockEntity be) { + if (side == Direction.UP) { + var holding = player.getStackInHand(hand); + if (holding.getItem() == PhonosItems.SATELLITE) { + if (world.isClient()) { + return ActionResult.CONSUME; + } else { + if (be.addRocket() && !player.isCreative()) { + holding.decrement(1); + } + + return ActionResult.SUCCESS; + } + } + + return ActionResult.PASS; + } + if (!world.isClient()) { if (PhonosUtil.holdingAudioCable(player)) { return ActionResult.PASS; @@ -54,7 +71,7 @@ public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEnt } } else { if (player instanceof ServerPlayerEntity sPlayer) { - PayloadPackets.sendOpenSatelliteStationScreen(sPlayer, pos); + be.tryOpenScreen(sPlayer); } return ActionResult.CONSUME; @@ -109,6 +126,20 @@ public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos po return SHAPE; } + @Override + public boolean hasComparatorOutput(BlockState state) { + return true; + } + + @Override + public int getComparatorOutput(BlockState state, World world, BlockPos pos) { + if (world.getBlockEntity(pos) instanceof SatelliteStationBlockEntity be) { + return be.getComparatorOutput(); + } + + return super.getComparatorOutput(state, world, pos); + } + @Nullable @Override public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { 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 817ed2e..b7a8c6a 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 @@ -1,6 +1,7 @@ package io.github.foundationgames.phonos.block.entity; import io.github.foundationgames.phonos.block.PhonosBlocks; +import io.github.foundationgames.phonos.item.PhonosItems; import io.github.foundationgames.phonos.network.PayloadPackets; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.custom.ServerCustomAudio; @@ -13,27 +14,48 @@ import io.github.foundationgames.phonos.world.sound.data.StreamSoundData; import net.minecraft.block.BlockState; import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.ItemEntity; import net.minecraft.item.ItemStack; import net.minecraft.item.ItemUsageContext; import net.minecraft.nbt.NbtCompound; +import net.minecraft.particle.ParticleTypes; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.state.property.Properties; +import net.minecraft.text.Text; import net.minecraft.util.DyeColor; +import net.minecraft.util.Formatting; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; public class SatelliteStationBlockEntity extends AbstractOutputBlockEntity { + public static final int ACTION_CRASH = 0; + public static final int ACTION_LAUNCH = 1; + public static final BlockConnectionLayout OUTPUT_LAYOUT = new BlockConnectionLayout() .addPoint(-8, -5, 0, Direction.WEST) .addPoint(8, -5, 0, Direction.EAST) .addPoint(0, -5, 8, Direction.SOUTH); + public static final int SCREEN_LAUNCH = 0; + public static final int SCREEN_CRASH = 1; public final long streamId; + + private String error = null; + private Vec3d launchpadPos = null; private @Nullable SoundEmitterTree playingSound = null; + private Status status = Status.NONE; + + private int playDuration = 0; + private int playingTimer = 0; + + private Rocket rocket = null; public SatelliteStationBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state, OUTPUT_LAYOUT); @@ -47,12 +69,15 @@ public SatelliteStationBlockEntity(BlockPos pos, BlockState state) { public void play() { if (world instanceof ServerWorld sWorld && ServerCustomAudio.hasSaved(this.streamId)) { - ServerOutgoingStreamHandler.startStream(this.streamId, ServerCustomAudio.loadSaved(this.streamId), sWorld.getServer()); + var aud = ServerCustomAudio.loadSaved(this.streamId); + ServerOutgoingStreamHandler.startStream(this.streamId, aud, sWorld.getServer()); this.playingSound = new SoundEmitterTree(this.emitterId); SoundStorage.getInstance(this.world).play(this.world, new StreamSoundData(SoundDataTypes.STREAM, this.emitterId(), this.streamId, 2, 1), this.playingSound); + + this.playingTimer = this.playDuration = (int) ((aud.originalSize * 20f) / aud.sampleRate); } } @@ -62,13 +87,46 @@ public void stop() { SoundStorage.getInstance(this.world).stop(this.world, this.emitterId()); this.playingSound = null; } + + this.playDuration = this.playingTimer = 0; + } + + public boolean addRocket() { + if (this.rocket != null || this.status != Status.NONE) { + return false; + } + + this.rocket = dormantRocket(); + sync(); + markDirty(); + + return true; } @Override public void tick(World world, BlockPos pos, BlockState state) { super.tick(world, pos, state); + if (this.rocket != null) { + this.rocket.tick(); + + if (this.rocket.removed) { + this.rocket = null; + this.status = Status.IN_ORBIT; + + sync(); + } + } + if (!world.isClient()) { + if (this.playingTimer > 0) { + this.playingTimer--; + + if (this.getComparatorOutput(this.playingTimer + 1) != this.getComparatorOutput()) { + world.updateComparators(this.pos, this.getCachedState().getBlock()); + } + } + if (this.playingSound != null) { var delta = this.playingSound.updateServer(world); @@ -76,7 +134,96 @@ public void tick(World world, BlockPos pos, BlockState state) { PayloadPackets.sendSoundUpdate(player, delta); } } + + if (ServerCustomAudio.ERRORS.containsKey(this.streamId)) { + if (status == Status.IN_ORBIT) this.performAction(ACTION_CRASH); + this.error = ServerCustomAudio.ERRORS.remove(this.streamId); + + sync(); + } + + if (status == Status.NONE && (ServerCustomAudio.UPLOADING.containsKey(this.streamId) || ServerCustomAudio.SAVED.containsKey(this.streamId))) { + this.performAction(ACTION_LAUNCH); + } else if (status == Status.IN_ORBIT) { + if (!ServerCustomAudio.SAVED.containsKey(this.streamId)) { + this.performAction(ACTION_CRASH); + } + } + } else { + if (this.rocket != null) { + this.rocket.addParticles(); + } + } + } + + public void performAction(int action) { + if (world instanceof ServerWorld sWorld) { + for (var player : sWorld.getPlayers()) { + sWorld.sendToPlayerIfNearby(player, + true, this.getPos().getX(), this.getPos().getY(), this.getPos().getZ(), + PayloadPackets.pktSatelliteAction(this, action)); + } + } + + switch (action) { + case ACTION_LAUNCH -> { + if (this.rocket == null) { + this.rocket = dormantRocket(); + } + + this.status = Status.LAUNCHING; + this.rocket.inFlight = true; + } + case ACTION_CRASH -> { + this.status = Status.NONE; + + if (world instanceof ServerWorld sWorld) { + ServerCustomAudio.deleteSaved(sWorld.getServer(), this.streamId); + + this.stop(); + } + + spawnCrashingSatellite(); + } + } + + if (!world.isClient()) { + markDirty(); + } + } + + public @Nullable Rocket getRocket() { + return this.rocket; + } + + public @Nullable String getError() { + return this.error; + } + + public Status getStatus() { + return this.status; + } + + public int getComparatorOutput() { + return getComparatorOutput(this.playingTimer); + } + + protected int getComparatorOutput(int timer) { + if (this.playDuration == 0) { + return 0; + } + + return MathHelper.clamp(15 * timer / this.playDuration, 0, 15); + } + + public Vec3d launchpadPos() { + if (this.launchpadPos == null) { + this.launchpadPos = Vec3d.ofBottomCenter(this.getPos()) + .add(new Vec3d(-0.21875, 0.4375, -0.21875) + .rotateY((float) Math.toRadians(180 - this.getRotation().asRotation()))); } + + return this.launchpadPos; } @Override @@ -85,21 +232,74 @@ public void onDestroyed() { if (world instanceof ServerWorld sWorld) { ServerCustomAudio.deleteSaved(sWorld.getServer(), this.streamId); + + if (status == Status.IN_ORBIT) { + spawnCrashingSatellite(); + } else if (this.rocket != null) { + var launchpad = this.launchpadPos(); + + double rX = launchpad.x + rocket.x; + double rY = launchpad.y + rocket.y; + double rZ = launchpad.z + rocket.z; + + var item = new ItemEntity(world, rX, rY, rZ, PhonosItems.SATELLITE.getDefaultStack()); + world.spawnEntity(item); + } } } + public void spawnCrashingSatellite() { + var pos = this.somewhereInTheSky().add(this.getPos().toCenterPos()).subtract(0, 100, 0); + + // TODO: Special crashing entity + var item = new ItemEntity(world, pos.x, pos.y, pos.z, PhonosItems.SATELLITE.getDefaultStack()); + world.spawnEntity(item); + } + @Override public void readNbt(NbtCompound nbt) { super.readNbt(nbt); + + this.status = Status.values()[nbt.getInt("status")]; + this.playingTimer = nbt.getInt("playing_timer"); + this.playDuration = nbt.getInt("play_duration"); + + if (nbt.contains("error")) { + this.error = nbt.getString("error"); + } + + if (nbt.contains("Rocket")) { + this.rocket = rocketFromNbt(nbt.getCompound("Rocket")); + } else { + this.rocket = null; + } } @Override protected void writeNbt(NbtCompound nbt) { super.writeNbt(nbt); + + nbt.putInt("status", this.status.ordinal()); + nbt.putInt("playing_timer", this.playingTimer); + nbt.putInt("play_duration", this.playDuration); + + if (this.error != null) { + nbt.putString("error", this.error); + } + + if (this.rocket != null) { + var rocketNbt = new NbtCompound(); + this.rocket.writeNbt(rocketNbt); + nbt.put("Rocket", rocketNbt); + } } public boolean canUpload(ServerPlayerEntity player) { - return player.canModifyBlocks(); + return player.getPos().squaredDistanceTo(this.getPos().toCenterPos()) < 96 && player.canModifyBlocks(); + } + + public boolean canCrash(ServerPlayerEntity player) { + return player.getPos().squaredDistanceTo(this.getPos().toCenterPos()) < 96 && player.canModifyBlocks(); } @Override @@ -137,7 +337,126 @@ public boolean forwards() { return false; } + public void tryOpenScreen(ServerPlayerEntity player) { + if (this.status == Status.LAUNCHING) { + player.sendMessageToClient(Text.translatable("message.phonos.satellite_launching").formatted(Formatting.GOLD), true); + } else if (this.status == Status.IN_ORBIT) { + PayloadPackets.sendOpenSatelliteStationScreen(player, pos, SCREEN_CRASH); + } else if (this.rocket == null) { + player.sendMessageToClient(Text.translatable("message.phonos.no_satellite").formatted(Formatting.RED), true); + } else { + PayloadPackets.sendOpenSatelliteStationScreen(player, pos, SCREEN_LAUNCH); + } + } + public enum Status { - NOT_LAUNCHED, LAUNCHING, IN_ORBIT, CRASHED; + NONE, IN_ORBIT, LAUNCHING; + } + + public Vec3d somewhereInTheSky() { + return new Vec3d(world.random.nextBetween(-5, 5), 200, world.random.nextBetween(-5, 5)); + } + + public Rocket dormantRocket() { + return new Rocket(this.world.random.nextFloat() * 0.2, somewhereInTheSky()); + } + + public Rocket rocketFromNbt(NbtCompound nbt) { + var rocket = new Rocket(nbt.getDouble("drift"), new Vec3d( + nbt.getDouble("targX"), nbt.getDouble("targY"), nbt.getDouble("targZ") + )); + + rocket.prevX = nbt.getDouble("prevX"); + rocket.prevY = nbt.getDouble("prevY"); + rocket.prevZ = nbt.getDouble("prevZ"); + rocket.x = nbt.getDouble("x"); + rocket.y = nbt.getDouble("y"); + rocket.z = nbt.getDouble("z"); + rocket.vel = nbt.getDouble("vel"); + rocket.inFlight = nbt.getBoolean("flying"); + + return rocket; + } + + // All coordinates are relative to the block's "launchpad", not world coords + public class Rocket { + private final Vector3d calc = new Vector3d(); + + public double prevX, prevY, prevZ; + public double x, y, z; + public double vel; + public boolean inFlight; + public boolean removed; + + public final double drift; + private final Vector3d target; + + public Rocket(double drift, Vec3d target) { + this.drift = drift; + this.target = new Vector3d(target.x, target.y, target.z); + } + + public void writeNbt(NbtCompound nbt) { + nbt.putDouble("prevX", prevX); + nbt.putDouble("prevY", prevY); + nbt.putDouble("prevZ", prevZ); + nbt.putDouble("x", x); + nbt.putDouble("y", y); + nbt.putDouble("z", z); + nbt.putDouble("vel", vel); + nbt.putBoolean("flying", inFlight); + nbt.putDouble("drift", drift); + nbt.putDouble("targX", target.x); + nbt.putDouble("targY", target.y); + nbt.putDouble("targZ", target.z); + } + + public float getX(float delta) { + return (float) MathHelper.lerp(delta, prevX, x); + } + + public float getY(float delta) { + return (float) MathHelper.lerp(delta, prevY, y); + } + + public float getZ(float delta) { + return (float) MathHelper.lerp(delta, prevZ, z); + } + + public void tick() { + if (inFlight) { + this.prevX = x; + this.prevY = y; + this.prevZ = z; + + this.vel = Math.min(this.vel + 0.25, 3.25); + + calc.set(target.x - x, target.y - y, target.z - z).normalize(vel); + + this.x += calc.x; + this.y += calc.y; + this.z += calc.z; + + this.target.add(1 * drift, 0, 1 * drift); + + if (this.y > target.y) { + this.removed = true; + } + } + } + + public void addParticles() { + if (inFlight) { + var be = SatelliteStationBlockEntity.this; + + if (be.world.isClient()) { + double x = be.launchpadPos().x + this.x; + double y = be.launchpadPos().y + this.y; + double z = be.launchpadPos().z + this.z; + + MinecraftClient.getInstance().particleManager.addParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, x, y, z, 0, -0.03 ,0); + } + } + } } } diff --git a/src/main/java/io/github/foundationgames/phonos/client/model/AudioCableEndModel.java b/src/main/java/io/github/foundationgames/phonos/client/model/BasicModel.java similarity index 70% rename from src/main/java/io/github/foundationgames/phonos/client/model/AudioCableEndModel.java rename to src/main/java/io/github/foundationgames/phonos/client/model/BasicModel.java index 4944019..85df048 100644 --- a/src/main/java/io/github/foundationgames/phonos/client/model/AudioCableEndModel.java +++ b/src/main/java/io/github/foundationgames/phonos/client/model/BasicModel.java @@ -1,19 +1,15 @@ package io.github.foundationgames.phonos.client.model; -import io.github.foundationgames.phonos.Phonos; import net.minecraft.client.model.Model; import net.minecraft.client.model.ModelPart; import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.VertexConsumer; import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.util.Identifier; - -public class AudioCableEndModel extends Model { - public static final Identifier TEXTURE = Phonos.id("textures/entity/audio_cable.png"); +public class BasicModel extends Model { private final ModelPart root; - public AudioCableEndModel(ModelPart root) { + public BasicModel(ModelPart root) { super(RenderLayer::getEntitySolid); this.root = root; diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java index b9ce99b..6604c53 100644 --- a/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java +++ b/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java @@ -1,7 +1,8 @@ package io.github.foundationgames.phonos.client.render.block; +import io.github.foundationgames.phonos.Phonos; import io.github.foundationgames.phonos.PhonosClient; -import io.github.foundationgames.phonos.client.model.AudioCableEndModel; +import io.github.foundationgames.phonos.client.model.BasicModel; import io.github.foundationgames.phonos.client.render.ConnectionRenderer; import io.github.foundationgames.phonos.world.sound.block.OutputBlockEntity; import net.minecraft.block.entity.BlockEntity; @@ -9,17 +10,19 @@ import net.minecraft.client.render.block.entity.BlockEntityRenderer; import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; public class CableOutputBlockEntityRenderer implements BlockEntityRenderer { - private final AudioCableEndModel cableEndModel; + public static final Identifier TEXTURE = Phonos.id("textures/entity/audio_cable.png"); + private final BasicModel cableEndModel; public CableOutputBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) { - cableEndModel = new AudioCableEndModel(ctx.getLayerModelPart(PhonosClient.AUDIO_CABLE_END_LAYER)); + cableEndModel = new BasicModel(ctx.getLayerModelPart(PhonosClient.AUDIO_CABLE_END_LAYER)); } @Override public void render(E entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) { - var buffer = vertexConsumers.getBuffer(cableEndModel.getLayer(AudioCableEndModel.TEXTURE)); + var buffer = vertexConsumers.getBuffer(cableEndModel.getLayer(TEXTURE)); matrices.push(); matrices.translate(-entity.getPos().getX(), -entity.getPos().getY(), -entity.getPos().getZ()); diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/block/SatelliteStationBlockEntityRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/block/SatelliteStationBlockEntityRenderer.java new file mode 100644 index 0000000..01d3bdc --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/client/render/block/SatelliteStationBlockEntityRenderer.java @@ -0,0 +1,102 @@ +package io.github.foundationgames.phonos.client.render.block; + +import io.github.foundationgames.phonos.Phonos; +import io.github.foundationgames.phonos.PhonosClient; +import io.github.foundationgames.phonos.block.entity.SatelliteStationBlockEntity; +import io.github.foundationgames.phonos.client.model.BasicModel; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.RotationAxis; + +public class SatelliteStationBlockEntityRenderer extends CableOutputBlockEntityRenderer { + public static final String[] LOAD_ANIM = {"Oooo", "oOoo", "ooOo", "oooO"}; + public static final Text TEXT_LAUNCH_READY = Text.translatable("display.phonos.satellite_station.launch_ready"); + public static final Text TEXT_LAUNCHING = Text.translatable("display.phonos.satellite_station.launching").formatted(Formatting.YELLOW); + public static final Text TEXT_IN_ORBIT = Text.translatable("display.phonos.satellite_station.in_orbit"); + public static final Text TEXT_ERROR = Text.translatable("display.phonos.satellite_station.error").formatted(Formatting.RED); + + public static final Identifier TEXTURE = Phonos.id("textures/entity/satellite/satellite.png"); + public static final Identifier EXHAUST_TEX_1 = Phonos.id("textures/entity/satellite/exhaust_1.png"); + public static final Identifier EXHAUST_TEX_2 = Phonos.id("textures/entity/satellite/exhaust_2.png"); + + public static int TEXT_COLOR = 0xEEEFFF; + public static int OUTLINE_COLOR = 0x0512A7; + + private final BasicModel satelliteModel; + + private final TextRenderer font; + + public SatelliteStationBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) { + super(ctx); + + this.satelliteModel = new BasicModel(ctx.getLayerModelPart(PhonosClient.SATELLITE_LAYER)); + this.font = ctx.getTextRenderer(); + } + + @Override + public void render(SatelliteStationBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) { + super.render(entity, tickDelta, matrices, vertexConsumers, light, overlay); + + var rocket = entity.getRocket(); + + if (rocket != null) { + matrices.push(); + + var origin = entity.launchpadPos(); + var pos = entity.getPos(); + matrices.translate(origin.x - pos.getX(), origin.y - pos.getY(), origin.z - pos.getZ()); + + matrices.translate(rocket.getX(tickDelta), rocket.getY(tickDelta), rocket.getZ(tickDelta)); + + matrices.multiply(RotationAxis.POSITIVE_Z.rotation((float) Math.PI)); + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(45 + entity.getRotation().asRotation())); + + satelliteModel.render(matrices, vertexConsumers.getBuffer(RenderLayer.getEntityCutoutNoCull(TEXTURE)), + light, overlay, 1, 1, 1, 1); + + if (rocket.inFlight) { + boolean texFlag = (entity.getWorld().getTime() & 0b11) < 2; + + satelliteModel.render(matrices, vertexConsumers.getBuffer(RenderLayer.getEyes(texFlag ? EXHAUST_TEX_2 : EXHAUST_TEX_1)), + light, overlay, 1, 1, 1, 1); + } + + matrices.pop(); + } + + matrices.push(); + + matrices.translate(0.5, 0.5, 0.5); + matrices.multiply(RotationAxis.NEGATIVE_Y.rotationDegrees(180 + entity.getRotation().asRotation())); + matrices.translate(0, 0, -0.501); + + matrices.scale(0.02f, 0.02f, 0.02f); + matrices.multiply(RotationAxis.POSITIVE_Z.rotation((float) Math.PI)); + matrices.translate(0, 10.75, 0); + + var text = switch (entity.getStatus()) { + case IN_ORBIT -> TEXT_IN_ORBIT; + case LAUNCHING -> TEXT_LAUNCHING; + default -> { + if (entity.getError() != null) { + yield TEXT_ERROR; + } + if (entity.getRocket() != null) { + yield TEXT_LAUNCH_READY; + } + yield Text.literal(LOAD_ANIM[(int) ((entity.getWorld().getTime() / 4) % 4)]); + } + }; + + this.font.drawWithOutline(text.asOrderedText(), -this.font.getWidth(text) * 0.5f, 0, TEXT_COLOR, OUTLINE_COLOR, + matrices.peek().getPositionMatrix(), vertexConsumers, 15728880); + + matrices.pop(); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/client/screen/CrashSatelliteStationScreen.java b/src/main/java/io/github/foundationgames/phonos/client/screen/CrashSatelliteStationScreen.java new file mode 100644 index 0000000..8b65418 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/client/screen/CrashSatelliteStationScreen.java @@ -0,0 +1,21 @@ +package io.github.foundationgames.phonos.client.screen; + +import io.github.foundationgames.phonos.block.entity.SatelliteStationBlockEntity; +import io.github.foundationgames.phonos.network.ClientPayloadPackets; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public class CrashSatelliteStationScreen extends ConfirmScreen { + public static final Text MESSAGE = Text.translatable("message.phonos.crash_satellite_station").formatted(Formatting.RED); + + public CrashSatelliteStationScreen(SatelliteStationBlockEntity blockEntity) { + super(ok -> { + if (ok) { + ClientPayloadPackets.sendRequestSatelliteCrash(blockEntity); + } + MinecraftClient.getInstance().setScreen(null); + }, LaunchSatelliteStationScreen.TITLE, MESSAGE); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/client/screen/SatelliteStationScreen.java b/src/main/java/io/github/foundationgames/phonos/client/screen/LaunchSatelliteStationScreen.java similarity index 92% rename from src/main/java/io/github/foundationgames/phonos/client/screen/SatelliteStationScreen.java rename to src/main/java/io/github/foundationgames/phonos/client/screen/LaunchSatelliteStationScreen.java index 972ec59..532c50d 100644 --- a/src/main/java/io/github/foundationgames/phonos/client/screen/SatelliteStationScreen.java +++ b/src/main/java/io/github/foundationgames/phonos/client/screen/LaunchSatelliteStationScreen.java @@ -17,7 +17,7 @@ import java.nio.file.Path; import java.util.List; -public class SatelliteStationScreen extends Screen { +public class LaunchSatelliteStationScreen extends Screen { public static final Text TITLE = Text.translatable("container.phonos.satellite_station"); public static final Text UPLOAD = Text.translatable("button.phonos.satellite_station.upload"); public static final Text DRAG_PROMPT = Text.translatable("hint.phonos.satellite_station.drag").formatted(Formatting.GRAY, Formatting.ITALIC); @@ -29,10 +29,15 @@ public class SatelliteStationScreen extends Screen { private ButtonWidget uploadButton; - public SatelliteStationScreen(SatelliteStationBlockEntity entity) { + public LaunchSatelliteStationScreen(SatelliteStationBlockEntity entity) { super(TITLE); this.station = entity; + + var err = entity.getError(); + if (err != null) { + this.status = Text.translatable("error.phonos.satellite." + err).formatted(Formatting.RED); + } } @Override diff --git a/src/main/java/io/github/foundationgames/phonos/item/PhonosItems.java b/src/main/java/io/github/foundationgames/phonos/item/PhonosItems.java index bfef767..cae760e 100644 --- a/src/main/java/io/github/foundationgames/phonos/item/PhonosItems.java +++ b/src/main/java/io/github/foundationgames/phonos/item/PhonosItems.java @@ -7,6 +7,7 @@ import net.minecraft.util.DyeColor; public class PhonosItems { + public static final Item SATELLITE = register(new Item(new Item.Settings()), "satellite"); public static final Item AUDIO_CABLE = register(new AudioCableItem(null, new Item.Settings()), "audio_cable"); public static final Item WHITE_AUDIO_CABLE = register(new AudioCableItem(DyeColor.WHITE, new Item.Settings()), "white_audio_cable"); public static final Item ORANGE_AUDIO_CABLE = register(new AudioCableItem(DyeColor.ORANGE, new Item.Settings()), "orange_audio_cable"); diff --git a/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java b/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java index 02ef445..e497c1a 100644 --- a/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java +++ b/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java @@ -2,7 +2,8 @@ import io.github.foundationgames.phonos.Phonos; import io.github.foundationgames.phonos.block.entity.SatelliteStationBlockEntity; -import io.github.foundationgames.phonos.client.screen.SatelliteStationScreen; +import io.github.foundationgames.phonos.client.screen.CrashSatelliteStationScreen; +import io.github.foundationgames.phonos.client.screen.LaunchSatelliteStationScreen; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.custom.ClientCustomAudioUploader; import io.github.foundationgames.phonos.sound.emitter.SoundEmitterTree; @@ -41,10 +42,15 @@ public static void initClient() { ClientPlayNetworking.registerGlobalReceiver(Phonos.id("open_satellite_station_screen"), (client, handler, buf, responseSender) -> { var pos = buf.readBlockPos(); + int screenType = buf.readInt(); client.execute(() -> { if (client.world.getBlockEntity(pos) instanceof SatelliteStationBlockEntity sat) { - client.setScreen(new SatelliteStationScreen(sat)); + client.setScreen(switch (screenType) { + case SatelliteStationBlockEntity.SCREEN_LAUNCH -> new LaunchSatelliteStationScreen(sat); + case SatelliteStationBlockEntity.SCREEN_CRASH -> new CrashSatelliteStationScreen(sat); + default -> null; + }); } }); }); @@ -62,6 +68,12 @@ public static void initClient() { }); }); + ClientPlayNetworking.registerGlobalReceiver(Phonos.id("audio_upload_stop"), (client, handler, buf, responseSender) -> { + long id = buf.readLong(); + + client.execute(() -> ClientCustomAudioUploader.cancelUpload(id)); + }); + ClientPlayNetworking.registerGlobalReceiver(Phonos.id("audio_stream_data"), (client, handler, buf, responseSender) -> { long id = buf.readLong(); int sampleRate = buf.readInt(); @@ -75,14 +87,33 @@ public static void initClient() { client.execute(() -> ClientIncomingStreamHandler.endStream(id)); }); + + ClientPlayNetworking.registerGlobalReceiver(Phonos.id("satellite_action"), (client, handler, buf, responseSender) -> { + var pos = buf.readBlockPos(); + int action = buf.readInt(); + + client.execute(() -> { + if (client.world.getBlockEntity(pos) instanceof SatelliteStationBlockEntity be) { + be.performAction(action); + } + }); + }); } public static void sendRequestSatelliteUploadSession(SatelliteStationBlockEntity entity) { var buf = new PacketByteBuf(Unpooled.buffer()); buf.writeBlockPos(entity.getPos()); + ClientPlayNetworking.send(Phonos.id("request_satellite_upload_session"), buf); } + public static void sendRequestSatelliteCrash(SatelliteStationBlockEntity entity) { + var buf = new PacketByteBuf(Unpooled.buffer()); + buf.writeBlockPos(entity.getPos()); + + ClientPlayNetworking.send(Phonos.id("request_satellite_crash"), buf); + } + public static void sendAudioUploadPacket(long streamId, int sampleRate, ByteBuffer samples, boolean last) { var buf = new PacketByteBuf(Unpooled.buffer()); buf.writeLong(streamId); diff --git a/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java b/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java index 3733068..807215b 100644 --- a/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java +++ b/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java @@ -9,6 +9,8 @@ import io.netty.buffer.Unpooled; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.listener.ClientPlayPacketListener; +import net.minecraft.network.packet.Packet; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.math.BlockPos; @@ -36,6 +38,28 @@ public static void initCommon() { }); }); + ServerPlayNetworking.registerGlobalReceiver(Phonos.id("request_satellite_crash"), (server, player, handler, buf, responseSender) -> { + var pos = buf.readBlockPos(); + + server.execute(() -> { + var world = player.getWorld(); + + if (world.getBlockEntity(pos) instanceof SatelliteStationBlockEntity entity && entity.canCrash(player)) { + entity.performAction(SatelliteStationBlockEntity.ACTION_CRASH); + } + }); + }); + + ServerPlayNetworking.registerGlobalReceiver(Phonos.id("audio_upload"), (server, player, handler, buf, responseSender) -> { + long streamId = buf.readLong(); + int sampleRate = buf.readInt(); + var samples = PhonosUtil.readBufferFromPacket(buf, ByteBuffer::allocate); + + boolean last = buf.readBoolean(); + + server.execute(() -> ServerCustomAudio.receiveUpload(server, player, streamId, sampleRate, samples, last)); + }); + ServerPlayNetworking.registerGlobalReceiver(Phonos.id("audio_upload"), (server, player, handler, buf, responseSender) -> { long streamId = buf.readLong(); int sampleRate = buf.readInt(); @@ -66,12 +90,21 @@ public static void sendSoundUpdate(ServerPlayerEntity player, SoundEmitterTree.D ServerPlayNetworking.send(player, Phonos.id("sound_update"), buf); } - public static void sendOpenSatelliteStationScreen(ServerPlayerEntity player, BlockPos pos) { + public static void sendOpenSatelliteStationScreen(ServerPlayerEntity player, BlockPos pos, int screenType) { var buf = new PacketByteBuf(Unpooled.buffer()); buf.writeBlockPos(pos); + buf.writeInt(screenType); + ServerPlayNetworking.send(player, Phonos.id("open_satellite_station_screen"), buf); } + public static void sendUploadStop(ServerPlayerEntity player, long uploadId) { + var buf = new PacketByteBuf(Unpooled.buffer()); + buf.writeLong(uploadId); + + ServerPlayNetworking.send(player, Phonos.id("audio_upload_stop"), buf); + } + public static void sendUploadStatus(ServerPlayerEntity player, long uploadId, boolean ok) { var buf = new PacketByteBuf(Unpooled.buffer()); buf.writeLong(uploadId); @@ -92,6 +125,15 @@ public static void sendAudioStreamData(ServerPlayerEntity player, long streamId, public static void sendAudioStreamEnd(ServerPlayerEntity player, long streamId) { var buf = new PacketByteBuf(Unpooled.buffer()); buf.writeLong(streamId); + ServerPlayNetworking.send(player, Phonos.id("audio_stream_end"), buf); } + + public static Packet pktSatelliteAction(SatelliteStationBlockEntity be, int action) { + var buf = new PacketByteBuf(Unpooled.buffer()); + buf.writeBlockPos(be.getPos()); + buf.writeInt(action); + + return ServerPlayNetworking.createS2CPacket(Phonos.id("satellite_action"), buf); + } } diff --git a/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java b/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java index 2b02cd0..dccc27a 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java @@ -21,7 +21,7 @@ public class MultiSourceSoundInstance extends AbstractSoundInstance implements T private boolean done; protected MultiSourceSoundInstance(SoundEmitterTree tree, Identifier sound, Random random, float volume, float pitch) { - super(sound, SoundCategory.MASTER, random); + super(sound, SoundCategory.RECORDS, random); this.emitters = new AtomicReference<>(tree); this.volume = volume; diff --git a/src/main/java/io/github/foundationgames/phonos/sound/custom/ClientCustomAudioUploader.java b/src/main/java/io/github/foundationgames/phonos/sound/custom/ClientCustomAudioUploader.java index 29fcfb4..0cfc7b6 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/custom/ClientCustomAudioUploader.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/custom/ClientCustomAudioUploader.java @@ -3,10 +3,16 @@ import io.github.foundationgames.phonos.network.ClientPayloadPackets; import io.github.foundationgames.phonos.sound.stream.AudioDataQueue; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + public class ClientCustomAudioUploader { - public static final Long2ObjectMap UPLOAD_QUEUE = new Long2ObjectOpenHashMap<>(); + public static final ExecutorService UPLOAD_POOL = Executors.newFixedThreadPool(1); + + public static final Long2ObjectMap UPLOAD_QUEUE = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); public static void queueForUpload(long id, AudioDataQueue audio) { UPLOAD_QUEUE.put(id, audio); @@ -14,14 +20,22 @@ public static void queueForUpload(long id, AudioDataQueue audio) { public static void sendUploadPackets(long id) { if (UPLOAD_QUEUE.containsKey(id)) { - var aud = UPLOAD_QUEUE.get(id); + UPLOAD_POOL.submit(() -> sendAudioDataPackets(id)); + } + } - while (!aud.data.isEmpty()) { - ClientPayloadPackets.sendAudioUploadPacket(id, aud.sampleRate, aud.data.removeFirst().rewind(), aud.data.isEmpty()); - } + private static void sendAudioDataPackets(long id) { + var aud = UPLOAD_QUEUE.get(id); - UPLOAD_QUEUE.remove(id); + while (!aud.data.isEmpty() && UPLOAD_QUEUE.containsKey(id)) { + ClientPayloadPackets.sendAudioUploadPacket(id, aud.sampleRate, aud.data.removeFirst().rewind(), aud.data.isEmpty()); } + + UPLOAD_QUEUE.remove(id); + } + + public static void cancelUpload(long id) { + UPLOAD_QUEUE.remove(id); } public static void reset() { 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 538e42e..cdf06ae 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 @@ -1,12 +1,12 @@ package io.github.foundationgames.phonos.sound.custom; import io.github.foundationgames.phonos.Phonos; +import io.github.foundationgames.phonos.network.PayloadPackets; import io.github.foundationgames.phonos.sound.stream.AudioDataQueue; import io.github.foundationgames.phonos.util.PhonosUtil; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongArraySet; +import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongMaps; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; @@ -17,14 +17,23 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class ServerCustomAudio { public static final String FILE_EXT = ".phonosaud"; - public static final Long2ObjectMap UPLOADING = new Long2ObjectOpenHashMap<>(); + public static final ExecutorService UPLOAD_POOL = Executors.newFixedThreadPool(1); + + public static final Long2ObjectMap UPLOADING = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); public static final Long2ObjectMap SAVED = new Long2ObjectOpenHashMap<>(); - private static final Object2LongMap UPLOAD_SESSIONS = new Object2LongOpenHashMap<>(); + private static int TOTAL_SAVED_SIZE = 0; + + public static final Long2ObjectMap ERRORS = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); + public static final LongSet SUCCESSES = new LongOpenHashSet(); + + private static final Object2LongMap UPLOAD_SESSIONS = Object2LongMaps.synchronize(new Object2LongOpenHashMap<>()); public static boolean hasSaved(long id) { return SAVED.containsKey(id); @@ -52,12 +61,26 @@ public static void endUploadSession(UUID player) { } public static void receiveUpload(MinecraftServer srv, ServerPlayerEntity player, long id, int sampleRate, ByteBuffer samples, boolean last) { + UPLOAD_POOL.submit(() -> handleUpload(srv, player, id, sampleRate, samples, last)); + } + + private static void handleUpload(MinecraftServer srv, ServerPlayerEntity player, long id, int sampleRate, ByteBuffer samples, boolean last) { if (UPLOAD_SESSIONS.containsKey(player.getUuid()) && UPLOAD_SESSIONS.getLong(player.getUuid()) == id) { var aud = UPLOADING.computeIfAbsent(id, k -> new AudioDataQueue(sampleRate)); - aud.data.addLast(samples); + aud.push(samples); + + int maxAud = srv.getGameRules().getInt(Phonos.PHONOS_UPLOAD_LIMIT_KB) * 1000; + if (maxAud > 0 && TOTAL_SAVED_SIZE + aud.originalSize > maxAud) { + endUploadSession(player.getUuid()); + PayloadPackets.sendUploadStop(player, id); + + ERRORS.put(id, "upload_limit"); + return; + } if (last) { SAVED.put(id, aud); + SUCCESSES.add(id); endUploadSession(player.getUuid()); try { @@ -70,12 +93,12 @@ public static void receiveUpload(MinecraftServer srv, ServerPlayerEntity player, } public static void deleteSaved(MinecraftServer srv, long id) { - SAVED.remove(id); + var aud = SAVED.remove(id); try { deleteOnly(id, PhonosUtil.getCustomSoundFolder(srv)); - Phonos.LOG.info("Saved audio with ID {} was deleted.", Long.toHexString(id)); + Phonos.LOG.info("Saved audio with ID {} ({} bytes) was deleted.", Long.toHexString(id), aud.originalSize); } catch (IOException ex) { Phonos.LOG.error("Error saving uploaded sound", ex); } @@ -143,6 +166,7 @@ public static void load(Path folder) throws IOException { try (var in = Files.newInputStream(path)) { var aud = AudioDataQueue.read(in, ByteBuffer::allocate); SAVED.put(id, aud); + TOTAL_SAVED_SIZE += aud.originalSize; } } catch (NumberFormatException ex) { @@ -151,5 +175,7 @@ public static void load(Path folder) throws IOException { } } } + + Phonos.LOG.info("Loaded " + TOTAL_SAVED_SIZE + " bytes of saved audio from /phonos/"); } } diff --git a/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java b/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java index 61666da..abccf43 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java @@ -203,7 +203,10 @@ public void apply(SoundEmitterTree tree) { if (idx < tree.levels.size()) { tree.levels.set(idx, entry.getValue()); } else { - tree.levels.add(idx, entry.getValue()); + while (tree.levels.size() < idx - 1) { + tree.levels.add(new Level(new LongArrayList(), new LongArrayList())); + } + tree.levels.add(entry.getValue()); } } } diff --git a/src/main/java/io/github/foundationgames/phonos/sound/stream/AudioDataQueue.java b/src/main/java/io/github/foundationgames/phonos/sound/stream/AudioDataQueue.java index f813e8b..10f536a 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/stream/AudioDataQueue.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/stream/AudioDataQueue.java @@ -16,16 +16,23 @@ public class AudioDataQueue { public final int sampleRate; public final Deque data = new ArrayDeque<>(); + public int originalSize; + public AudioDataQueue(int sampleRate) { this.sampleRate = sampleRate; } + public void push(ByteBuffer buf) { + this.data.addLast(buf); + originalSize += buf.capacity(); + } + public AudioDataQueue copy(IntFunction bufferProvider) { var copy = new AudioDataQueue(this.sampleRate); for (var buf : this.data) { int pos = buf.position(); - copy.data.addLast(bufferProvider.apply(buf.capacity()).put(buf)); + copy.push(bufferProvider.apply(buf.capacity()).put(buf)); buf.position(pos); } @@ -60,7 +67,7 @@ public static AudioDataQueue read(InputStream stream, IntFunction bu } buf.position(cur); - aud.data.addLast(buf); + aud.push(buf); } return aud; diff --git a/src/main/java/io/github/foundationgames/phonos/sound/stream/QueueAudioStream.java b/src/main/java/io/github/foundationgames/phonos/sound/stream/QueueAudioStream.java index 5394b0b..8456f89 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/stream/QueueAudioStream.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/stream/QueueAudioStream.java @@ -11,9 +11,6 @@ public class QueueAudioStream implements AudioStream { private AudioFormat format = new AudioFormat(20000, 8, 1, true, false); - private ByteBuffer stall = null; - private int signalDiv = 1; - public QueueAudioStream() { } diff --git a/src/main/resources/assets/phonos/lang/en_us.json b/src/main/resources/assets/phonos/lang/en_us.json index 79386de..5bc2532 100644 --- a/src/main/resources/assets/phonos/lang/en_us.json +++ b/src/main/resources/assets/phonos/lang/en_us.json @@ -1,15 +1,28 @@ { "itemGroup.phonos.phonos": "Phonos", + "gamerule.phonosUploadLimitKB": "Satellite upload limit (KB)", + "container.phonos.satellite_station": "Satellite Station", - "button.phonos.satellite_station.upload": "Upload", + "button.phonos.satellite_station.upload": "Launch", "hint.phonos.satellite_station.drag": "+ Drag and drop .ogg file to upload", - "status.phonos.satellite.ready_upload": "Ready to Upload!", + "status.phonos.satellite.ready_upload": "Ready to upload and launch!", "status.phonos.satellite.mono_only": "Audio file must be mono only", "status.phonos.satellite.invalid_format": "Invalid .ogg file! (Check log for details)", "status.phonos.satellite.uploading": "Power the Satellite Station to play.", + "error.phonos.satellite.upload_limit": "Too many satellites were in orbit! (Upload limit reached)", + + "display.phonos.satellite_station.launch_ready": "READY", + "display.phonos.satellite_station.launching": "LAUNCH", + "display.phonos.satellite_station.in_orbit": "ORBIT", + "display.phonos.satellite_station.error": "ERROR", + + "message.phonos.crash_satellite_station": "Crash the currently orbiting satellite?", + "message.phonos.satellite_launching": "Launch in progress!", + "message.phonos.no_satellite": "Place a satellite on the launch pad first!", + "block.phonos.loudspeaker": "Loudspeaker", "block.phonos.electronic_jukebox": "Electronic Jukebox", "block.phonos.electronic_note_block": "Electronic Note Block", @@ -18,6 +31,7 @@ "block.phonos.radio_loudspeaker": "Radio Loudspeaker", "block.phonos.satellite_station": "Satellite Station", + "item.phonos.satellite": "Satellite", "item.phonos.audio_cable": "Audio Cable", "item.phonos.black_audio_cable": "Black Audio Cable", "item.phonos.blue_audio_cable": "Blue Audio Cable", diff --git a/src/main/resources/assets/phonos/models/entity/satellite/main.json b/src/main/resources/assets/phonos/models/entity/satellite/main.json new file mode 100644 index 0000000..e1fb7c3 --- /dev/null +++ b/src/main/resources/assets/phonos/models/entity/satellite/main.json @@ -0,0 +1,255 @@ +{ + "texture": { + "width": 32, + "height": 32 + }, + "bones": { + "main": { + "transform": { + "origin": [ + -0.5, + 0, + -0.5 + ] + }, + "cuboids": [ + { + "offset": [ + -1, + -13, + 2 + ], + "dimensions": [ + 3, + 5, + 2 + ], + "uv": [ + 20, + 0 + ] + }, + { + "offset": [ + -1, + -16, + -1 + ], + "dimensions": [ + 3, + 14, + 3 + ], + "uv": [ + 0, + 4 + ] + }, + { + "offset": [ + -0.5, + -18, + -0.5 + ], + "dimensions": [ + 2, + 2, + 2 + ], + "uv": [ + 0, + 0 + ] + }, + { + "offset": [ + 2, + -6, + 0.5 + ], + "dimensions": [ + 2, + 6, + 0 + ], + "uv": [ + 12, + 15 + ], + "mirror": true + }, + { + "offset": [ + -3, + -6, + 0.5 + ], + "dimensions": [ + 2, + 6, + 0 + ], + "uv": [ + 12, + 15 + ] + }, + { + "offset": [ + -0.5, + -2, + -0.5 + ], + "dimensions": [ + 2, + 8, + 2 + ], + "uv": [ + 12, + 0 + ] + } + ], + "children": { + "solar_panel": { + "transform": { + "origin": [ + 0, + -10.5, + 3 + ], + "rotation": [ + 0.30543261909900765, + 0, + 0 + ] + }, + "cuboids": [ + { + "offset": [ + -5, + -3.5, + 0 + ], + "dimensions": [ + 11, + 7, + 0.020000000000000018 + ], + "uv": [ + 0, + 21 + ] + } + ], + "children": {} + }, + "dish": { + "transform": { + "origin": [ + 0, + -13, + 4 + ], + "rotation": [ + 0.17453292519943295, + 0, + 0 + ] + }, + "cuboids": [ + { + "offset": [ + -1, + -1.5, + -0.25 + ], + "dimensions": [ + 3, + 3, + 2 + ], + "dilation": [ + 0.02, + 0.02, + 0.02 + ], + "uv": [ + 12, + 10 + ] + }, + { + "offset": [ + -1, + -1.5, + 0 + ], + "dimensions": [ + 3, + 3, + 0.020000000000000018 + ], + "uv": [ + 11, + 12 + ] + } + ], + "children": {} + }, + "h_fins": { + "transform": { + "origin": [ + 0.5, + 0, + 0.5 + ], + "rotation": [ + 0, + -1.5707963267948966, + 0 + ] + }, + "cuboids": [ + { + "offset": [ + 1.5, + -6, + 0 + ], + "dimensions": [ + 2, + 6, + 0 + ], + "uv": [ + 12, + 15 + ], + "mirror": true + }, + { + "offset": [ + -3.5, + -6, + 0 + ], + "dimensions": [ + 2, + 6, + 0 + ], + "uv": [ + 12, + 15 + ] + } + ], + "children": {} + } + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/phonos/models/item/satellite.json b/src/main/resources/assets/phonos/models/item/satellite.json new file mode 100644 index 0000000..b6259ba --- /dev/null +++ b/src/main/resources/assets/phonos/models/item/satellite.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "phonos:item/satellite" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/phonos/textures/entity/satellite/exhaust_1.png b/src/main/resources/assets/phonos/textures/entity/satellite/exhaust_1.png new file mode 100644 index 0000000000000000000000000000000000000000..c5cbbca521947ed705576669558b0c0401d14a2f GIT binary patch literal 4325 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phonos/textures/entity/satellite/satellite.png b/src/main/resources/assets/phonos/textures/entity/satellite/satellite.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5640a76ae3759ed4176b9eb5f7690cde557db5 GIT binary patch literal 5077 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 literal 0 HcmV?d00001