mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-30 21:23:00 +00:00
Split CC:T into common and forge projects
After several weeks of carefully arranging ribbons, we pull the string and end up with, ... a bit of a messy bow. There were still some things I'd missed. - Split the mod into a common (vanilla-only) project and Forge-specific project. This gives us room to add Fabric support later on. - Split the project into main/client source sets. This is not currently statically checked: we'll do that soon. - Rename block/item/tile entities to use suffixes rather than prefixes.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import com.mojang.blaze3d.audio.Channel;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.CableHighlightRenderer;
|
||||
import dan200.computercraft.client.render.PocketItemRenderer;
|
||||
import dan200.computercraft.client.render.PrintoutItemRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorHighlightRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorRenderState;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.CommonHooks;
|
||||
import dan200.computercraft.shared.command.CommandComputerCraft;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.util.PauseAwareTimer;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Event listeners for client-only code.
|
||||
* <p>
|
||||
* This is the client-only version of {@link CommonHooks}, and so should be where all client-specific event handlers are
|
||||
* defined.
|
||||
*/
|
||||
public final class ClientHooks {
|
||||
private ClientHooks() {
|
||||
}
|
||||
|
||||
public static void onTick() {
|
||||
FrameInfo.onTick();
|
||||
}
|
||||
|
||||
public static void onRenderTick() {
|
||||
PauseAwareTimer.tick(Minecraft.getInstance().isPaused());
|
||||
FrameInfo.onRenderTick();
|
||||
}
|
||||
|
||||
public static void onWorldUnload() {
|
||||
MonitorRenderState.destroyAll();
|
||||
SpeakerManager.reset();
|
||||
ClientPocketComputers.reset();
|
||||
}
|
||||
|
||||
public static boolean onChatMessage(String message) {
|
||||
return handleOpenComputerCommand(message);
|
||||
}
|
||||
|
||||
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
|
||||
return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit)
|
||||
|| MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit);
|
||||
}
|
||||
|
||||
public static boolean onRenderHeldItem(
|
||||
PoseStack transform, MultiBufferSource render, int lightTexture, InteractionHand hand,
|
||||
float pitch, float equipProgress, float swingProgress, ItemStack stack
|
||||
) {
|
||||
if (stack.getItem() instanceof PocketComputerItem) {
|
||||
PocketItemRenderer.INSTANCE.renderItemFirstPerson(transform, render, lightTexture, hand, pitch, equipProgress, swingProgress, stack);
|
||||
return true;
|
||||
}
|
||||
if (stack.getItem() instanceof PrintoutItem) {
|
||||
PrintoutItemRenderer.INSTANCE.renderItemFirstPerson(transform, render, lightTexture, hand, pitch, equipProgress, swingProgress, stack);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int light) {
|
||||
if (stack.getItem() instanceof PrintoutItem) {
|
||||
PrintoutItemRenderer.onRenderInFrame(transform, render, frame, stack, light);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void onPlayStreaming(SoundEngine engine, Channel channel, AudioStream stream) {
|
||||
SpeakerManager.onPlayStreaming(engine, channel, stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the {@link CommandComputerCraft#OPEN_COMPUTER} "clientside command". This isn't a true command, as we
|
||||
* don't want it to actually be visible to the user.
|
||||
*
|
||||
* @param message The current chat message.
|
||||
* @return Whether to cancel sending this message.
|
||||
*/
|
||||
private static boolean handleOpenComputerCommand(String message) {
|
||||
if (!message.startsWith(CommandComputerCraft.OPEN_COMPUTER)) return false;
|
||||
|
||||
var server = Minecraft.getInstance().getSingleplayerServer();
|
||||
if (server == null) return false;
|
||||
|
||||
var idStr = message.substring(CommandComputerCraft.OPEN_COMPUTER.length()).trim();
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(idStr);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
|
||||
if (!file.isDirectory()) return false;
|
||||
|
||||
Util.getPlatform().openFile(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional information about the currently targeted block to the debug screen.
|
||||
*
|
||||
* @param addText A callback which adds a single line of text.
|
||||
*/
|
||||
public static void addDebugInfo(Consumer<String> addText) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
if (!minecraft.options.renderDebug || minecraft.level == null) return;
|
||||
if (minecraft.hitResult == null || minecraft.hitResult.getType() != HitResult.Type.BLOCK) return;
|
||||
|
||||
var tile = minecraft.level.getBlockEntity(((BlockHitResult) minecraft.hitResult).getBlockPos());
|
||||
|
||||
if (tile instanceof MonitorBlockEntity monitor) {
|
||||
addText.accept("");
|
||||
addText.accept(
|
||||
String.format("Targeted monitor: (%d, %d), %d x %d", monitor.getXIndex(), monitor.getYIndex(), monitor.getWidth(), monitor.getHeight())
|
||||
);
|
||||
} else if (tile instanceof TurtleBlockEntity turtle) {
|
||||
addText.accept("");
|
||||
addText.accept("Targeted turtle:");
|
||||
addText.accept(String.format("Id: %d", turtle.getComputerID()));
|
||||
addTurtleUpgrade(addText, turtle, TurtleSide.LEFT);
|
||||
addTurtleUpgrade(addText, turtle, TurtleSide.RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addTurtleUpgrade(Consumer<String> out, TurtleBlockEntity turtle, TurtleSide side) {
|
||||
var upgrade = turtle.getUpgrade(side);
|
||||
if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.getUpgradeID()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.ComputerCraftAPIClient;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.client.gui.*;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
|
||||
import dan200.computercraft.client.turtle.TurtleModemModeller;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
|
||||
import dan200.computercraft.shared.media.items.DiskItem;
|
||||
import dan200.computercraft.shared.media.items.ItemTreasureDisk;
|
||||
import net.minecraft.client.color.item.ItemColor;
|
||||
import net.minecraft.client.gui.screens.MenuScreens;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
|
||||
import net.minecraft.client.renderer.item.ItemProperties;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.ItemLike;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
|
||||
* {@link MenuScreens.ScreenConstructor}.
|
||||
* <p>
|
||||
* The functions in this class should be called from a loader-specific class.
|
||||
*
|
||||
* @see ModRegistry The common registry for actual game objects.
|
||||
*/
|
||||
public final class ClientRegistry {
|
||||
private ClientRegistry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any client-side objects which don't have to be done on the main thread.
|
||||
*/
|
||||
public static void register() {
|
||||
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
|
||||
));
|
||||
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
|
||||
));
|
||||
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
|
||||
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
|
||||
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any client-side objects which must be done on the main thread.
|
||||
*/
|
||||
public static void registerMainThread() {
|
||||
MenuScreens.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
|
||||
MenuScreens.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER.get(), ComputerScreen::new);
|
||||
MenuScreens.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
|
||||
MenuScreens.register(ModRegistry.Menus.TURTLE.get(), TurtleScreen::new);
|
||||
|
||||
MenuScreens.register(ModRegistry.Menus.PRINTER.get(), PrinterScreen::new);
|
||||
MenuScreens.register(ModRegistry.Menus.DISK_DRIVE.get(), DiskDriveScreen::new);
|
||||
MenuScreens.register(ModRegistry.Menus.PRINTOUT.get(), PrintoutScreen::new);
|
||||
|
||||
MenuScreens.<ViewComputerMenu, ComputerScreen<ViewComputerMenu>>register(ModRegistry.Menus.VIEW_COMPUTER.get(), ComputerScreen::new);
|
||||
|
||||
registerItemProperty("state",
|
||||
new UnclampedPropertyFunction((stack, world, player, random) -> ClientPocketComputers.get(stack).getState().ordinal()),
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
|
||||
);
|
||||
registerItemProperty("coloured",
|
||||
(stack, world, player, random) -> IColouredItem.getColourBasic(stack) != -1 ? 1 : 0,
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
|
||||
);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
|
||||
var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
|
||||
for (var item : items) ItemProperties.register(item.get(), id, getter);
|
||||
}
|
||||
|
||||
private static final String[] EXTRA_MODELS = new String[]{
|
||||
// Turtle upgrades
|
||||
"block/turtle_modem_normal_off_left",
|
||||
"block/turtle_modem_normal_on_left",
|
||||
"block/turtle_modem_normal_off_right",
|
||||
"block/turtle_modem_normal_on_right",
|
||||
|
||||
"block/turtle_modem_advanced_off_left",
|
||||
"block/turtle_modem_advanced_on_left",
|
||||
"block/turtle_modem_advanced_off_right",
|
||||
"block/turtle_modem_advanced_on_right",
|
||||
|
||||
"block/turtle_crafting_table_left",
|
||||
"block/turtle_crafting_table_right",
|
||||
|
||||
"block/turtle_speaker_left",
|
||||
"block/turtle_speaker_right",
|
||||
|
||||
// Turtle block renderer
|
||||
"block/turtle_colour",
|
||||
"block/turtle_elf_overlay",
|
||||
};
|
||||
|
||||
public static void registerExtraModels(Consumer<ResourceLocation> register) {
|
||||
for (var model : EXTRA_MODELS) register.accept(new ResourceLocation(ComputerCraftAPI.MOD_ID, model));
|
||||
}
|
||||
|
||||
public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
|
||||
register.accept(
|
||||
(stack, layer) -> layer == 1 ? ((DiskItem) stack.getItem()).getColour(stack) : 0xFFFFFF,
|
||||
ModRegistry.Items.DISK.get()
|
||||
);
|
||||
|
||||
register.accept(
|
||||
(stack, layer) -> layer == 1 ? ItemTreasureDisk.getColour(stack) : 0xFFFFFF,
|
||||
ModRegistry.Items.TREASURE_DISK.get()
|
||||
);
|
||||
|
||||
register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get());
|
||||
register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
|
||||
|
||||
register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_NORMAL.get());
|
||||
register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_ADVANCED.get());
|
||||
}
|
||||
|
||||
private static int getPocketColour(ItemStack stack, int layer) {
|
||||
switch (layer) {
|
||||
case 0:
|
||||
default:
|
||||
return 0xFFFFFF;
|
||||
case 1: // Frame colour
|
||||
return IColouredItem.getColourBasic(stack);
|
||||
case 2: { // Light colour
|
||||
var light = ClientPocketComputers.get(stack).getLightState();
|
||||
return light == -1 ? Colour.BLACK.getHex() : light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int getTurtleColour(ItemStack stack, int layer) {
|
||||
return layer == 0 ? ((IColouredItem) stack.getItem()).getColour(stack) : 0xFFFFFF;
|
||||
}
|
||||
|
||||
public static void registerBlockEntityRenderers(BlockEntityRenderRegistry register) {
|
||||
register.register(ModRegistry.BlockEntities.MONITOR_NORMAL.get(), MonitorBlockEntityRenderer::new);
|
||||
register.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
|
||||
register.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new);
|
||||
register.register(ModRegistry.BlockEntities.TURTLE_ADVANCED.get(), TurtleBlockEntityRenderer::new);
|
||||
}
|
||||
|
||||
public interface BlockEntityRenderRegistry {
|
||||
<T extends BlockEntity> void register(BlockEntityType<? extends T> type, BlockEntityRendererProvider<T> provider);
|
||||
}
|
||||
|
||||
public static void registerShaders(ResourceManager resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
|
||||
RenderTypes.registerShaders(resources, load);
|
||||
}
|
||||
|
||||
private record UnclampedPropertyFunction(
|
||||
ClampedItemPropertyFunction function
|
||||
) implements ClampedItemPropertyFunction {
|
||||
@Override
|
||||
public float unclampedCall(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
|
||||
return function.unclampedCall(stack, level, entity, layer);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public float call(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
|
||||
return function.unclampedCall(stack, level, entity, layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import dan200.computercraft.shared.command.text.ChatHelpers;
|
||||
import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.command.text.TableFormatter;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.GuiMessageTag;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Mth;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ClientTableFormatter implements TableFormatter {
|
||||
public static final ClientTableFormatter INSTANCE = new ClientTableFormatter();
|
||||
|
||||
private static Font renderer() {
|
||||
return Minecraft.getInstance().font;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Component getPadding(Component component, int width) {
|
||||
var extraWidth = width - getWidth(component);
|
||||
if (extraWidth <= 0) return null;
|
||||
|
||||
var renderer = renderer();
|
||||
|
||||
float spaceWidth = renderer.width(" ");
|
||||
var spaces = Mth.floor(extraWidth / spaceWidth);
|
||||
var extra = extraWidth - (int) (spaces * spaceWidth);
|
||||
|
||||
return ChatHelpers.coloured(StringUtils.repeat(' ', spaces) + StringUtils.repeat((char) 712, extra), ChatFormatting.GRAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPadding() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth(Component component) {
|
||||
return renderer().width(component);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLine(String label, Component component) {
|
||||
var mc = Minecraft.getInstance();
|
||||
var chat = mc.gui.getChat();
|
||||
|
||||
// TODO: Trim the text if it goes over the allowed length
|
||||
// int maxWidth = MathHelper.floor( chat.getChatWidth() / chat.getScale() );
|
||||
// List<ITextProperties> list = RenderComponentsUtil.wrapComponents( component, maxWidth, mc.fontRenderer );
|
||||
// if( !list.isEmpty() ) chat.printChatMessageWithOptionalDeletion( list.get( 0 ), id );
|
||||
chat.addMessage(component, null, createTag(label));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(TableBuilder table) {
|
||||
var chat = Minecraft.getInstance().gui.getChat();
|
||||
|
||||
var tag = createTag(table.getId());
|
||||
if (chat.allMessages.removeIf(guiMessage -> guiMessage.tag() != null && Objects.equals(guiMessage.tag().logTag(), tag.logTag()))) {
|
||||
chat.refreshTrimmedMessage();
|
||||
}
|
||||
|
||||
TableFormatter.super.display(table);
|
||||
}
|
||||
|
||||
private static GuiMessageTag createTag(String id) {
|
||||
return new GuiMessageTag(0xa0a0a0, null, null, "ComputerCraft/" + id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.impl.client.ComputerCraftAPIClientService;
|
||||
|
||||
@AutoService(ComputerCraftAPIClientService.class)
|
||||
public final class ComputerCraftAPIClientImpl implements ComputerCraftAPIClientService {
|
||||
@Override
|
||||
public <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
|
||||
TurtleUpgradeModellers.register(serialiser, modeller);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client;
|
||||
|
||||
public final class FrameInfo {
|
||||
private static int tick;
|
||||
private static long renderFrame;
|
||||
|
||||
private FrameInfo() {
|
||||
}
|
||||
|
||||
public static boolean getGlobalCursorBlink() {
|
||||
return (tick / 8) % 2 == 0;
|
||||
}
|
||||
|
||||
public static long getRenderFrame() {
|
||||
return renderFrame;
|
||||
}
|
||||
|
||||
public static void onTick() {
|
||||
tick++;
|
||||
}
|
||||
|
||||
public static void onRenderTick() {
|
||||
renderFrame++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.InputHandler;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.network.server.UploadFileMessage;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> extends AbstractContainerScreen<T> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractComputerScreen.class);
|
||||
|
||||
private static final Component OK = Component.translatable("gui.ok");
|
||||
private static final Component NO_RESPONSE_TITLE = Component.translatable("gui.computercraft.upload.no_response");
|
||||
private static final Component NO_RESPONSE_MSG = Component.translatable("gui.computercraft.upload.no_response.msg",
|
||||
Component.literal("import").withStyle(ChatFormatting.DARK_GRAY));
|
||||
|
||||
protected @Nullable TerminalWidget terminal;
|
||||
protected Terminal terminalData;
|
||||
protected final ComputerFamily family;
|
||||
protected final InputHandler input;
|
||||
|
||||
protected final int sidebarYOffset;
|
||||
|
||||
private long uploadNagDeadline = Long.MAX_VALUE;
|
||||
private final ItemStack displayStack;
|
||||
|
||||
public AbstractComputerScreen(T container, Inventory player, Component title, int sidebarYOffset) {
|
||||
super(container, player, title);
|
||||
terminalData = container.getTerminal();
|
||||
family = container.getFamily();
|
||||
displayStack = container.getDisplayStack();
|
||||
input = new ClientInputHandler(menu);
|
||||
this.sidebarYOffset = sidebarYOffset;
|
||||
}
|
||||
|
||||
protected abstract TerminalWidget createTerminal();
|
||||
|
||||
protected final TerminalWidget getTerminal() {
|
||||
if (terminal == null) throw new IllegalStateException("Screen has not been initialised yet");
|
||||
return terminal;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
minecraft.keyboardHandler.setSendRepeatsToGui(true);
|
||||
|
||||
terminal = addRenderableWidget(createTerminal());
|
||||
ComputerSidebar.addButtons(this, menu::isOn, input, this::addRenderableWidget, leftPos, topPos + sidebarYOffset);
|
||||
setFocused(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed() {
|
||||
super.removed();
|
||||
minecraft.keyboardHandler.setSendRepeatsToGui(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void containerTick() {
|
||||
super.containerTick();
|
||||
getTerminal().update();
|
||||
|
||||
if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
|
||||
new ItemToast(minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
|
||||
.showOrReplace(minecraft.getToasts());
|
||||
uploadNagDeadline = Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int key, int scancode, int modifiers) {
|
||||
// Forward the tab key to the terminal, rather than moving between controls.
|
||||
if (key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminal) {
|
||||
return getFocused().keyPressed(key, scancode, modifiers);
|
||||
}
|
||||
|
||||
return super.keyPressed(key, scancode, modifiers);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void render(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
|
||||
renderBackground(stack);
|
||||
super.render(stack, mouseX, mouseY, partialTicks);
|
||||
renderTooltip(stack, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double x, double y, int button) {
|
||||
var changed = super.mouseClicked(x, y, button);
|
||||
// Clicking the terminate/shutdown button steals focus, which means then pressing "enter" will click the button
|
||||
// again. Restore the focus to the terminal in these cases.
|
||||
if (getFocused() instanceof DynamicImageButton) setFocused(terminal);
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(double x, double y, int button, double deltaX, double deltaY) {
|
||||
return (getFocused() != null && getFocused().mouseDragged(x, y, button, deltaX, deltaY))
|
||||
|| super.mouseDragged(x, y, button, deltaX, deltaY);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void renderLabels(PoseStack transform, int mouseX, int mouseY) {
|
||||
// Skip rendering labels.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFilesDrop(List<Path> files) {
|
||||
if (files.isEmpty()) return;
|
||||
|
||||
if (!menu.isOn()) {
|
||||
alert(UploadResult.FAILED_TITLE, UploadResult.COMPUTER_OFF_MSG);
|
||||
return;
|
||||
}
|
||||
|
||||
long size = 0;
|
||||
|
||||
List<FileUpload> toUpload = new ArrayList<>();
|
||||
for (var file : files) {
|
||||
// TODO: Recurse directories? If so, we probably want to shunt this off-thread.
|
||||
if (!Files.isRegularFile(file)) continue;
|
||||
|
||||
try (var sbc = Files.newByteChannel(file)) {
|
||||
var fileSize = sbc.size();
|
||||
if (fileSize > UploadFileMessage.MAX_SIZE || (size += fileSize) >= UploadFileMessage.MAX_SIZE) {
|
||||
alert(UploadResult.FAILED_TITLE, UploadResult.TOO_MUCH_MSG);
|
||||
return;
|
||||
}
|
||||
|
||||
var name = file.getFileName().toString();
|
||||
if (name.length() > UploadFileMessage.MAX_FILE_NAME) {
|
||||
alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.name_too_long"));
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = ByteBuffer.allocateDirect((int) fileSize);
|
||||
sbc.read(buffer);
|
||||
buffer.flip();
|
||||
|
||||
var digest = FileUpload.getDigest(buffer);
|
||||
if (digest == null) {
|
||||
alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.corrupted"));
|
||||
return;
|
||||
}
|
||||
|
||||
toUpload.add(new FileUpload(name, buffer, digest));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed uploading files", e);
|
||||
alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.generic", "Cannot compute checksum"));
|
||||
}
|
||||
}
|
||||
|
||||
if (toUpload.size() > UploadFileMessage.MAX_FILES) {
|
||||
alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.too_many_files"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientPlatformHelper.get()::sendToServer);
|
||||
}
|
||||
|
||||
public void uploadResult(UploadResult result, @Nullable Component message) {
|
||||
switch (result) {
|
||||
case QUEUED -> {
|
||||
if (Config.uploadNagDelay > 0) {
|
||||
uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos(Config.uploadNagDelay);
|
||||
}
|
||||
}
|
||||
case CONSUMED -> uploadNagDeadline = Long.MAX_VALUE;
|
||||
case ERROR -> alert(UploadResult.FAILED_TITLE, assertNonNull(message));
|
||||
}
|
||||
}
|
||||
|
||||
private void alert(Component title, Component message) {
|
||||
OptionScreen.show(minecraft, title, message,
|
||||
Collections.singletonList(OptionScreen.newButton(OK, b -> minecraft.setScreen(this))),
|
||||
() -> minecraft.setScreen(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.shared.computer.core.InputHandler;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
|
||||
import dan200.computercraft.shared.network.server.KeyEventServerMessage;
|
||||
import dan200.computercraft.shared.network.server.MouseEventServerMessage;
|
||||
import dan200.computercraft.shared.network.server.QueueEventServerMessage;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An {@link InputHandler} which for use on the client.
|
||||
* <p>
|
||||
* This queues events on the remote player's open {@link ComputerMenu}
|
||||
*/
|
||||
public final class ClientInputHandler implements InputHandler {
|
||||
private final AbstractContainerMenu menu;
|
||||
|
||||
public ClientInputHandler(AbstractContainerMenu menu) {
|
||||
this.menu = menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void turnOn() {
|
||||
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reboot() {
|
||||
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
ClientPlatformHelper.get().sendToServer(new QueueEventServerMessage(menu, event, arguments));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyDown(int key, boolean repeat) {
|
||||
ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyUp(int key) {
|
||||
ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClick(int button, int x, int y) {
|
||||
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseUp(int button, int x, int y) {
|
||||
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDrag(int button, int x, int y) {
|
||||
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseScroll(int direction, int x, int y) {
|
||||
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER;
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
public final class ComputerScreen<T extends AbstractComputerMenu> extends AbstractComputerScreen<T> {
|
||||
public ComputerScreen(T container, Inventory player, Component title) {
|
||||
super(container, player, title, BORDER);
|
||||
|
||||
imageWidth = TerminalWidget.getWidth(terminalData.getWidth()) + BORDER * 2 + AbstractComputerMenu.SIDEBAR_WIDTH;
|
||||
imageHeight = TerminalWidget.getHeight(terminalData.getHeight()) + BORDER * 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TerminalWidget createTerminal() {
|
||||
return new TerminalWidget(terminalData, input, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH + BORDER, topPos + BORDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) {
|
||||
// Draw a border around the terminal
|
||||
var terminal = getTerminal();
|
||||
ComputerBorderRenderer.render(
|
||||
stack.last().pose(), ComputerBorderRenderer.getTexture(family), terminal.x, terminal.y, getBlitOffset(),
|
||||
FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight()
|
||||
);
|
||||
ComputerSidebar.renderBackground(stack, leftPos, topPos + sidebarYOffset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
|
||||
public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
|
||||
private static final ResourceLocation BACKGROUND = new ResourceLocation("computercraft", "textures/gui/disk_drive.png");
|
||||
|
||||
public DiskDriveScreen(DiskDriveMenu container, Inventory player, Component title) {
|
||||
super(container, player, title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderBg(PoseStack transform, float partialTicks, int mouseX, int mouseY) {
|
||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
RenderSystem.setShaderTexture(0, BACKGROUND);
|
||||
blit(transform, leftPos, topPos, 0, 0, imageWidth, imageHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack transform, int mouseX, int mouseY, float partialTicks) {
|
||||
renderBackground(transform);
|
||||
super.render(transform, mouseX, mouseY, partialTicks);
|
||||
renderTooltip(transform, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.toasts.Toast;
|
||||
import net.minecraft.client.gui.components.toasts.ToastComponent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link Toast} implementation which displays an arbitrary message along with an optional {@link ItemStack}.
|
||||
*/
|
||||
public class ItemToast implements Toast {
|
||||
public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object();
|
||||
|
||||
private static final long DISPLAY_TIME = 7000L;
|
||||
private static final int MAX_LINE_SIZE = 200;
|
||||
|
||||
private static final int IMAGE_SIZE = 16;
|
||||
private static final int LINE_SPACING = 10;
|
||||
private static final int MARGIN = 8;
|
||||
|
||||
private final ItemStack stack;
|
||||
private final Component title;
|
||||
private final List<FormattedCharSequence> message;
|
||||
private final Object token;
|
||||
private final int width;
|
||||
|
||||
private boolean isNew = true;
|
||||
private long firstDisplay;
|
||||
|
||||
public ItemToast(Minecraft minecraft, ItemStack stack, Component title, Component message, Object token) {
|
||||
this.stack = stack;
|
||||
this.title = title;
|
||||
this.token = token;
|
||||
|
||||
var font = minecraft.font;
|
||||
this.message = font.split(message, MAX_LINE_SIZE);
|
||||
width = Math.max(MAX_LINE_SIZE, this.message.stream().mapToInt(font::width).max().orElse(MAX_LINE_SIZE)) + MARGIN * 3 + IMAGE_SIZE;
|
||||
}
|
||||
|
||||
public void showOrReplace(ToastComponent toasts) {
|
||||
var existing = toasts.getToast(ItemToast.class, getToken());
|
||||
if (existing != null) {
|
||||
existing.isNew = true;
|
||||
} else {
|
||||
toasts.addToast(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int height() {
|
||||
return MARGIN * 2 + LINE_SPACING + message.size() * LINE_SPACING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Visibility render(PoseStack transform, ToastComponent component, long time) {
|
||||
if (isNew) {
|
||||
|
||||
firstDisplay = time;
|
||||
isNew = false;
|
||||
}
|
||||
|
||||
RenderSystem.setShaderTexture(0, TEXTURE);
|
||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
|
||||
if (width == 160 && message.size() <= 1) {
|
||||
component.blit(transform, 0, 0, 0, 64, width, height());
|
||||
} else {
|
||||
|
||||
var height = height();
|
||||
|
||||
var bottom = Math.min(4, height - 28);
|
||||
renderBackgroundRow(transform, component, width, 0, 0, 28);
|
||||
|
||||
for (var i = 28; i < height - bottom; i += 10) {
|
||||
renderBackgroundRow(transform, component, width, 16, i, Math.min(16, height - i - bottom));
|
||||
}
|
||||
|
||||
renderBackgroundRow(transform, component, width, 32 - bottom, height - bottom, bottom);
|
||||
}
|
||||
|
||||
var textX = MARGIN;
|
||||
if (!stack.isEmpty()) {
|
||||
textX += MARGIN + IMAGE_SIZE;
|
||||
component.getMinecraft().getItemRenderer().renderAndDecorateFakeItem(stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE);
|
||||
}
|
||||
|
||||
component.getMinecraft().font.draw(transform, title, textX, MARGIN, 0xff500050);
|
||||
for (var i = 0; i < message.size(); ++i) {
|
||||
component.getMinecraft().font.draw(transform, message.get(i), textX, (float) (LINE_SPACING + (i + 1) * LINE_SPACING), 0xff000000);
|
||||
}
|
||||
|
||||
return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
|
||||
}
|
||||
|
||||
private static void renderBackgroundRow(PoseStack transform, ToastComponent component, int x, int u, int y, int height) {
|
||||
var leftOffset = 5;
|
||||
var rightOffset = Math.min(60, x - leftOffset);
|
||||
|
||||
component.blit(transform, 0, y, 0, 32 + u, leftOffset, height);
|
||||
for (var k = leftOffset; k < x - rightOffset; k += 64) {
|
||||
component.blit(transform, k, y, 32, 32 + u, Math.min(64, x - k - rightOffset), height);
|
||||
}
|
||||
|
||||
component.blit(transform, x - rightOffset, y, 160 - rightOffset, 32 + u, rightOffset, height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen implements MenuAccess<T> {
|
||||
private final T menu;
|
||||
private final Terminal terminalData;
|
||||
private @Nullable TerminalWidget terminal;
|
||||
|
||||
public NoTermComputerScreen(T menu, Inventory player, Component title) {
|
||||
super(title);
|
||||
this.menu = menu;
|
||||
terminalData = menu.getTerminal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getMenu() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
passEvents = true; // Pass mouse vents through to the game's mouse handler.
|
||||
// First ensure we're still grabbing the mouse, so the user can look around. Then reset bits of state that
|
||||
// grabbing unsets.
|
||||
minecraft.mouseHandler.grabMouse();
|
||||
minecraft.screen = this;
|
||||
KeyMapping.releaseAll();
|
||||
|
||||
super.init();
|
||||
minecraft.keyboardHandler.setSendRepeatsToGui(true);
|
||||
|
||||
terminal = addWidget(new TerminalWidget(terminalData, new ClientInputHandler(menu), 0, 0));
|
||||
terminal.visible = false;
|
||||
terminal.active = false;
|
||||
setFocused(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removed() {
|
||||
super.removed();
|
||||
minecraft.keyboardHandler.setSendRepeatsToGui(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void tick() {
|
||||
super.tick();
|
||||
assertNonNull(terminal).update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) {
|
||||
minecraft.player.getInventory().swapPaint(pDelta);
|
||||
return super.mouseScrolled(pMouseX, pMouseY, pDelta);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
minecraft.player.closeContainer();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean keyPressed(int key, int scancode, int modifiers) {
|
||||
// Forward the tab key to the terminal, rather than moving between controls.
|
||||
if (key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminal) {
|
||||
return getFocused().keyPressed(key, scancode, modifiers);
|
||||
}
|
||||
|
||||
return super.keyPressed(key, scancode, modifiers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack transform, int mouseX, int mouseY, float partialTicks) {
|
||||
super.render(transform, mouseX, mouseY, partialTicks);
|
||||
|
||||
var font = minecraft.font;
|
||||
var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8));
|
||||
var y = 10.0f;
|
||||
for (var line : lines) {
|
||||
font.drawShadow(transform, line, (float) ((width / 2) - (minecraft.font.width(line) / 2)), y, 0xFFFFFF);
|
||||
y += 9.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.MultiLineLabel;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public final class OptionScreen extends Screen {
|
||||
private static final ResourceLocation BACKGROUND = new ResourceLocation("computercraft", "textures/gui/blank_screen.png");
|
||||
|
||||
public static final int BUTTON_WIDTH = 100;
|
||||
public static final int BUTTON_HEIGHT = 20;
|
||||
|
||||
private static final int PADDING = 16;
|
||||
private static final int FONT_HEIGHT = 9;
|
||||
|
||||
private int x;
|
||||
private int y;
|
||||
private int innerWidth;
|
||||
private int innerHeight;
|
||||
|
||||
private @Nullable MultiLineLabel messageRenderer;
|
||||
private final Component message;
|
||||
private final List<AbstractWidget> buttons;
|
||||
private final Runnable exit;
|
||||
|
||||
private final Screen originalScreen;
|
||||
|
||||
private OptionScreen(Component title, Component message, List<AbstractWidget> buttons, Runnable exit, Screen originalScreen) {
|
||||
super(title);
|
||||
this.message = message;
|
||||
this.buttons = buttons;
|
||||
this.exit = exit;
|
||||
this.originalScreen = originalScreen;
|
||||
}
|
||||
|
||||
public static void show(Minecraft minecraft, Component title, Component message, List<AbstractWidget> buttons, Runnable exit) {
|
||||
minecraft.setScreen(new OptionScreen(title, message, buttons, exit, unwrap(minecraft.screen)));
|
||||
}
|
||||
|
||||
public static Screen unwrap(Screen screen) {
|
||||
return screen instanceof OptionScreen option ? option.getOriginalScreen() : screen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
super.init();
|
||||
|
||||
var buttonWidth = BUTTON_WIDTH * buttons.size() + PADDING * (buttons.size() - 1);
|
||||
var innerWidth = this.innerWidth = Math.max(256, buttonWidth + PADDING * 2);
|
||||
|
||||
messageRenderer = MultiLineLabel.create(font, message, innerWidth - PADDING * 2);
|
||||
|
||||
var textHeight = messageRenderer.getLineCount() * FONT_HEIGHT + PADDING * 2;
|
||||
innerHeight = textHeight + (buttons.isEmpty() ? 0 : buttons.get(0).getHeight()) + PADDING;
|
||||
|
||||
x = (width - innerWidth) / 2;
|
||||
y = (height - innerHeight) / 2;
|
||||
|
||||
var x = (width - buttonWidth) / 2;
|
||||
for (var button : buttons) {
|
||||
button.x = x;
|
||||
button.y = y + textHeight;
|
||||
addRenderableWidget(button);
|
||||
|
||||
x += BUTTON_WIDTH + PADDING;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack transform, int mouseX, int mouseY, float partialTicks) {
|
||||
renderBackground(transform);
|
||||
|
||||
// Render the actual texture.
|
||||
RenderSystem.setShaderTexture(0, BACKGROUND);
|
||||
blit(transform, x, y, 0, 0, innerWidth, PADDING);
|
||||
blit(transform,
|
||||
x, y + PADDING, 0, PADDING, innerWidth, innerHeight - PADDING * 2,
|
||||
innerWidth, PADDING
|
||||
);
|
||||
blit(transform, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING);
|
||||
|
||||
assertNonNull(messageRenderer).renderLeftAlignedNoShadow(transform, x + PADDING, y + PADDING, FONT_HEIGHT, 0x404040);
|
||||
super.render(transform, mouseX, mouseY, partialTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
exit.run();
|
||||
}
|
||||
|
||||
public static AbstractWidget newButton(Component component, Button.OnPress clicked) {
|
||||
return new Button(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, component, clicked);
|
||||
}
|
||||
|
||||
public void disable() {
|
||||
for (var widget : buttons) widget.active = false;
|
||||
}
|
||||
|
||||
public Screen getOriginalScreen() {
|
||||
return originalScreen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterMenu;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
|
||||
public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
|
||||
private static final ResourceLocation BACKGROUND = new ResourceLocation("computercraft", "textures/gui/printer.png");
|
||||
|
||||
public PrinterScreen(PrinterMenu container, Inventory player, Component title) {
|
||||
super(container, player, title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderBg(PoseStack transform, float partialTicks, int mouseX, int mouseY) {
|
||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
RenderSystem.setShaderTexture(0, BACKGROUND);
|
||||
blit(transform, leftPos, topPos, 0, 0, imageWidth, imageHeight);
|
||||
|
||||
if (getMenu().isPrinting()) blit(transform, leftPos + 34, topPos + 21, 176, 0, 25, 45);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
|
||||
renderBackground(stack);
|
||||
super.render(stack, mouseX, mouseY, partialTicks);
|
||||
renderTooltip(stack, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
public class PrintoutScreen extends AbstractContainerScreen<HeldItemMenu> {
|
||||
private final boolean book;
|
||||
private final int pages;
|
||||
private final TextBuffer[] text;
|
||||
private final TextBuffer[] colours;
|
||||
private int page;
|
||||
|
||||
public PrintoutScreen(HeldItemMenu container, Inventory player, Component title) {
|
||||
super(container, player, title);
|
||||
|
||||
imageHeight = Y_SIZE;
|
||||
|
||||
var text = PrintoutItem.getText(container.getStack());
|
||||
this.text = new TextBuffer[text.length];
|
||||
for (var i = 0; i < this.text.length; i++) this.text[i] = new TextBuffer(text[i]);
|
||||
|
||||
var colours = PrintoutItem.getColours(container.getStack());
|
||||
this.colours = new TextBuffer[colours.length];
|
||||
for (var i = 0; i < this.colours.length; i++) this.colours[i] = new TextBuffer(colours[i]);
|
||||
|
||||
page = 0;
|
||||
pages = Math.max(this.text.length / PrintoutItem.LINES_PER_PAGE, 1);
|
||||
book = ((PrintoutItem) container.getStack().getItem()).getType() == PrintoutItem.Type.BOOK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int key, int scancode, int modifiers) {
|
||||
if (super.keyPressed(key, scancode, modifiers)) return true;
|
||||
|
||||
if (key == GLFW.GLFW_KEY_RIGHT) {
|
||||
if (page < pages - 1) page++;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key == GLFW.GLFW_KEY_LEFT) {
|
||||
if (page > 0) page--;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double x, double y, double delta) {
|
||||
if (super.mouseScrolled(x, y, delta)) return true;
|
||||
if (delta < 0) {
|
||||
// Scroll up goes to the next page
|
||||
if (page < pages - 1) page++;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (delta > 0) {
|
||||
// Scroll down goes to the previous page
|
||||
if (page > 0) page--;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderBg(PoseStack transform, float partialTicks, int mouseX, int mouseY) {
|
||||
// Draw the printout
|
||||
RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
RenderSystem.enableDepthTest();
|
||||
|
||||
var renderer = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
|
||||
drawBorder(transform, renderer, leftPos, topPos, getBlitOffset(), page, pages, book, FULL_BRIGHT_LIGHTMAP);
|
||||
drawText(transform, renderer, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutItem.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, text, colours);
|
||||
renderer.endBatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
|
||||
// We must take the background further back in order to not overlap with our printed pages.
|
||||
setBlitOffset(getBlitOffset() - 1);
|
||||
renderBackground(stack);
|
||||
setBlitOffset(getBlitOffset() + 1);
|
||||
|
||||
super.render(stack, mouseX, mouseY, partialTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderLabels(PoseStack transform, int mouseX, int mouseY) {
|
||||
// Skip rendering labels.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
import static dan200.computercraft.shared.turtle.inventory.TurtleMenu.*;
|
||||
|
||||
public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
|
||||
private static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
|
||||
|
||||
private static final int TEX_WIDTH = 254;
|
||||
private static final int TEX_HEIGHT = 217;
|
||||
|
||||
private final ComputerFamily family;
|
||||
|
||||
public TurtleScreen(TurtleMenu container, Inventory player, Component title) {
|
||||
super(container, player, title, BORDER);
|
||||
family = container.getFamily();
|
||||
|
||||
imageWidth = TEX_WIDTH + AbstractComputerMenu.SIDEBAR_WIDTH;
|
||||
imageHeight = TEX_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TerminalWidget createTerminal() {
|
||||
return new TerminalWidget(terminalData, input, leftPos + BORDER + AbstractComputerMenu.SIDEBAR_WIDTH, topPos + BORDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderBg(PoseStack transform, float partialTicks, int mouseX, int mouseY) {
|
||||
var advanced = family == ComputerFamily.ADVANCED;
|
||||
RenderSystem.setShaderTexture(0, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL);
|
||||
blit(transform, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0, TEX_WIDTH, TEX_HEIGHT);
|
||||
|
||||
var slot = getMenu().getSelectedSlot();
|
||||
if (slot >= 0) {
|
||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
var slotX = slot % 4;
|
||||
var slotY = slot / 4;
|
||||
blit(transform,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18,
|
||||
0, 217, 24, 24
|
||||
);
|
||||
}
|
||||
|
||||
RenderSystem.setShaderTexture(0, advanced ? ComputerBorderRenderer.BACKGROUND_ADVANCED : ComputerBorderRenderer.BACKGROUND_NORMAL);
|
||||
ComputerSidebar.renderBackground(transform, leftPos, topPos + sidebarYOffset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.shared.computer.core.InputHandler;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Registers buttons to interact with a computer.
|
||||
*/
|
||||
public final class ComputerSidebar {
|
||||
private static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/buttons.png");
|
||||
|
||||
private static final int TEX_SIZE = 64;
|
||||
|
||||
private static final int ICON_WIDTH = 12;
|
||||
private static final int ICON_HEIGHT = 12;
|
||||
private static final int ICON_MARGIN = 2;
|
||||
|
||||
private static final int ICON_TEX_Y_DIFF = 14;
|
||||
|
||||
private static final int CORNERS_BORDER = 3;
|
||||
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
|
||||
|
||||
private static final int BUTTONS = 2;
|
||||
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
|
||||
|
||||
private ComputerSidebar() {
|
||||
}
|
||||
|
||||
public static void addButtons(Screen screen, BooleanSupplier isOn, InputHandler input, Consumer<AbstractWidget> add, int x, int y) {
|
||||
x += CORNERS_BORDER + 1;
|
||||
y += CORNERS_BORDER + ICON_MARGIN;
|
||||
|
||||
add.accept(new DynamicImageButton(
|
||||
screen, x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF,
|
||||
TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input),
|
||||
() -> isOn.getAsBoolean() ? Arrays.asList(
|
||||
Component.translatable("gui.computercraft.tooltip.turn_off"),
|
||||
Component.translatable("gui.computercraft.tooltip.turn_off.key").withStyle(ChatFormatting.GRAY)
|
||||
) : Collections.singletonList(
|
||||
Component.translatable("gui.computercraft.tooltip.turn_on")
|
||||
)
|
||||
));
|
||||
|
||||
y += ICON_HEIGHT + ICON_MARGIN * 2;
|
||||
|
||||
add.accept(new DynamicImageButton(
|
||||
screen, x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF,
|
||||
TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"),
|
||||
Arrays.asList(
|
||||
Component.translatable("gui.computercraft.tooltip.terminate"),
|
||||
Component.translatable("gui.computercraft.tooltip.terminate.key").withStyle(ChatFormatting.GRAY)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
public static void renderBackground(PoseStack transform, int x, int y) {
|
||||
Screen.blit(transform,
|
||||
x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER,
|
||||
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
|
||||
);
|
||||
|
||||
Screen.blit(transform,
|
||||
x, y + FULL_BORDER, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT - FULL_BORDER * 2,
|
||||
0, 107, AbstractComputerMenu.SIDEBAR_WIDTH, 4,
|
||||
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
|
||||
);
|
||||
|
||||
Screen.blit(transform,
|
||||
x, y + HEIGHT - FULL_BORDER, 0, 111, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER,
|
||||
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {
|
||||
if (isOn.getAsBoolean()) {
|
||||
input.shutdown();
|
||||
} else {
|
||||
input.turnOn();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Version of {@link net.minecraft.client.gui.components.ImageButton} which allows changing some properties
|
||||
* dynamically.
|
||||
*/
|
||||
public class DynamicImageButton extends Button {
|
||||
private final Screen screen;
|
||||
private final ResourceLocation texture;
|
||||
private final IntSupplier xTexStart;
|
||||
private final int yTexStart;
|
||||
private final int yDiffTex;
|
||||
private final int textureWidth;
|
||||
private final int textureHeight;
|
||||
private final Supplier<List<Component>> tooltip;
|
||||
|
||||
public DynamicImageButton(
|
||||
Screen screen, int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex,
|
||||
ResourceLocation texture, int textureWidth, int textureHeight,
|
||||
OnPress onPress, List<Component> tooltip
|
||||
) {
|
||||
this(
|
||||
screen, x, y, width, height, () -> xTexStart, yTexStart, yDiffTex,
|
||||
texture, textureWidth, textureHeight,
|
||||
onPress, () -> tooltip
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public DynamicImageButton(
|
||||
Screen screen, int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex,
|
||||
ResourceLocation texture, int textureWidth, int textureHeight,
|
||||
OnPress onPress, Supplier<List<Component>> tooltip
|
||||
) {
|
||||
super(x, y, width, height, Component.empty(), onPress);
|
||||
this.screen = screen;
|
||||
this.textureWidth = textureWidth;
|
||||
this.textureHeight = textureHeight;
|
||||
this.xTexStart = xTexStart;
|
||||
this.yTexStart = yTexStart;
|
||||
this.yDiffTex = yDiffTex;
|
||||
this.texture = texture;
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderButton(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
|
||||
RenderSystem.setShaderTexture(0, texture);
|
||||
RenderSystem.disableDepthTest();
|
||||
|
||||
var yTex = yTexStart;
|
||||
if (isHoveredOrFocused()) yTex += yDiffTex;
|
||||
|
||||
blit(stack, x, y, xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight);
|
||||
RenderSystem.enableDepthTest();
|
||||
|
||||
if (isHovered) renderToolTip(stack, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getMessage() {
|
||||
var tooltip = this.tooltip.get();
|
||||
return tooltip.isEmpty() ? Component.empty() : tooltip.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderToolTip(PoseStack stack, int mouseX, int mouseY) {
|
||||
var tooltip = this.tooltip.get();
|
||||
if (!tooltip.isEmpty()) {
|
||||
screen.renderComponentTooltip(stack, tooltip, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.computer.core.InputHandler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
|
||||
public class TerminalWidget extends AbstractWidget {
|
||||
private static final float TERMINATE_TIME = 0.5f;
|
||||
|
||||
private final Terminal terminal;
|
||||
private final InputHandler computer;
|
||||
|
||||
// The positions of the actual terminal
|
||||
private final int innerX;
|
||||
private final int innerY;
|
||||
private final int innerWidth;
|
||||
private final int innerHeight;
|
||||
|
||||
private float terminateTimer = -1;
|
||||
private float rebootTimer = -1;
|
||||
private float shutdownTimer = -1;
|
||||
|
||||
private int lastMouseButton = -1;
|
||||
private int lastMouseX = -1;
|
||||
private int lastMouseY = -1;
|
||||
|
||||
private final BitSet keysDown = new BitSet(256);
|
||||
|
||||
public TerminalWidget(Terminal terminal, InputHandler computer, int x, int y) {
|
||||
super(x, y, terminal.getWidth() * FONT_WIDTH + MARGIN * 2, terminal.getHeight() * FONT_HEIGHT + MARGIN * 2, Component.empty());
|
||||
|
||||
this.terminal = terminal;
|
||||
this.computer = computer;
|
||||
|
||||
innerX = x + MARGIN;
|
||||
innerY = y + MARGIN;
|
||||
innerWidth = terminal.getWidth() * FONT_WIDTH;
|
||||
innerHeight = terminal.getHeight() * FONT_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean charTyped(char ch, int modifiers) {
|
||||
if (ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255) {
|
||||
// Queue the char event for any printable chars in byte range
|
||||
computer.queueEvent("char", new Object[]{ Character.toString(ch) });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int key, int scancode, int modifiers) {
|
||||
if (key == GLFW.GLFW_KEY_ESCAPE) return false;
|
||||
if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) {
|
||||
switch (key) {
|
||||
case GLFW.GLFW_KEY_T -> {
|
||||
if (terminateTimer < 0) terminateTimer = 0;
|
||||
return true;
|
||||
}
|
||||
case GLFW.GLFW_KEY_S -> {
|
||||
if (shutdownTimer < 0) shutdownTimer = 0;
|
||||
return true;
|
||||
}
|
||||
case GLFW.GLFW_KEY_R -> {
|
||||
if (rebootTimer < 0) rebootTimer = 0;
|
||||
return true;
|
||||
}
|
||||
case GLFW.GLFW_KEY_V -> {
|
||||
// Ctrl+V for paste
|
||||
var clipboard = Minecraft.getInstance().keyboardHandler.getClipboard();
|
||||
if (clipboard != null) {
|
||||
// Clip to the first occurrence of \r or \n
|
||||
var newLineIndex1 = clipboard.indexOf("\r");
|
||||
var newLineIndex2 = clipboard.indexOf("\n");
|
||||
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
|
||||
clipboard = clipboard.substring(0, Math.min(newLineIndex1, newLineIndex2));
|
||||
} else if (newLineIndex1 >= 0) {
|
||||
clipboard = clipboard.substring(0, newLineIndex1);
|
||||
} else if (newLineIndex2 >= 0) {
|
||||
clipboard = clipboard.substring(0, newLineIndex2);
|
||||
}
|
||||
|
||||
// Filter the string
|
||||
clipboard = SharedConstants.filterText(clipboard);
|
||||
if (!clipboard.isEmpty()) {
|
||||
// Clip to 512 characters and queue the event
|
||||
if (clipboard.length() > 512) clipboard = clipboard.substring(0, 512);
|
||||
computer.queueEvent("paste", new Object[]{ clipboard });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key >= 0 && terminateTimer < 0 && rebootTimer < 0 && shutdownTimer < 0) {
|
||||
// Queue the "key" event and add to the down set
|
||||
var repeat = keysDown.get(key);
|
||||
keysDown.set(key);
|
||||
computer.keyDown(key, repeat);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyReleased(int key, int scancode, int modifiers) {
|
||||
// Queue the "key_up" event and remove from the down set
|
||||
if (key >= 0 && keysDown.get(key)) {
|
||||
keysDown.set(key, false);
|
||||
computer.keyUp(key);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case GLFW.GLFW_KEY_T -> terminateTimer = -1;
|
||||
case GLFW.GLFW_KEY_R -> rebootTimer = -1;
|
||||
case GLFW.GLFW_KEY_S -> shutdownTimer = -1;
|
||||
case GLFW.GLFW_KEY_LEFT_CONTROL, GLFW.GLFW_KEY_RIGHT_CONTROL ->
|
||||
terminateTimer = rebootTimer = shutdownTimer = -1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (!inTermRegion(mouseX, mouseY)) return false;
|
||||
if (!hasMouseSupport() || button < 0 || button > 2) return false;
|
||||
|
||||
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
|
||||
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
|
||||
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
|
||||
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
|
||||
|
||||
computer.mouseClick(button + 1, charX + 1, charY + 1);
|
||||
|
||||
lastMouseButton = button;
|
||||
lastMouseX = charX;
|
||||
lastMouseY = charY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseReleased(double mouseX, double mouseY, int button) {
|
||||
if (!inTermRegion(mouseX, mouseY)) return false;
|
||||
if (!hasMouseSupport() || button < 0 || button > 2) return false;
|
||||
|
||||
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
|
||||
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
|
||||
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
|
||||
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
|
||||
|
||||
if (lastMouseButton == button) {
|
||||
computer.mouseUp(lastMouseButton + 1, charX + 1, charY + 1);
|
||||
lastMouseButton = -1;
|
||||
}
|
||||
|
||||
lastMouseX = charX;
|
||||
lastMouseY = charY;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(double mouseX, double mouseY, int button, double v2, double v3) {
|
||||
if (!inTermRegion(mouseX, mouseY)) return false;
|
||||
if (!hasMouseSupport() || button < 0 || button > 2) return false;
|
||||
|
||||
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
|
||||
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
|
||||
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
|
||||
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
|
||||
|
||||
if (button == lastMouseButton && (charX != lastMouseX || charY != lastMouseY)) {
|
||||
computer.mouseDrag(button + 1, charX + 1, charY + 1);
|
||||
lastMouseX = charX;
|
||||
lastMouseY = charY;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
|
||||
if (!inTermRegion(mouseX, mouseY)) return false;
|
||||
if (!hasMouseSupport() || delta == 0) return false;
|
||||
|
||||
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
|
||||
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
|
||||
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
|
||||
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
|
||||
|
||||
computer.mouseScroll(delta < 0 ? 1 : -1, charX + 1, charY + 1);
|
||||
|
||||
lastMouseX = charX;
|
||||
lastMouseY = charY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean inTermRegion(double mouseX, double mouseY) {
|
||||
return active && visible && mouseX >= innerX && mouseY >= innerY && mouseX < innerX + innerWidth && mouseY < innerY + innerHeight;
|
||||
}
|
||||
|
||||
private boolean hasMouseSupport() {
|
||||
return terminal.isColour();
|
||||
}
|
||||
|
||||
public void update() {
|
||||
if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) {
|
||||
computer.queueEvent("terminate");
|
||||
}
|
||||
|
||||
if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) {
|
||||
computer.shutdown();
|
||||
}
|
||||
|
||||
if (rebootTimer >= 0 && rebootTimer < TERMINATE_TIME && (rebootTimer += 0.05f) > TERMINATE_TIME) {
|
||||
computer.reboot();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusedChanged(boolean focused) {
|
||||
if (!focused) {
|
||||
// When blurring, we should make all keys go up
|
||||
for (var key = 0; key < keysDown.size(); key++) {
|
||||
if (keysDown.get(key)) computer.keyUp(key);
|
||||
}
|
||||
keysDown.clear();
|
||||
|
||||
// When blurring, we should make the last mouse button go up
|
||||
if (lastMouseButton > 0) {
|
||||
computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1);
|
||||
lastMouseButton = -1;
|
||||
}
|
||||
|
||||
shutdownTimer = terminateTimer = rebootTimer = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack transform, int mouseX, int mouseY, float partialTicks) {
|
||||
if (!visible) return;
|
||||
|
||||
var bufferSource = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
|
||||
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
|
||||
|
||||
FixedWidthFontRenderer.drawTerminal(
|
||||
emitter,
|
||||
(float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
|
||||
);
|
||||
|
||||
bufferSource.endBatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNarration(NarrationElementOutput output) {
|
||||
// I'm not sure what the right option is here.
|
||||
}
|
||||
|
||||
public static int getWidth(int termWidth) {
|
||||
return termWidth * FONT_WIDTH + MARGIN * 2;
|
||||
}
|
||||
|
||||
public static int getHeight(int termHeight) {
|
||||
return termHeight * FONT_HEIGHT + MARGIN * 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.integration;
|
||||
|
||||
/**
|
||||
* Detect whether Optifine is installed.
|
||||
*/
|
||||
public final class Optifine {
|
||||
private static final boolean LOADED;
|
||||
|
||||
static {
|
||||
boolean loaded;
|
||||
try {
|
||||
Class.forName("optifine.Installer", false, Optifine.class.getClassLoader());
|
||||
loaded = true;
|
||||
} catch (ReflectiveOperationException | LinkageError ignore) {
|
||||
loaded = false;
|
||||
}
|
||||
|
||||
LOADED = loaded;
|
||||
}
|
||||
|
||||
private Optifine() {
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return LOADED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.integration;
|
||||
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
/**
|
||||
* Find the currently loaded shader mod (if present) and provides utilities for interacting with it.
|
||||
*/
|
||||
public class ShaderMod {
|
||||
public static ShaderMod get() {
|
||||
return Storage.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if shaders may be used in the current session.
|
||||
*
|
||||
* @return Whether a shader mod is loaded.
|
||||
*/
|
||||
public boolean isShaderMod() {
|
||||
return Optifine.isLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we're currently rendering shadows. Rendering may fall back to a faster but less detailed pass.
|
||||
*
|
||||
* @return Whether we're rendering shadows.
|
||||
*/
|
||||
public boolean isRenderingShadowPass() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an appropriate quad emitter for use with {@link DirectVertexBuffer} and {@link DirectFixedWidthFontRenderer} .
|
||||
*
|
||||
* @param vertexCount The number of vertices.
|
||||
* @param makeBuffer A function to allocate a temporary buffer.
|
||||
* @return The quad emitter.
|
||||
*/
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(
|
||||
makeBuffer.apply(RenderTypes.TERMINAL.format().getVertexSize() * vertexCount * 4)
|
||||
);
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
Optional<ShaderMod> get();
|
||||
}
|
||||
|
||||
private static class Storage {
|
||||
static final ShaderMod INSTANCE = ServiceLoader.load(Provider.class)
|
||||
.stream()
|
||||
.flatMap(x -> x.get().get().stream())
|
||||
.findFirst()
|
||||
.orElseGet(ShaderMod::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.model.turtle;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import dan200.computercraft.shared.util.HolidayUtil;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Combines several individual models together to form a turtle.
|
||||
*/
|
||||
public final class TurtleModelParts {
|
||||
private static final Transformation identity, flip;
|
||||
|
||||
static {
|
||||
var stack = new PoseStack();
|
||||
stack.translate(0.5f, 0.5f, 0.5f);
|
||||
stack.scale(1, -1, 1);
|
||||
stack.translate(-0.5f, -0.5f, -0.5f);
|
||||
|
||||
identity = Transformation.identity();
|
||||
flip = new Transformation(stack.last().pose());
|
||||
}
|
||||
|
||||
public record Combination(
|
||||
boolean colour,
|
||||
@Nullable ITurtleUpgrade leftUpgrade,
|
||||
@Nullable ITurtleUpgrade rightUpgrade,
|
||||
@Nullable ResourceLocation overlay,
|
||||
boolean christmas,
|
||||
boolean flip
|
||||
) {
|
||||
}
|
||||
|
||||
private final BakedModel familyModel;
|
||||
private final BakedModel colourModel;
|
||||
private final Function<TransformedModel, BakedModel> transformer;
|
||||
|
||||
/**
|
||||
* A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed
|
||||
* instances, reducing memory usage and hopefully ensuring their caches are hit more often!
|
||||
*/
|
||||
private final Map<TransformedModel, BakedModel> transformCache = new HashMap<>();
|
||||
|
||||
public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer) {
|
||||
this.familyModel = familyModel;
|
||||
this.colourModel = colourModel;
|
||||
this.transformer = x -> transformer.transform(x.getModel(), x.getMatrix());
|
||||
}
|
||||
|
||||
public Combination getCombination(ItemStack stack) {
|
||||
var turtle = (TurtleItem) stack.getItem();
|
||||
|
||||
var colour = turtle.getColour(stack);
|
||||
var leftUpgrade = turtle.getUpgrade(stack, TurtleSide.LEFT);
|
||||
var rightUpgrade = turtle.getUpgrade(stack, TurtleSide.RIGHT);
|
||||
var overlay = turtle.getOverlay(stack);
|
||||
var christmas = HolidayUtil.getCurrentHoliday() == Holiday.CHRISTMAS;
|
||||
var label = turtle.getLabel(stack);
|
||||
var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm"));
|
||||
|
||||
return new Combination(colour != -1, leftUpgrade, rightUpgrade, overlay, christmas, flip);
|
||||
}
|
||||
|
||||
public List<BakedModel> buildModel(Combination combo) {
|
||||
var mc = Minecraft.getInstance();
|
||||
var modelManager = mc.getItemRenderer().getItemModelShaper().getModelManager();
|
||||
|
||||
var transformation = combo.flip ? flip : identity;
|
||||
var parts = new ArrayList<BakedModel>(4);
|
||||
parts.add(transform(combo.colour() ? colourModel : familyModel, transformation));
|
||||
|
||||
var overlayModelLocation = TurtleBlockEntityRenderer.getTurtleOverlayModel(combo.overlay(), combo.christmas());
|
||||
if (overlayModelLocation != null) {
|
||||
parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, overlayModelLocation), transformation));
|
||||
}
|
||||
if (combo.leftUpgrade() != null) {
|
||||
var model = TurtleUpgradeModellers.getModel(combo.leftUpgrade(), null, TurtleSide.LEFT);
|
||||
parts.add(transform(model.getModel(), transformation.compose(model.getMatrix())));
|
||||
}
|
||||
if (combo.rightUpgrade() != null) {
|
||||
var model = TurtleUpgradeModellers.getModel(combo.rightUpgrade(), null, TurtleSide.RIGHT);
|
||||
parts.add(transform(model.getModel(), transformation.compose(model.getMatrix())));
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public BakedModel transform(BakedModel model, Transformation transformation) {
|
||||
if (transformation.equals(Transformation.identity())) return model;
|
||||
return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer);
|
||||
}
|
||||
|
||||
public interface ModelTransformer {
|
||||
BakedModel transform(BakedModel model, Transformation transformation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import dan200.computercraft.client.ClientTableFormatter;
|
||||
import dan200.computercraft.client.gui.AbstractComputerScreen;
|
||||
import dan200.computercraft.client.gui.OptionScreen;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* The base implementation of {@link ClientNetworkContext}.
|
||||
* <p>
|
||||
* This should be extended by mod loader specific modules with the remaining abstract methods.
|
||||
*/
|
||||
public abstract class AbstractClientNetworkContext implements ClientNetworkContext {
|
||||
@Override
|
||||
public final void handleChatTable(TableBuilder table) {
|
||||
ClientTableFormatter.INSTANCE.display(table);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleComputerTerminal(int containerId, TerminalState terminal) {
|
||||
Player player = Minecraft.getInstance().player;
|
||||
if (player != null && player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
|
||||
menu.updateTerminal(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleMonitorData(BlockPos pos, TerminalState terminal) {
|
||||
var player = Minecraft.getInstance().player;
|
||||
if (player == null) return;
|
||||
|
||||
var te = player.level.getBlockEntity(pos);
|
||||
if (!(te instanceof MonitorBlockEntity monitor)) return;
|
||||
|
||||
monitor.read(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
|
||||
var computer = ClientPocketComputers.get(instanceId, terminal.colour);
|
||||
computer.setState(state, lightState);
|
||||
if (terminal.hasTerminal()) computer.setTerminal(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handlePocketComputerDeleted(int instanceId) {
|
||||
ClientPocketComputers.remove(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume) {
|
||||
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleSpeakerAudioPush(UUID source, ByteBuf buffer) {
|
||||
SpeakerManager.getSound(source).pushAudio(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
|
||||
SpeakerManager.moveSound(source, reifyPosition(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
|
||||
SpeakerManager.getSound(source).playSound(reifyPosition(position), sound, volume, pitch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleSpeakerStop(UUID source) {
|
||||
SpeakerManager.stopSound(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
|
||||
var screen = OptionScreen.unwrap(minecraft.screen);
|
||||
if (screen instanceof AbstractComputerScreen<?> && ((AbstractComputerScreen<?>) screen).getMenu().containerId == containerId) {
|
||||
((AbstractComputerScreen<?>) screen).uploadResult(result, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static SpeakerPosition reifyPosition(SpeakerPosition.Message pos) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
Level level = minecraft.level;
|
||||
if (level != null && !level.dimension().location().equals(pos.level())) level = null;
|
||||
|
||||
return new SpeakerPosition(
|
||||
level, pos.position(),
|
||||
level != null && pos.entity().isPresent() ? level.getEntity(pos.entity().getAsInt()) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworkContext;
|
||||
|
||||
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
|
||||
static ClientPlatformHelper get() {
|
||||
return (ClientPlatformHelper) dan200.computercraft.impl.client.ClientPlatformHelper.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a network message to the server.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void sendToServer(NetworkMessage<ServerNetworkContext> message);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.pocket;
|
||||
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.items.ComputerItem;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Maps {@link ServerComputer#getInstanceID()} to locals {@link PocketComputerData}.
|
||||
* <p>
|
||||
* This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
|
||||
*/
|
||||
public final class ClientPocketComputers {
|
||||
private static final Int2ObjectMap<PocketComputerData> instances = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
private ClientPocketComputers() {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
public static void remove(int id) {
|
||||
instances.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a pocket computer.
|
||||
*
|
||||
* @param instanceId The instance ID of the pocket computer.
|
||||
* @param advanced Whether this computer has an advanced terminal.
|
||||
* @return The pocket computer data.
|
||||
*/
|
||||
public static PocketComputerData get(int instanceId, boolean advanced) {
|
||||
var computer = instances.get(instanceId);
|
||||
if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced));
|
||||
return computer;
|
||||
}
|
||||
|
||||
public static PocketComputerData get(ItemStack stack) {
|
||||
var family = stack.getItem() instanceof ComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL;
|
||||
return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.pocket;
|
||||
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
|
||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||
|
||||
/**
|
||||
* Clientside data about a pocket computer.
|
||||
* <p>
|
||||
* Normal computers don't store any state long-term on the client - everything is tied to the container and only synced
|
||||
* while the UI is open. Pocket computers are a little more complex, as their on/off state is visible on the item's
|
||||
* texture, and the terminal can be viewed at any time. This class is what holds this needed data clientside.
|
||||
*
|
||||
* @see ClientPocketComputers The registry which holds pocket computers.
|
||||
* @see PocketServerComputer The server-side pocket computer.
|
||||
*/
|
||||
public class PocketComputerData {
|
||||
private final NetworkedTerminal terminal;
|
||||
private ComputerState state = ComputerState.OFF;
|
||||
private int lightColour = -1;
|
||||
|
||||
public PocketComputerData(boolean colour) {
|
||||
terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour);
|
||||
}
|
||||
|
||||
public int getLightState() {
|
||||
return state != ComputerState.OFF ? lightColour : -1;
|
||||
}
|
||||
|
||||
public Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
public ComputerState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(ComputerState state, int lightColour) {
|
||||
this.state = state;
|
||||
this.lightColour = lightColour;
|
||||
}
|
||||
|
||||
public void setTerminal(TerminalState state) {
|
||||
state.apply(terminal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
|
||||
import dan200.computercraft.shared.util.WorldUtil;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class CableHighlightRenderer {
|
||||
private CableHighlightRenderer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an outline for a specific part of a cable "Multipart".
|
||||
*
|
||||
* @param transform The current transformation matrix.
|
||||
* @param bufferSource The buffer to draw to.
|
||||
* @param camera The current camera.
|
||||
* @param hit The block hit result for the current player.
|
||||
* @return If we rendered a custom outline.
|
||||
* @see net.minecraft.client.renderer.LevelRenderer#renderHitOutline
|
||||
*/
|
||||
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
|
||||
var pos = hit.getBlockPos();
|
||||
var world = camera.getEntity().getCommandSenderWorld();
|
||||
|
||||
var state = world.getBlockState(pos);
|
||||
|
||||
// We only care about instances with both cable and modem.
|
||||
if (state.getBlock() != ModRegistry.Blocks.CABLE.get() || state.getValue(CableBlock.MODEM).getFacing() == null || !state.getValue(CableBlock.CABLE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var shape = WorldUtil.isVecInside(CableShapes.getModemShape(state), hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()))
|
||||
? CableShapes.getModemShape(state)
|
||||
: CableShapes.getCableShape(state);
|
||||
|
||||
var cameraPos = camera.getPosition();
|
||||
var xOffset = pos.getX() - cameraPos.x();
|
||||
var yOffset = pos.getY() - cameraPos.y();
|
||||
var zOffset = pos.getZ() - cameraPos.z();
|
||||
|
||||
var buffer = bufferSource.getBuffer(RenderType.lines());
|
||||
var matrix4f = transform.last().pose();
|
||||
var normal = transform.last().normal();
|
||||
// TODO: Can we just accesstransformer out LevelRenderer.renderShape?
|
||||
shape.forAllEdges((x1, y1, z1, x2, y2, z2) -> {
|
||||
var xDelta = (float) (x2 - x1);
|
||||
var yDelta = (float) (y2 - y1);
|
||||
var zDelta = (float) (z2 - z1);
|
||||
var len = Mth.sqrt(xDelta * xDelta + yDelta * yDelta + zDelta * zDelta);
|
||||
xDelta = xDelta / len;
|
||||
yDelta = yDelta / len;
|
||||
zDelta = zDelta / len;
|
||||
|
||||
buffer
|
||||
.vertex(matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset))
|
||||
.color(0, 0, 0, 0.4f)
|
||||
.normal(normal, xDelta, yDelta, zDelta)
|
||||
.endVertex();
|
||||
buffer
|
||||
.vertex(matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset))
|
||||
.color(0, 0, 0, 0.4f)
|
||||
.normal(normal, xDelta, yDelta, zDelta)
|
||||
.endVertex();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public class ComputerBorderRenderer {
|
||||
public static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_normal.png");
|
||||
public static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_advanced.png");
|
||||
public static final ResourceLocation BACKGROUND_COMMAND = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_command.png");
|
||||
public static final ResourceLocation BACKGROUND_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_colour.png");
|
||||
|
||||
/**
|
||||
* The margin between the terminal and its border.
|
||||
*/
|
||||
public static final int MARGIN = 2;
|
||||
|
||||
/**
|
||||
* The width of the terminal border.
|
||||
*/
|
||||
public static final int BORDER = 12;
|
||||
|
||||
private static final int CORNER_TOP_Y = 28;
|
||||
private static final int CORNER_BOTTOM_Y = CORNER_TOP_Y + BORDER;
|
||||
private static final int CORNER_LEFT_X = BORDER;
|
||||
private static final int CORNER_RIGHT_X = CORNER_LEFT_X + BORDER;
|
||||
private static final int BORDER_RIGHT_X = 36;
|
||||
private static final int LIGHT_BORDER_Y = 56;
|
||||
private static final int LIGHT_CORNER_Y = 80;
|
||||
|
||||
public static final int LIGHT_HEIGHT = 8;
|
||||
|
||||
public static final int TEX_SIZE = 256;
|
||||
private static final float TEX_SCALE = 1 / (float) TEX_SIZE;
|
||||
|
||||
private final Matrix4f transform;
|
||||
private final VertexConsumer builder;
|
||||
private final int light;
|
||||
private final int z;
|
||||
private final float r, g, b;
|
||||
|
||||
public ComputerBorderRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, float r, float g, float b) {
|
||||
this.transform = transform;
|
||||
this.builder = builder;
|
||||
this.z = z;
|
||||
this.light = light;
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
public static ResourceLocation getTexture(ComputerFamily family) {
|
||||
return switch (family) {
|
||||
case NORMAL -> BACKGROUND_NORMAL;
|
||||
case ADVANCED -> BACKGROUND_ADVANCED;
|
||||
case COMMAND -> BACKGROUND_COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
public static RenderType getRenderType(ResourceLocation location) {
|
||||
// See note in RenderTypes about why we use text rather than anything intuitive.
|
||||
return RenderType.text(location);
|
||||
}
|
||||
|
||||
public static void render(Matrix4f transform, ResourceLocation location, int x, int y, int z, int light, int width, int height) {
|
||||
var source = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
|
||||
render(transform, source.getBuffer(getRenderType(location)), x, y, z, light, width, height, false, 1, 1, 1);
|
||||
source.endBatch();
|
||||
}
|
||||
|
||||
public static void render(Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int light, int width, int height, boolean withLight, float r, float g, float b) {
|
||||
new ComputerBorderRenderer(transform, buffer, z, light, r, g, b).doRender(x, y, width, height, withLight);
|
||||
}
|
||||
|
||||
public void doRender(int x, int y, int width, int height, boolean withLight) {
|
||||
var endX = x + width;
|
||||
var endY = y + height;
|
||||
|
||||
// Vertical bars
|
||||
renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y);
|
||||
renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y);
|
||||
|
||||
// Top bar
|
||||
renderLine(x, y - BORDER, 0, 0, endX - x, BORDER);
|
||||
renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y);
|
||||
renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y);
|
||||
|
||||
// Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the
|
||||
// pocket computer's lights).
|
||||
if (withLight) {
|
||||
renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT);
|
||||
renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT);
|
||||
renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT);
|
||||
} else {
|
||||
renderLine(x, endY, 0, BORDER, endX - x, BORDER);
|
||||
renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y);
|
||||
renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCorner(int x, int y, int u, int v) {
|
||||
renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER);
|
||||
}
|
||||
|
||||
private void renderLine(int x, int y, int u, int v, int width, int height) {
|
||||
renderTexture(x, y, u, v, width, height, BORDER, BORDER);
|
||||
}
|
||||
|
||||
private void renderTexture(int x, int y, int u, int v, int width, int height) {
|
||||
renderTexture(x, y, u, v, width, height, width, height);
|
||||
}
|
||||
|
||||
private void renderTexture(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) {
|
||||
builder.vertex(transform, x, y + height, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex();
|
||||
builder.vertex(transform, x + width, y + height, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex();
|
||||
builder.vertex(transform, x + width, y, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex();
|
||||
builder.vertex(transform, x, y, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Vector3f;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.ItemInHandRenderer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public abstract class ItemMapLikeRenderer {
|
||||
/**
|
||||
* The main rendering method for the item.
|
||||
*
|
||||
* @param transform The matrix transformation stack
|
||||
* @param render The buffer to render to
|
||||
* @param stack The stack to render
|
||||
* @param light The packed lightmap coordinates.
|
||||
* @see ItemInHandRenderer#renderItem(LivingEntity, ItemStack, ItemTransforms.TransformType, boolean, PoseStack, MultiBufferSource, int)
|
||||
*/
|
||||
protected abstract void renderItem(PoseStack transform, MultiBufferSource render, ItemStack stack, int light);
|
||||
|
||||
public void renderItemFirstPerson(PoseStack transform, MultiBufferSource render, int lightTexture, InteractionHand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack) {
|
||||
Player player = Minecraft.getInstance().player;
|
||||
|
||||
transform.pushPose();
|
||||
if (hand == InteractionHand.MAIN_HAND && player.getOffhandItem().isEmpty()) {
|
||||
renderItemFirstPersonCenter(transform, render, lightTexture, pitch, equipProgress, swingProgress, stack);
|
||||
} else {
|
||||
renderItemFirstPersonSide(
|
||||
transform, render, lightTexture,
|
||||
hand == InteractionHand.MAIN_HAND ? player.getMainArm() : player.getMainArm().getOpposite(),
|
||||
equipProgress, swingProgress, stack
|
||||
);
|
||||
}
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the item to one side of the player.
|
||||
*
|
||||
* @param transform The matrix transformation stack
|
||||
* @param render The buffer to render to
|
||||
* @param combinedLight The current light level
|
||||
* @param side The side to render on
|
||||
* @param equipProgress The equip progress of this item
|
||||
* @param swingProgress The swing progress of this item
|
||||
* @param stack The stack to render
|
||||
* @see ItemInHandRenderer#renderOneHandedMap(PoseStack, MultiBufferSource, int, float, HumanoidArm, float, ItemStack)
|
||||
*/
|
||||
private void renderItemFirstPersonSide(PoseStack transform, MultiBufferSource render, int combinedLight, HumanoidArm side, float equipProgress, float swingProgress, ItemStack stack) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
var offset = side == HumanoidArm.RIGHT ? 1f : -1f;
|
||||
transform.translate(offset * 0.125f, -0.125f, 0f);
|
||||
|
||||
// If the player is not invisible then render a single arm
|
||||
if (!minecraft.player.isInvisible()) {
|
||||
transform.pushPose();
|
||||
transform.mulPose(Vector3f.ZP.rotationDegrees(offset * 10f));
|
||||
minecraft.getEntityRenderDispatcher().getItemInHandRenderer().renderPlayerArm(transform, render, combinedLight, equipProgress, swingProgress, side);
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
// Setup the appropriate transformations. This is just copied from the
|
||||
// corresponding method in ItemRenderer.
|
||||
transform.pushPose();
|
||||
transform.translate(offset * 0.51f, -0.08f + equipProgress * -1.2f, -0.75f);
|
||||
var f1 = Mth.sqrt(swingProgress);
|
||||
var f2 = Mth.sin(f1 * (float) Math.PI);
|
||||
var f3 = -0.5f * f2;
|
||||
var f4 = 0.4f * Mth.sin(f1 * ((float) Math.PI * 2f));
|
||||
var f5 = -0.3f * Mth.sin(swingProgress * (float) Math.PI);
|
||||
transform.translate(offset * f3, f4 - 0.3f * f2, f5);
|
||||
transform.mulPose(Vector3f.XP.rotationDegrees(f2 * -45f));
|
||||
transform.mulPose(Vector3f.YP.rotationDegrees(offset * f2 * -30f));
|
||||
|
||||
renderItem(transform, render, stack, combinedLight);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an item in the middle of the screen.
|
||||
*
|
||||
* @param transform The matrix transformation stack
|
||||
* @param render The buffer to render to
|
||||
* @param combinedLight The current light level
|
||||
* @param pitch The pitch of the player
|
||||
* @param equipProgress The equip progress of this item
|
||||
* @param swingProgress The swing progress of this item
|
||||
* @param stack The stack to render
|
||||
* @see ItemInHandRenderer#renderTwoHandedMap(PoseStack, MultiBufferSource, int, float, float, float)
|
||||
*/
|
||||
private void renderItemFirstPersonCenter(PoseStack transform, MultiBufferSource render, int combinedLight, float pitch, float equipProgress, float swingProgress, ItemStack stack) {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
var renderer = minecraft.getEntityRenderDispatcher().getItemInHandRenderer();
|
||||
|
||||
// Setup the appropriate transformations. This is just copied from the
|
||||
// corresponding method in ItemRenderer.
|
||||
var swingRt = Mth.sqrt(swingProgress);
|
||||
var tX = -0.2f * Mth.sin(swingProgress * (float) Math.PI);
|
||||
var tZ = -0.4f * Mth.sin(swingRt * (float) Math.PI);
|
||||
transform.translate(0, -tX / 2, tZ);
|
||||
|
||||
var pitchAngle = renderer.calculateMapTilt(pitch);
|
||||
transform.translate(0, 0.04F + equipProgress * -1.2f + pitchAngle * -0.5f, -0.72f);
|
||||
transform.mulPose(Vector3f.XP.rotationDegrees(pitchAngle * -85.0f));
|
||||
if (!minecraft.player.isInvisible()) {
|
||||
transform.pushPose();
|
||||
transform.mulPose(Vector3f.YP.rotationDegrees(90.0F));
|
||||
renderer.renderMapHand(transform, render, combinedLight, HumanoidArm.RIGHT);
|
||||
renderer.renderMapHand(transform, render, combinedLight, HumanoidArm.LEFT);
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
var rX = Mth.sin(swingRt * (float) Math.PI);
|
||||
transform.mulPose(Vector3f.XP.rotationDegrees(rX * 20.0F));
|
||||
transform.scale(2.0F, 2.0F, 2.0F);
|
||||
|
||||
renderItem(transform, render, stack, combinedLight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import com.mojang.math.Vector3f;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.*;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
|
||||
/**
|
||||
* Emulates map rendering for pocket computers.
|
||||
*/
|
||||
public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
public static final PocketItemRenderer INSTANCE = new PocketItemRenderer();
|
||||
|
||||
private PocketItemRenderer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
var terminal = computer.getTerminal();
|
||||
|
||||
var termWidth = terminal.getWidth();
|
||||
var termHeight = terminal.getHeight();
|
||||
|
||||
var width = termWidth * FONT_WIDTH + MARGIN * 2;
|
||||
var height = termHeight * FONT_HEIGHT + MARGIN * 2;
|
||||
|
||||
// Setup various transformations. Note that these are partially adapted from the corresponding method
|
||||
// in ItemRenderer
|
||||
transform.pushPose();
|
||||
transform.mulPose(Vector3f.YP.rotationDegrees(180f));
|
||||
transform.mulPose(Vector3f.ZP.rotationDegrees(180f));
|
||||
transform.scale(0.5f, 0.5f, 0.5f);
|
||||
|
||||
var scale = 0.75f / Math.max(width + BORDER * 2, height + BORDER * 2 + LIGHT_HEIGHT);
|
||||
transform.scale(scale, scale, -1.0f);
|
||||
transform.translate(-0.5 * width, -0.5 * height, 0);
|
||||
|
||||
// Render the main frame
|
||||
var item = (PocketComputerItem) stack.getItem();
|
||||
var family = item.getFamily();
|
||||
var frameColour = item.getColour(stack);
|
||||
|
||||
var matrix = transform.last().pose();
|
||||
renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
|
||||
|
||||
// Render the light
|
||||
var lightColour = ClientPocketComputers.get(stack).getLightState();
|
||||
if (lightColour == -1) lightColour = Colour.BLACK.getHex();
|
||||
renderLight(transform, bufferSource, lightColour, width, height);
|
||||
|
||||
FixedWidthFontRenderer.drawTerminal(
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
|
||||
MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN
|
||||
);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
|
||||
var texture = colour != -1 ? ComputerBorderRenderer.BACKGROUND_COLOUR : ComputerBorderRenderer.getTexture(family);
|
||||
|
||||
var r = ((colour >>> 16) & 0xFF) / 255.0f;
|
||||
var g = ((colour >>> 8) & 0xFF) / 255.0f;
|
||||
var b = (colour & 0xFF) / 255.0f;
|
||||
|
||||
ComputerBorderRenderer.render(transform, render.getBuffer(ComputerBorderRenderer.getRenderType(texture)), 0, 0, 0, light, width, height, true, r, g, b);
|
||||
}
|
||||
|
||||
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
|
||||
var r = (byte) ((colour >>> 16) & 0xFF);
|
||||
var g = (byte) ((colour >>> 8) & 0xFF);
|
||||
var b = (byte) (colour & 0xFF);
|
||||
var c = new byte[]{ r, g, b, (byte) 255 };
|
||||
|
||||
var buffer = render.getBuffer(RenderTypes.TERMINAL);
|
||||
FixedWidthFontRenderer.drawQuad(
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, buffer),
|
||||
width - LIGHT_HEIGHT * 2, height + BORDER / 2.0f, 0.001f, LIGHT_HEIGHT * 2, LIGHT_HEIGHT,
|
||||
c, RenderTypes.FULL_BRIGHT_LIGHTMAP
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Vector3f;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
import static dan200.computercraft.shared.media.items.PrintoutItem.LINES_PER_PAGE;
|
||||
import static dan200.computercraft.shared.media.items.PrintoutItem.LINE_MAX_LENGTH;
|
||||
|
||||
/**
|
||||
* Emulates map and item-frame rendering for printouts.
|
||||
*/
|
||||
public final class PrintoutItemRenderer extends ItemMapLikeRenderer {
|
||||
public static final PrintoutItemRenderer INSTANCE = new PrintoutItemRenderer();
|
||||
|
||||
private PrintoutItemRenderer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderItem(PoseStack transform, MultiBufferSource render, ItemStack stack, int light) {
|
||||
transform.mulPose(Vector3f.XP.rotationDegrees(180f));
|
||||
transform.scale(0.42f, 0.42f, -0.42f);
|
||||
transform.translate(-0.5f, -0.48f, 0.0f);
|
||||
|
||||
drawPrintout(transform, render, stack, light);
|
||||
}
|
||||
|
||||
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int packedLight) {
|
||||
if (!(stack.getItem() instanceof PrintoutItem)) return;
|
||||
|
||||
// Move a little bit forward to ensure we're not clipping with the frame
|
||||
transform.translate(0.0f, 0.0f, -0.001f);
|
||||
transform.mulPose(Vector3f.ZP.rotationDegrees(180f));
|
||||
transform.scale(0.95f, 0.95f, -0.95f);
|
||||
transform.translate(-0.5f, -0.5f, 0.0f);
|
||||
|
||||
var light = frame.getType() == EntityType.GLOW_ITEM_FRAME ? 0xf000d2 : packedLight; // See getLightVal.
|
||||
drawPrintout(transform, render, stack, light);
|
||||
}
|
||||
|
||||
private static void drawPrintout(PoseStack transform, MultiBufferSource render, ItemStack stack, int light) {
|
||||
var pages = PrintoutItem.getPageCount(stack);
|
||||
var book = ((PrintoutItem) stack.getItem()).getType() == PrintoutItem.Type.BOOK;
|
||||
|
||||
double width = LINE_MAX_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2;
|
||||
double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2;
|
||||
|
||||
// Non-books will be left aligned
|
||||
if (!book) width += offsetAt(pages - 1);
|
||||
|
||||
double visualWidth = width, visualHeight = height;
|
||||
|
||||
// Meanwhile books will be centred
|
||||
if (book) {
|
||||
visualWidth += 2 * COVER_SIZE + 2 * offsetAt(pages);
|
||||
visualHeight += 2 * COVER_SIZE;
|
||||
}
|
||||
|
||||
var max = Math.max(visualHeight, visualWidth);
|
||||
|
||||
// Scale the printout to fit correctly.
|
||||
var scale = (float) (1.0 / max);
|
||||
transform.scale(scale, scale, scale);
|
||||
transform.translate((max - width) / 2.0, (max - height) / 2.0, 0.0);
|
||||
|
||||
drawBorder(transform, render, 0, 0, -0.01f, 0, pages, book, light);
|
||||
drawText(
|
||||
transform, render, X_TEXT_MARGIN, Y_TEXT_MARGIN, 0, light,
|
||||
PrintoutItem.getText(stack), PrintoutItem.getColours(stack)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.shared.media.items.PrintoutItem.LINES_PER_PAGE;
|
||||
|
||||
public final class PrintoutRenderer {
|
||||
private static final float BG_SIZE = 256.0f;
|
||||
|
||||
/**
|
||||
* Width of a page.
|
||||
*/
|
||||
public static final int X_SIZE = 172;
|
||||
|
||||
/**
|
||||
* Height of a page.
|
||||
*/
|
||||
public static final int Y_SIZE = 209;
|
||||
|
||||
/**
|
||||
* Padding between the left and right of a page and the text.
|
||||
*/
|
||||
public static final int X_TEXT_MARGIN = 13;
|
||||
|
||||
/**
|
||||
* Padding between the top and bottom of a page and the text.
|
||||
*/
|
||||
public static final int Y_TEXT_MARGIN = 11;
|
||||
|
||||
/**
|
||||
* Width of the extra page texture.
|
||||
*/
|
||||
private static final int X_FOLD_SIZE = 12;
|
||||
|
||||
/**
|
||||
* Size of the leather cover.
|
||||
*/
|
||||
public static final int COVER_SIZE = 12;
|
||||
|
||||
private static final int COVER_Y = Y_SIZE;
|
||||
private static final int COVER_X = X_SIZE + 4 * X_FOLD_SIZE;
|
||||
|
||||
private PrintoutRenderer() {
|
||||
}
|
||||
|
||||
public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, TextBuffer[] text, TextBuffer[] colours) {
|
||||
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
|
||||
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
|
||||
for (var line = 0; line < LINES_PER_PAGE && line < text.length; line++) {
|
||||
FixedWidthFontRenderer.drawString(emitter,
|
||||
x, y + line * FONT_HEIGHT, text[start + line], colours[start + line],
|
||||
Palette.DEFAULT, light
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, String[] text, String[] colours) {
|
||||
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
|
||||
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
|
||||
for (var line = 0; line < LINES_PER_PAGE && line < text.length; line++) {
|
||||
FixedWidthFontRenderer.drawString(emitter,
|
||||
x, y + line * FONT_HEIGHT,
|
||||
new TextBuffer(text[start + line]), new TextBuffer(colours[start + line]),
|
||||
Palette.DEFAULT, light
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawBorder(PoseStack transform, MultiBufferSource bufferSource, float x, float y, float z, int page, int pages, boolean isBook, int light) {
|
||||
var matrix = transform.last().pose();
|
||||
var leftPages = page;
|
||||
var rightPages = pages - page - 1;
|
||||
|
||||
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_BACKGROUND);
|
||||
|
||||
if (isBook) {
|
||||
// Border
|
||||
var offset = offsetAt(pages);
|
||||
var left = x - 4 - offset;
|
||||
var right = x + X_SIZE + offset - 4;
|
||||
|
||||
// Left and right border
|
||||
drawTexture(matrix, buffer, left - 4, y - 8, z - 0.02f, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
|
||||
drawTexture(matrix, buffer, right, y - 8, z - 0.02f, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
|
||||
|
||||
// Draw centre panel (just stretched texture, sorry).
|
||||
drawTexture(matrix, buffer,
|
||||
x - offset, y, z - 0.02f, X_SIZE + offset * 2, Y_SIZE,
|
||||
COVER_X + COVER_SIZE / 2.0f, COVER_SIZE, COVER_SIZE, Y_SIZE,
|
||||
light
|
||||
);
|
||||
|
||||
var borderX = left;
|
||||
while (borderX < right) {
|
||||
double thisWidth = Math.min(right - borderX, X_SIZE);
|
||||
drawTexture(matrix, buffer, borderX, y - 8, z - 0.02f, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
|
||||
drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z - 0.02f, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
|
||||
borderX += thisWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Current page background: Z-offset is interleaved between the "zeroth" left/right page and the first
|
||||
// left/right page, so that the "bold" border can be drawn over the edge where appropriate.
|
||||
drawTexture(matrix, buffer, x, y, z - 1e-3f * 0.5f, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
|
||||
|
||||
// Left pages
|
||||
for (var n = 0; n <= leftPages; n++) {
|
||||
drawTexture(matrix, buffer,
|
||||
x - offsetAt(n), y, z - 1e-3f * n,
|
||||
// Use the left "bold" fold for the outermost page
|
||||
n == leftPages ? 0 : X_FOLD_SIZE, 0,
|
||||
X_FOLD_SIZE, Y_SIZE, light
|
||||
);
|
||||
}
|
||||
|
||||
// Right pages
|
||||
for (var n = 0; n <= rightPages; n++) {
|
||||
drawTexture(matrix, buffer,
|
||||
x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z - 1e-3f * n,
|
||||
// Two folds, then the main page. Use the right "bold" fold for the outermost page.
|
||||
X_FOLD_SIZE * 2 + X_SIZE + (n == rightPages ? X_FOLD_SIZE : 0), 0,
|
||||
X_FOLD_SIZE, Y_SIZE, light
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawTexture(Matrix4f matrix, VertexConsumer buffer, float x, float y, float z, float u, float v, float width, float height, int light) {
|
||||
vertex(buffer, matrix, x, y + height, z, u / BG_SIZE, (v + height) / BG_SIZE, light);
|
||||
vertex(buffer, matrix, x + width, y + height, z, (u + width) / BG_SIZE, (v + height) / BG_SIZE, light);
|
||||
vertex(buffer, matrix, x + width, y, z, (u + width) / BG_SIZE, v / BG_SIZE, light);
|
||||
vertex(buffer, matrix, x, y, z, u / BG_SIZE, v / BG_SIZE, light);
|
||||
}
|
||||
|
||||
private static void drawTexture(Matrix4f matrix, VertexConsumer buffer, float x, float y, float z, float width, float height, float u, float v, float tWidth, float tHeight, int light) {
|
||||
vertex(buffer, matrix, x, y + height, z, u / BG_SIZE, (v + tHeight) / BG_SIZE, light);
|
||||
vertex(buffer, matrix, x + width, y + height, z, (u + tWidth) / BG_SIZE, (v + tHeight) / BG_SIZE, light);
|
||||
vertex(buffer, matrix, x + width, y, z, (u + tWidth) / BG_SIZE, v / BG_SIZE, light);
|
||||
vertex(buffer, matrix, x, y, z, u / BG_SIZE, v / BG_SIZE, light);
|
||||
}
|
||||
|
||||
private static void vertex(VertexConsumer buffer, Matrix4f matrix, float x, float y, float z, float u, float v, int light) {
|
||||
buffer.vertex(matrix, x, y, z).color(255, 255, 255, 255).uv(u, v).uv2(light).endVertex();
|
||||
}
|
||||
|
||||
public static float offsetAt(int page) {
|
||||
return (float) (32 * (1 - Math.pow(1.2, -page)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.RenderStateShard;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class RenderTypes {
|
||||
public static final int FULL_BRIGHT_LIGHTMAP = (0xF << 4) | (0xF << 20);
|
||||
|
||||
private static @Nullable MonitorTextureBufferShader monitorTboShader;
|
||||
|
||||
/**
|
||||
* Renders a fullbright terminal.
|
||||
*/
|
||||
public static final RenderType TERMINAL = RenderType.text(FixedWidthFontRenderer.FONT);
|
||||
|
||||
/**
|
||||
* Renders a monitor with the TBO shader.
|
||||
*
|
||||
* @see MonitorTextureBufferShader
|
||||
*/
|
||||
public static final RenderType MONITOR_TBO = Types.MONITOR_TBO;
|
||||
|
||||
/**
|
||||
* A variant of {@link #TERMINAL} which uses the lightmap rather than rendering fullbright.
|
||||
*/
|
||||
public static final RenderType PRINTOUT_TEXT = RenderType.text(FixedWidthFontRenderer.FONT);
|
||||
|
||||
/**
|
||||
* Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
|
||||
* it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
|
||||
*/
|
||||
public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(new ResourceLocation("computercraft", "textures/gui/printout.png"));
|
||||
|
||||
public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
|
||||
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
|
||||
return monitorTboShader;
|
||||
}
|
||||
|
||||
public static ShaderInstance getTerminalShader() {
|
||||
return Objects.requireNonNull(GameRenderer.getRendertypeTextShader(), "Text shader has not been registered");
|
||||
}
|
||||
|
||||
public static void registerShaders(ResourceManager resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
|
||||
load.accept(
|
||||
new MonitorTextureBufferShader(
|
||||
resources,
|
||||
ComputerCraftAPI.MOD_ID + "/monitor_tbo",
|
||||
MONITOR_TBO.format()
|
||||
),
|
||||
x -> monitorTboShader = (MonitorTextureBufferShader) x
|
||||
);
|
||||
}
|
||||
|
||||
private static final class Types extends RenderType {
|
||||
private static final RenderStateShard.TextureStateShard TERM_FONT_TEXTURE = new TextureStateShard(
|
||||
FixedWidthFontRenderer.FONT,
|
||||
false, false // blur, minimap
|
||||
);
|
||||
|
||||
static final RenderType MONITOR_TBO = RenderType.create(
|
||||
"monitor_tbo", DefaultVertexFormat.POSITION_TEX, VertexFormat.Mode.TRIANGLE_STRIP, 128,
|
||||
false, false, // useDelegate, needsSorting
|
||||
RenderType.CompositeState.builder()
|
||||
.setTextureState(TERM_FONT_TEXTURE)
|
||||
.setShaderState(new ShaderStateShard(RenderTypes::getMonitorTextureBufferShader))
|
||||
.createCompositeState(false)
|
||||
);
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private Types(String name, VertexFormat format, VertexFormat.Mode mode, int buffer, boolean crumbling, boolean sort, Runnable setup, Runnable teardown) {
|
||||
super(name, format, mode, buffer, crumbling, sort, setup, teardown);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Transformation;
|
||||
import com.mojang.math.Vector3f;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import dan200.computercraft.shared.util.HolidayUtil;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.Sheets;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
|
||||
private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation("computercraft:turtle_normal", "inventory");
|
||||
private static final ModelResourceLocation ADVANCED_TURTLE_MODEL = new ModelResourceLocation("computercraft:turtle_advanced", "inventory");
|
||||
private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
|
||||
private static final ResourceLocation ELF_OVERLAY_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
|
||||
|
||||
private final RandomSource random = RandomSource.create(0);
|
||||
|
||||
private final BlockEntityRenderDispatcher renderer;
|
||||
private final Font font;
|
||||
|
||||
public TurtleBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
renderer = context.getBlockEntityRenderDispatcher();
|
||||
font = context.getFont();
|
||||
}
|
||||
|
||||
public static ResourceLocation getTurtleModel(ComputerFamily family, boolean coloured) {
|
||||
return switch (family) {
|
||||
default -> coloured ? COLOUR_TURTLE_MODEL : NORMAL_TURTLE_MODEL;
|
||||
case ADVANCED -> coloured ? COLOUR_TURTLE_MODEL : ADVANCED_TURTLE_MODEL;
|
||||
};
|
||||
}
|
||||
|
||||
public static @Nullable ResourceLocation getTurtleOverlayModel(@Nullable ResourceLocation overlay, boolean christmas) {
|
||||
if (overlay != null) return overlay;
|
||||
if (christmas) return ELF_OVERLAY_MODEL;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight) {
|
||||
// Render the label
|
||||
var label = turtle.getLabel();
|
||||
var hit = renderer.cameraHitResult;
|
||||
if (label != null && hit.getType() == HitResult.Type.BLOCK && turtle.getBlockPos().equals(((BlockHitResult) hit).getBlockPos())) {
|
||||
var mc = Minecraft.getInstance();
|
||||
var font = this.font;
|
||||
|
||||
transform.pushPose();
|
||||
transform.translate(0.5, 1.2, 0.5);
|
||||
transform.mulPose(mc.getEntityRenderDispatcher().cameraOrientation());
|
||||
transform.scale(-0.025f, -0.025f, 0.025f);
|
||||
|
||||
var matrix = transform.last().pose();
|
||||
var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
|
||||
var width = -font.width(label) / 2.0f;
|
||||
font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, true, opacity, lightmapCoord);
|
||||
font.drawInBatch(label, width, (float) 0, 0xffffffff, false, matrix, buffers, false, 0, lightmapCoord);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
transform.pushPose();
|
||||
|
||||
// Setup the transform.
|
||||
var offset = turtle.getRenderOffset(partialTicks);
|
||||
var yaw = turtle.getRenderYaw(partialTicks);
|
||||
transform.translate(offset.x, offset.y, offset.z);
|
||||
|
||||
transform.translate(0.5f, 0.5f, 0.5f);
|
||||
transform.mulPose(Vector3f.YP.rotationDegrees(180.0f - yaw));
|
||||
if (label != null && (label.equals("Dinnerbone") || label.equals("Grumm"))) {
|
||||
// Flip the model
|
||||
transform.scale(1.0f, -1.0f, 1.0f);
|
||||
}
|
||||
transform.translate(-0.5f, -0.5f, -0.5f);
|
||||
|
||||
// Render the turtle
|
||||
var colour = turtle.getColour();
|
||||
var family = turtle.getFamily();
|
||||
var overlay = turtle.getOverlay();
|
||||
|
||||
var buffer = buffers.getBuffer(Sheets.translucentCullBlockSheet());
|
||||
renderModel(transform, buffer, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
|
||||
|
||||
// Render the overlay
|
||||
var overlayModel = getTurtleOverlayModel(overlay, HolidayUtil.getCurrentHoliday() == Holiday.CHRISTMAS);
|
||||
if (overlayModel != null) {
|
||||
renderModel(transform, buffer, lightmapCoord, overlayLight, overlayModel, null);
|
||||
}
|
||||
|
||||
// Render the upgrades
|
||||
renderUpgrade(transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks);
|
||||
renderUpgrade(transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.RIGHT, partialTicks);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private void renderUpgrade(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
|
||||
var upgrade = turtle.getUpgrade(side);
|
||||
if (upgrade == null) return;
|
||||
transform.pushPose();
|
||||
|
||||
var toolAngle = turtle.getToolRenderAngle(side, f);
|
||||
transform.translate(0.0f, 0.5f, 0.5f);
|
||||
transform.mulPose(Vector3f.XN.rotationDegrees(toolAngle));
|
||||
transform.translate(0.0f, -0.5f, -0.5f);
|
||||
|
||||
var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
|
||||
pushPoseFromTransformation(transform, model.getMatrix());
|
||||
renderModel(transform, renderer, lightmapCoord, overlayLight, model.getModel(), null);
|
||||
transform.popPose();
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private void renderModel(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, @Nullable int[] tints) {
|
||||
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getModelManager();
|
||||
renderModel(transform, renderer, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
|
||||
}
|
||||
|
||||
private void renderModel(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) {
|
||||
random.setSeed(0);
|
||||
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, null, random), tints);
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, facing, random), tints);
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderQuads(PoseStack transform, VertexConsumer buffer, int lightmapCoord, int overlayLight, List<BakedQuad> quads, @Nullable int[] tints) {
|
||||
var matrix = transform.last();
|
||||
|
||||
for (var bakedquad : quads) {
|
||||
var tint = -1;
|
||||
if (tints != null && bakedquad.isTinted()) {
|
||||
var idx = bakedquad.getTintIndex();
|
||||
if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
|
||||
}
|
||||
|
||||
var r = (float) (tint >> 16 & 255) / 255.0F;
|
||||
var g = (float) (tint >> 8 & 255) / 255.0F;
|
||||
var b = (float) (tint & 255) / 255.0F;
|
||||
buffer.putBulkData(matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pushPoseFromTransformation(PoseStack stack, Transformation transformation) {
|
||||
stack.pushPose();
|
||||
|
||||
var trans = transformation.getTranslation();
|
||||
stack.translate(trans.x(), trans.y(), trans.z());
|
||||
|
||||
stack.mulPose(transformation.getLeftRotation());
|
||||
|
||||
var scale = transformation.getScale();
|
||||
stack.scale(scale.x(), scale.y(), scale.z());
|
||||
|
||||
stack.mulPose(transformation.getRightRotation());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.platform.MemoryTracker;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import com.mojang.blaze3d.vertex.VertexBuffer;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Matrix3f;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import com.mojang.math.Vector3f;
|
||||
import dan200.computercraft.client.FrameInfo;
|
||||
import dan200.computercraft.client.integration.ShaderMod;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.vbo.DirectBuffers;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBlockEntity> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MonitorBlockEntityRenderer.class);
|
||||
|
||||
/**
|
||||
* {@link MonitorBlockEntity#RENDER_MARGIN}, but a tiny bit of additional padding to ensure that there is no space between
|
||||
* the monitor frame and contents.
|
||||
*/
|
||||
private static final float MARGIN = (float) (MonitorBlockEntity.RENDER_MARGIN * 1.1);
|
||||
|
||||
private static final Matrix3f IDENTITY_NORMAL = Util.make(new Matrix3f(), Matrix3f::setIdentity);
|
||||
|
||||
private static @Nullable ByteBuffer backingBuffer;
|
||||
|
||||
public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
|
||||
// Render from the origin monitor
|
||||
var originTerminal = monitor.getClientMonitor();
|
||||
|
||||
if (originTerminal == null) return;
|
||||
var origin = originTerminal.getOrigin();
|
||||
var renderState = originTerminal.getRenderState(MonitorRenderState::new);
|
||||
var monitorPos = monitor.getBlockPos();
|
||||
|
||||
// Ensure each monitor terminal is rendered only once. We allow rendering a specific tile
|
||||
// multiple times in a single frame to ensure compatibility with shaders which may run a
|
||||
// pass multiple times.
|
||||
var renderFrame = FrameInfo.getRenderFrame();
|
||||
if (renderState.lastRenderFrame == renderFrame && !monitorPos.equals(renderState.lastRenderPos)) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderState.lastRenderFrame = renderFrame;
|
||||
renderState.lastRenderPos = monitorPos;
|
||||
|
||||
var originPos = origin.getBlockPos();
|
||||
|
||||
// Determine orientation
|
||||
var dir = origin.getDirection();
|
||||
var front = origin.getFront();
|
||||
var yaw = dir.toYRot();
|
||||
var pitch = DirectionUtil.toPitchAngle(front);
|
||||
|
||||
// Setup initial transform
|
||||
transform.pushPose();
|
||||
transform.translate(
|
||||
originPos.getX() - monitorPos.getX() + 0.5,
|
||||
originPos.getY() - monitorPos.getY() + 0.5,
|
||||
originPos.getZ() - monitorPos.getZ() + 0.5
|
||||
);
|
||||
|
||||
transform.mulPose(Vector3f.YN.rotationDegrees(yaw));
|
||||
transform.mulPose(Vector3f.XP.rotationDegrees(pitch));
|
||||
transform.translate(
|
||||
-0.5 + MonitorBlockEntity.RENDER_BORDER + MonitorBlockEntity.RENDER_MARGIN,
|
||||
origin.getHeight() - 0.5 - (MonitorBlockEntity.RENDER_BORDER + MonitorBlockEntity.RENDER_MARGIN) + 0,
|
||||
0.5
|
||||
);
|
||||
var xSize = origin.getWidth() - 2.0 * (MonitorBlockEntity.RENDER_MARGIN + MonitorBlockEntity.RENDER_BORDER);
|
||||
var ySize = origin.getHeight() - 2.0 * (MonitorBlockEntity.RENDER_MARGIN + MonitorBlockEntity.RENDER_BORDER);
|
||||
|
||||
// Draw the contents
|
||||
var terminal = originTerminal.getTerminal();
|
||||
if (terminal != null && !ShaderMod.get().isRenderingShadowPass()) {
|
||||
// Draw a terminal
|
||||
int width = terminal.getWidth(), height = terminal.getHeight();
|
||||
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
|
||||
var xScale = xSize / pixelWidth;
|
||||
var yScale = ySize / pixelHeight;
|
||||
transform.pushPose();
|
||||
transform.scale((float) xScale, (float) -yScale, 1.0f);
|
||||
|
||||
var matrix = transform.last().pose();
|
||||
|
||||
renderTerminal(matrix, originTerminal, renderState, terminal, (float) (MARGIN / xScale), (float) (MARGIN / yScale));
|
||||
|
||||
transform.popPose();
|
||||
} else {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
|
||||
-MARGIN, MARGIN,
|
||||
(float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2)
|
||||
);
|
||||
}
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private static void renderTerminal(
|
||||
Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin
|
||||
) {
|
||||
int width = terminal.getWidth(), height = terminal.getHeight();
|
||||
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
|
||||
|
||||
var renderType = currentRenderer();
|
||||
var redraw = monitor.pollTerminalChanged();
|
||||
if (renderState.createBuffer(renderType)) redraw = true;
|
||||
|
||||
switch (renderType) {
|
||||
case TBO -> {
|
||||
if (redraw) {
|
||||
var terminalBuffer = getBuffer(width * height * 3);
|
||||
MonitorTextureBufferShader.setTerminalData(terminalBuffer, terminal);
|
||||
DirectBuffers.setBufferData(GL31.GL_TEXTURE_BUFFER, renderState.tboBuffer, terminalBuffer, GL20.GL_STATIC_DRAW);
|
||||
|
||||
var uniformBuffer = getBuffer(MonitorTextureBufferShader.UNIFORM_SIZE);
|
||||
MonitorTextureBufferShader.setUniformData(uniformBuffer, terminal);
|
||||
DirectBuffers.setBufferData(GL31.GL_UNIFORM_BUFFER, renderState.tboUniform, uniformBuffer, GL20.GL_STATIC_DRAW);
|
||||
}
|
||||
|
||||
// Nobody knows what they're doing!
|
||||
var active = GlStateManager._getActiveTexture();
|
||||
RenderSystem.activeTexture(MonitorTextureBufferShader.TEXTURE_INDEX);
|
||||
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, renderState.tboTexture);
|
||||
RenderSystem.activeTexture(active);
|
||||
|
||||
var shader = RenderTypes.getMonitorTextureBufferShader();
|
||||
shader.setupUniform(renderState.tboUniform);
|
||||
|
||||
var buffer = Tesselator.getInstance().getBuilder();
|
||||
buffer.begin(RenderTypes.MONITOR_TBO.mode(), RenderTypes.MONITOR_TBO.format());
|
||||
tboVertex(buffer, matrix, -xMargin, -yMargin);
|
||||
tboVertex(buffer, matrix, -xMargin, pixelHeight + yMargin);
|
||||
tboVertex(buffer, matrix, pixelWidth + xMargin, -yMargin);
|
||||
tboVertex(buffer, matrix, pixelWidth + xMargin, pixelHeight + yMargin);
|
||||
RenderTypes.MONITOR_TBO.end(buffer, 0, 0, 0);
|
||||
}
|
||||
case VBO -> {
|
||||
var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
|
||||
var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
|
||||
if (redraw) {
|
||||
var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
|
||||
|
||||
// In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
|
||||
// and starting and ending offset, and so need to use two buffers instead.
|
||||
|
||||
renderToBuffer(backgroundBuffer, size, sink ->
|
||||
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
|
||||
|
||||
renderToBuffer(foregroundBuffer, size, sink -> {
|
||||
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
|
||||
// If the cursor is visible, we append it to the end of our buffer. When rendering, we can either
|
||||
// render n or n+1 quads and so toggle the cursor on and off.
|
||||
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
|
||||
});
|
||||
}
|
||||
|
||||
// Our VBO doesn't transform its vertices with the provided pose stack, which means that the inverse view
|
||||
// rotation matrix gives entirely wrong numbers for fog distances. We just set it to the identity which
|
||||
// gives a good enough approximation.
|
||||
var oldInverseRotation = RenderSystem.getInverseViewRotationMatrix();
|
||||
RenderSystem.setInverseViewRotationMatrix(IDENTITY_NORMAL);
|
||||
|
||||
RenderTypes.TERMINAL.setupRenderState();
|
||||
|
||||
// Render background geometry
|
||||
backgroundBuffer.bind();
|
||||
backgroundBuffer.drawWithShader(matrix, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader());
|
||||
|
||||
// Render foreground geometry with glPolygonOffset enabled.
|
||||
GL11.glPolygonOffset(-1.0f, -10.0f);
|
||||
GL11.glEnable(GL11.GL_POLYGON_OFFSET_FILL);
|
||||
|
||||
foregroundBuffer.bind();
|
||||
foregroundBuffer.drawWithShader(
|
||||
matrix, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader(),
|
||||
// As mentioned in the above comment, render the extra cursor quad if it is visible this frame. Each
|
||||
// // quad has an index count of 6.
|
||||
FixedWidthFontRenderer.isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()
|
||||
? foregroundBuffer.getIndexCount() + 6 : foregroundBuffer.getIndexCount()
|
||||
);
|
||||
|
||||
// Clear state
|
||||
GL11.glPolygonOffset(0.0f, -0.0f);
|
||||
GL11.glDisable(GL11.GL_POLYGON_OFFSET_FILL);
|
||||
RenderTypes.TERMINAL.clearRenderState();
|
||||
VertexBuffer.unbind();
|
||||
|
||||
RenderSystem.setInverseViewRotationMatrix(oldInverseRotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderToBuffer(DirectVertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
|
||||
var sink = ShaderMod.get().getQuadEmitter(size, MonitorBlockEntityRenderer::getBuffer);
|
||||
var buffer = sink.buffer();
|
||||
|
||||
draw.accept(sink);
|
||||
buffer.flip();
|
||||
vbo.upload(buffer.limit() / sink.format().getVertexSize(), RenderTypes.TERMINAL.mode(), sink.format(), buffer);
|
||||
}
|
||||
|
||||
private static void tboVertex(VertexConsumer builder, Matrix4f matrix, float x, float y) {
|
||||
// We encode position in the UV, as that's not transformed by the matrix.
|
||||
builder.vertex(matrix, x, y, 0).uv(x, y).endVertex();
|
||||
}
|
||||
|
||||
private static ByteBuffer getBuffer(int capacity) {
|
||||
var buffer = backingBuffer;
|
||||
if (buffer == null || buffer.capacity() < capacity) {
|
||||
buffer = backingBuffer = buffer == null ? MemoryTracker.create(capacity) : MemoryTracker.resize(buffer, capacity);
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewDistance() {
|
||||
return Config.monitorDistance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current renderer to use.
|
||||
*
|
||||
* @return The current renderer. Will not return {@link MonitorRenderer#BEST}.
|
||||
*/
|
||||
public static MonitorRenderer currentRenderer() {
|
||||
var current = Config.monitorRenderer;
|
||||
if (current == MonitorRenderer.BEST) current = Config.monitorRenderer = bestRenderer();
|
||||
return current;
|
||||
}
|
||||
|
||||
private static MonitorRenderer bestRenderer() {
|
||||
if (!GL.getCapabilities().OpenGL31) {
|
||||
LOG.warn("Texture buffers are not supported on your graphics card. Falling back to VBO monitor renderer.");
|
||||
return MonitorRenderer.VBO;
|
||||
}
|
||||
|
||||
if (ShaderMod.get().isShaderMod()) {
|
||||
LOG.warn("A shader mod is loaded, assuming shaders are being used. Falling back to VBO monitor renderer.");
|
||||
return MonitorRenderer.VBO;
|
||||
}
|
||||
|
||||
return MonitorRenderer.TBO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Matrix3f;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static net.minecraft.core.Direction.*;
|
||||
|
||||
/**
|
||||
* Overrides monitor highlighting to only render the outline of the <em>whole</em> monitor, rather than the current
|
||||
* block. This means you do not get an intrusive outline on top of the screen.
|
||||
*/
|
||||
public final class MonitorHighlightRenderer {
|
||||
private MonitorHighlightRenderer() {
|
||||
}
|
||||
|
||||
public static boolean drawHighlight(PoseStack transformStack, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
|
||||
// Preserve normal behaviour when crouching.
|
||||
if (camera.getEntity().isCrouching()) return false;
|
||||
|
||||
var world = camera.getEntity().getCommandSenderWorld();
|
||||
var pos = hit.getBlockPos();
|
||||
|
||||
var tile = world.getBlockEntity(pos);
|
||||
if (!(tile instanceof MonitorBlockEntity monitor)) return false;
|
||||
|
||||
// Determine which sides are part of the external faces of the monitor, and so which need to be rendered.
|
||||
var faces = EnumSet.allOf(Direction.class);
|
||||
var front = monitor.getFront();
|
||||
faces.remove(front);
|
||||
if (monitor.getXIndex() != 0) faces.remove(monitor.getRight().getOpposite());
|
||||
if (monitor.getXIndex() != monitor.getWidth() - 1) faces.remove(monitor.getRight());
|
||||
if (monitor.getYIndex() != 0) faces.remove(monitor.getDown().getOpposite());
|
||||
if (monitor.getYIndex() != monitor.getHeight() - 1) faces.remove(monitor.getDown());
|
||||
|
||||
var cameraPos = camera.getPosition();
|
||||
transformStack.pushPose();
|
||||
transformStack.translate(pos.getX() - cameraPos.x(), pos.getY() - cameraPos.y(), pos.getZ() - cameraPos.z());
|
||||
|
||||
// I wish I could think of a better way to do this
|
||||
var buffer = bufferSource.getBuffer(RenderType.lines());
|
||||
var transform = transformStack.last().pose();
|
||||
var normal = transformStack.last().normal();
|
||||
if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 0, UP);
|
||||
if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 1, UP);
|
||||
if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 0, UP);
|
||||
if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 1, UP);
|
||||
if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, EAST);
|
||||
if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 1, EAST);
|
||||
if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, EAST);
|
||||
if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 1, EAST);
|
||||
if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, SOUTH);
|
||||
if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, normal, 1, 0, 0, SOUTH);
|
||||
if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, SOUTH);
|
||||
if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, normal, 1, 1, 0, SOUTH);
|
||||
|
||||
transformStack.popPose();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void line(VertexConsumer buffer, Matrix4f transform, Matrix3f normal, float x, float y, float z, Direction direction) {
|
||||
buffer
|
||||
.vertex(transform, x, y, z)
|
||||
.color(0, 0, 0, 0.4f)
|
||||
.normal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ())
|
||||
.endVertex();
|
||||
buffer
|
||||
.vertex(transform,
|
||||
x + direction.getStepX(),
|
||||
y + direction.getStepY(),
|
||||
z + direction.getStepZ()
|
||||
)
|
||||
.color(0, 0, 0, 0.4f)
|
||||
.normal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ())
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import dan200.computercraft.client.render.vbo.DirectBuffers;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class MonitorRenderState implements ClientMonitor.RenderState {
|
||||
@GuardedBy("allMonitors")
|
||||
private static final Set<MonitorRenderState> allMonitors = new HashSet<>();
|
||||
|
||||
public long lastRenderFrame = -1;
|
||||
public @Nullable BlockPos lastRenderPos = null;
|
||||
|
||||
public int tboBuffer;
|
||||
public int tboTexture;
|
||||
public int tboUniform;
|
||||
public @Nullable DirectVertexBuffer backgroundBuffer;
|
||||
public @Nullable DirectVertexBuffer foregroundBuffer;
|
||||
|
||||
/**
|
||||
* Create the appropriate buffer if needed.
|
||||
*
|
||||
* @param renderer The renderer to use.
|
||||
* @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer,
|
||||
* or this mode does not require one.
|
||||
*/
|
||||
public boolean createBuffer(MonitorRenderer renderer) {
|
||||
switch (renderer) {
|
||||
case TBO: {
|
||||
if (tboBuffer != 0) return false;
|
||||
|
||||
deleteBuffers();
|
||||
|
||||
tboBuffer = DirectBuffers.createBuffer();
|
||||
DirectBuffers.setEmptyBufferData(GL31.GL_TEXTURE_BUFFER, tboBuffer, GL15.GL_STATIC_DRAW);
|
||||
tboTexture = GlStateManager._genTexture();
|
||||
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, tboTexture);
|
||||
GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL30.GL_R8UI, tboBuffer);
|
||||
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, 0);
|
||||
|
||||
tboUniform = DirectBuffers.createBuffer();
|
||||
DirectBuffers.setEmptyBufferData(GL31.GL_UNIFORM_BUFFER, tboUniform, GL15.GL_STATIC_DRAW);
|
||||
|
||||
addMonitor();
|
||||
return true;
|
||||
}
|
||||
|
||||
case VBO:
|
||||
if (backgroundBuffer != null) return false;
|
||||
|
||||
deleteBuffers();
|
||||
backgroundBuffer = new DirectVertexBuffer();
|
||||
foregroundBuffer = new DirectVertexBuffer();
|
||||
addMonitor();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addMonitor() {
|
||||
synchronized (allMonitors) {
|
||||
allMonitors.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBuffers() {
|
||||
if (tboBuffer != 0) {
|
||||
DirectBuffers.deleteBuffer(GL31.GL_TEXTURE_BUFFER, tboBuffer);
|
||||
tboBuffer = 0;
|
||||
}
|
||||
|
||||
if (tboTexture != 0) {
|
||||
GlStateManager._deleteTexture(tboTexture);
|
||||
tboTexture = 0;
|
||||
}
|
||||
|
||||
if (tboUniform != 0) {
|
||||
DirectBuffers.deleteBuffer(GL31.GL_UNIFORM_BUFFER, tboUniform);
|
||||
tboUniform = 0;
|
||||
}
|
||||
|
||||
if (backgroundBuffer != null) {
|
||||
backgroundBuffer.close();
|
||||
backgroundBuffer = null;
|
||||
}
|
||||
|
||||
if (foregroundBuffer != null) {
|
||||
foregroundBuffer.close();
|
||||
foregroundBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (tboBuffer != 0 || backgroundBuffer != null) {
|
||||
synchronized (allMonitors) {
|
||||
allMonitors.remove(this);
|
||||
}
|
||||
|
||||
deleteBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
public static void destroyAll() {
|
||||
synchronized (allMonitors) {
|
||||
for (var iterator = allMonitors.iterator(); iterator.hasNext(); ) {
|
||||
var monitor = iterator.next();
|
||||
monitor.deleteBuffers();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.shaders.Uniform;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.client.FrameInfo;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.getColour;
|
||||
|
||||
public class MonitorTextureBufferShader extends ShaderInstance {
|
||||
public static final int UNIFORM_SIZE = 4 * 4 * 16 + 4 + 4 + 2 * 4 + 4;
|
||||
|
||||
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
private final int monitorData;
|
||||
private int uniformBuffer = 0;
|
||||
|
||||
private final @Nullable Uniform cursorBlink;
|
||||
|
||||
public MonitorTextureBufferShader(ResourceProvider provider, String location, VertexFormat format) throws IOException {
|
||||
super(provider, location, format);
|
||||
monitorData = GL31.glGetUniformBlockIndex(getId(), "MonitorData");
|
||||
if (monitorData == -1) throw new IllegalStateException("Could not find MonitorData uniform.");
|
||||
|
||||
cursorBlink = getUniformChecked("CursorBlink");
|
||||
|
||||
var tbo = getUniformChecked("Tbo");
|
||||
if (tbo != null) tbo.set(TEXTURE_INDEX - GL13.GL_TEXTURE0);
|
||||
}
|
||||
|
||||
public void setupUniform(int buffer) {
|
||||
uniformBuffer = buffer;
|
||||
|
||||
var cursorAlpha = FrameInfo.getGlobalCursorBlink() ? 1 : 0;
|
||||
if (cursorBlink != null && cursorBlink.getIntBuffer().get(0) != cursorAlpha) cursorBlink.set(cursorAlpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
super.apply();
|
||||
GL31.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, monitorData, uniformBuffer);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Uniform getUniformChecked(String name) {
|
||||
var uniform = getUniform(name);
|
||||
if (uniform == null) {
|
||||
LOGGER.warn("Monitor shader {} should have uniform {}, but it was not present.", getName(), name);
|
||||
}
|
||||
|
||||
return uniform;
|
||||
}
|
||||
|
||||
public static void setTerminalData(ByteBuffer buffer, Terminal terminal) {
|
||||
int width = terminal.getWidth(), height = terminal.getHeight();
|
||||
|
||||
var pos = 0;
|
||||
for (var y = 0; y < height; y++) {
|
||||
TextBuffer text = terminal.getLine(y), textColour = terminal.getTextColourLine(y), background = terminal.getBackgroundColourLine(y);
|
||||
for (var x = 0; x < width; x++) {
|
||||
buffer.put(pos, (byte) (text.charAt(x) & 0xFF));
|
||||
buffer.put(pos + 1, (byte) getColour(textColour.charAt(x), Colour.WHITE));
|
||||
buffer.put(pos + 2, (byte) getColour(background.charAt(x), Colour.BLACK));
|
||||
pos += 3;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.limit(pos);
|
||||
}
|
||||
|
||||
public static void setUniformData(ByteBuffer buffer, Terminal terminal) {
|
||||
var pos = 0;
|
||||
var palette = terminal.getPalette();
|
||||
for (var i = 0; i < 16; i++) {
|
||||
{
|
||||
var colour = palette.getColour(i);
|
||||
if (!terminal.isColour()) {
|
||||
var f = FixedWidthFontRenderer.toGreyscale(colour);
|
||||
buffer.putFloat(pos, f).putFloat(pos + 4, f).putFloat(pos + 8, f);
|
||||
} else {
|
||||
buffer.putFloat(pos, (float) colour[0]).putFloat(pos + 4, (float) colour[1]).putFloat(pos + 8, (float) colour[2]);
|
||||
}
|
||||
}
|
||||
|
||||
pos += 4 * 4; // std140 requires these are 4-wide
|
||||
}
|
||||
|
||||
var showCursor = FixedWidthFontRenderer.isCursorVisible(terminal);
|
||||
buffer
|
||||
.putInt(pos, terminal.getWidth()).putInt(pos + 4, terminal.getHeight())
|
||||
.putInt(pos + 8, showCursor ? terminal.getCursorX() : -2)
|
||||
.putInt(pos + 12, showCursor ? terminal.getCursorY() : -2)
|
||||
.putInt(pos + 16, 15 - terminal.getTextColour());
|
||||
|
||||
buffer.limit(UNIFORM_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.text;
|
||||
|
||||
import com.mojang.blaze3d.platform.MemoryTracker;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.*;
|
||||
import static org.lwjgl.system.MemoryUtil.*;
|
||||
|
||||
/**
|
||||
* An optimised copy of {@link FixedWidthFontRenderer} emitter emits directly to a {@link QuadEmitter} rather than
|
||||
* emitting to {@link VertexConsumer}. This allows us to emit vertices very quickly, when using the VBO renderer.
|
||||
* <p>
|
||||
* There are some limitations here:
|
||||
* <ul>
|
||||
* <li>No transformation matrix (not needed for VBOs).</li>
|
||||
* <li>Only works with {@link DefaultVertexFormat#POSITION_COLOR_TEX_LIGHTMAP}.</li>
|
||||
* <li>The buffer <strong>MUST</strong> be allocated with {@link MemoryTracker}, and not through any other means.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Note this is almost an exact copy of {@link FixedWidthFontRenderer}. While the code duplication is unfortunate,
|
||||
* it is measurably faster than introducing polymorphism into {@link FixedWidthFontRenderer}.
|
||||
*
|
||||
* <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
|
||||
* {@link FixedWidthFontRenderer}.
|
||||
*/
|
||||
public final class DirectFixedWidthFontRenderer {
|
||||
private DirectFixedWidthFontRenderer() {
|
||||
}
|
||||
|
||||
private static void drawChar(QuadEmitter emitter, float x, float y, int index, byte[] colour) {
|
||||
// Short circuit to avoid the common case - the texture should be blank here after all.
|
||||
if (index == '\0' || index == ' ') return;
|
||||
|
||||
var column = index % 16;
|
||||
var row = index / 16;
|
||||
|
||||
var xStart = 1 + column * (FONT_WIDTH + 2);
|
||||
var yStart = 1 + row * (FONT_HEIGHT + 2);
|
||||
|
||||
quad(
|
||||
emitter, x, y, x + FONT_WIDTH, y + FONT_HEIGHT, 0, colour,
|
||||
xStart / WIDTH, yStart / WIDTH, (xStart + FONT_WIDTH) / WIDTH, (yStart + FONT_HEIGHT) / WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
private static void drawQuad(QuadEmitter emitter, float x, float y, float width, float height, Palette palette, char colourIndex) {
|
||||
var colour = palette.getRenderColours(getColour(colourIndex, Colour.BLACK));
|
||||
quad(emitter, x, y, x + width, y + height, 0f, colour, BACKGROUND_START, BACKGROUND_START, BACKGROUND_END, BACKGROUND_END);
|
||||
}
|
||||
|
||||
private static void drawBackground(
|
||||
QuadEmitter emitter, float x, float y, TextBuffer backgroundColour, Palette palette,
|
||||
float leftMarginSize, float rightMarginSize, float height
|
||||
) {
|
||||
if (leftMarginSize > 0) {
|
||||
drawQuad(emitter, x - leftMarginSize, y, leftMarginSize, height, palette, backgroundColour.charAt(0));
|
||||
}
|
||||
|
||||
if (rightMarginSize > 0) {
|
||||
drawQuad(emitter, x + backgroundColour.length() * FONT_WIDTH, y, rightMarginSize, height, palette, backgroundColour.charAt(backgroundColour.length() - 1));
|
||||
}
|
||||
|
||||
// Batch together runs of identical background cells.
|
||||
var blockStart = 0;
|
||||
var blockColour = '\0';
|
||||
for (var i = 0; i < backgroundColour.length(); i++) {
|
||||
var colourIndex = backgroundColour.charAt(i);
|
||||
if (colourIndex == blockColour) continue;
|
||||
|
||||
if (blockColour != '\0') {
|
||||
drawQuad(emitter, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (i - blockStart), height, palette, blockColour);
|
||||
}
|
||||
|
||||
blockColour = colourIndex;
|
||||
blockStart = i;
|
||||
}
|
||||
|
||||
if (blockColour != '\0') {
|
||||
drawQuad(emitter, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (backgroundColour.length() - blockStart), height, palette, blockColour);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawString(QuadEmitter emitter, float x, float y, TextBuffer text, TextBuffer textColour, Palette palette) {
|
||||
for (var i = 0; i < text.length(); i++) {
|
||||
var colour = palette.getRenderColours(getColour(textColour.charAt(i), Colour.BLACK));
|
||||
|
||||
int index = text.charAt(i);
|
||||
if (index > 255) index = '?';
|
||||
drawChar(emitter, x + i * FONT_WIDTH, y, index, colour);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void drawTerminalForeground(QuadEmitter emitter, float x, float y, Terminal terminal) {
|
||||
var palette = terminal.getPalette();
|
||||
var height = terminal.getHeight();
|
||||
|
||||
// The main text
|
||||
for (var i = 0; i < height; i++) {
|
||||
var rowY = y + FONT_HEIGHT * i;
|
||||
drawString(
|
||||
emitter, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
|
||||
palette
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawTerminalBackground(
|
||||
QuadEmitter emitter, float x, float y, Terminal terminal,
|
||||
float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize
|
||||
) {
|
||||
var palette = terminal.getPalette();
|
||||
var height = terminal.getHeight();
|
||||
|
||||
// Top and bottom margins
|
||||
drawBackground(
|
||||
emitter, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
|
||||
leftMarginSize, rightMarginSize, topMarginSize
|
||||
);
|
||||
|
||||
drawBackground(
|
||||
emitter, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
|
||||
leftMarginSize, rightMarginSize, bottomMarginSize
|
||||
);
|
||||
|
||||
// The main text
|
||||
for (var i = 0; i < height; i++) {
|
||||
var rowY = y + FONT_HEIGHT * i;
|
||||
drawBackground(
|
||||
emitter, x, rowY, terminal.getBackgroundColourLine(i), palette,
|
||||
leftMarginSize, rightMarginSize, FONT_HEIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawCursor(QuadEmitter emitter, float x, float y, Terminal terminal) {
|
||||
if (isCursorVisible(terminal)) {
|
||||
var colour = terminal.getPalette().getRenderColours(15 - terminal.getTextColour());
|
||||
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getVertexCount(Terminal terminal) {
|
||||
return (terminal.getHeight() + 2) * (terminal.getWidth() + 2) * 2;
|
||||
}
|
||||
|
||||
private static void quad(QuadEmitter buffer, float x1, float y1, float x2, float y2, float z, byte[] rgba, float u1, float v1, float u2, float v2) {
|
||||
buffer.quad(x1, y1, x2, y2, z, rgba, u1, v1, u2, v2);
|
||||
}
|
||||
|
||||
public interface QuadEmitter {
|
||||
VertexFormat format();
|
||||
|
||||
ByteBuffer buffer();
|
||||
|
||||
void quad(float x1, float y1, float x2, float y2, float z, byte[] rgba, float u1, float v1, float u2, float v2);
|
||||
}
|
||||
|
||||
public record ByteBufferEmitter(ByteBuffer buffer) implements QuadEmitter {
|
||||
@Override
|
||||
public VertexFormat format() {
|
||||
return RenderTypes.TERMINAL.format();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quad(float x1, float y1, float x2, float y2, float z, byte[] rgba, float u1, float v1, float u2, float v2) {
|
||||
DirectFixedWidthFontRenderer.quad(buffer, x1, y1, x2, y2, z, rgba, u1, v1, u2, v2);
|
||||
}
|
||||
}
|
||||
|
||||
static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, byte[] rgba, float u1, float v1, float u2, float v2) {
|
||||
// Emit a single quad to our buffer. This uses Unsafe (well, LWJGL's MemoryUtil) to directly blit bytes to the
|
||||
// underlying buffer. This allows us to have a single bounds check up-front, rather than one for every write.
|
||||
// This provides significant performance gains, at the cost of well, using Unsafe.
|
||||
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(rgba:BBBB)(uv1:FF)(uv2:SS),
|
||||
// which matches the POSITION_COLOR_TEX_LIGHTMAP vertex format.
|
||||
|
||||
var position = buffer.position();
|
||||
var addr = MemoryUtil.memAddress(buffer);
|
||||
|
||||
// We're doing terrible unsafe hacks below, so let's be really sure that what we're doing is reasonable.
|
||||
if (position < 0 || 112 > buffer.limit() - position) throw new IndexOutOfBoundsException();
|
||||
// Require the pointer to be aligned to a 32-bit boundary.
|
||||
if ((addr & 3) != 0) throw new IllegalStateException("Memory is not aligned");
|
||||
// Also assert the length of the array. This appears to help elide bounds checks on the array in some circumstances.
|
||||
if (rgba.length != 4) throw new IllegalStateException();
|
||||
|
||||
memPutFloat(addr + 0, x1);
|
||||
memPutFloat(addr + 4, y1);
|
||||
memPutFloat(addr + 8, z);
|
||||
memPutByte(addr + 12, rgba[0]);
|
||||
memPutByte(addr + 13, rgba[1]);
|
||||
memPutByte(addr + 14, rgba[2]);
|
||||
memPutByte(addr + 15, (byte) 255);
|
||||
memPutFloat(addr + 16, u1);
|
||||
memPutFloat(addr + 20, v1);
|
||||
memPutShort(addr + 24, (short) 0xF0);
|
||||
memPutShort(addr + 26, (short) 0xF0);
|
||||
|
||||
memPutFloat(addr + 28, x1);
|
||||
memPutFloat(addr + 32, y2);
|
||||
memPutFloat(addr + 36, z);
|
||||
memPutByte(addr + 40, rgba[0]);
|
||||
memPutByte(addr + 41, rgba[1]);
|
||||
memPutByte(addr + 42, rgba[2]);
|
||||
memPutByte(addr + 43, (byte) 255);
|
||||
memPutFloat(addr + 44, u1);
|
||||
memPutFloat(addr + 48, v2);
|
||||
memPutShort(addr + 52, (short) 0xF0);
|
||||
memPutShort(addr + 54, (short) 0xF0);
|
||||
|
||||
memPutFloat(addr + 56, x2);
|
||||
memPutFloat(addr + 60, y2);
|
||||
memPutFloat(addr + 64, z);
|
||||
memPutByte(addr + 68, rgba[0]);
|
||||
memPutByte(addr + 69, rgba[1]);
|
||||
memPutByte(addr + 70, rgba[2]);
|
||||
memPutByte(addr + 71, (byte) 255);
|
||||
memPutFloat(addr + 72, u2);
|
||||
memPutFloat(addr + 76, v2);
|
||||
memPutShort(addr + 80, (short) 0xF0);
|
||||
memPutShort(addr + 82, (short) 0xF0);
|
||||
|
||||
memPutFloat(addr + 84, x2);
|
||||
memPutFloat(addr + 88, y1);
|
||||
memPutFloat(addr + 92, z);
|
||||
memPutByte(addr + 96, rgba[0]);
|
||||
memPutByte(addr + 97, rgba[1]);
|
||||
memPutByte(addr + 98, rgba[2]);
|
||||
memPutByte(addr + 99, (byte) 255);
|
||||
memPutFloat(addr + 100, u2);
|
||||
memPutFloat(addr + 104, v1);
|
||||
memPutShort(addr + 108, (short) 0xF0);
|
||||
memPutShort(addr + 110, (short) 0xF0);
|
||||
|
||||
// Finally increment the position.
|
||||
buffer.position(position + 112);
|
||||
|
||||
// Well done for getting to the end of this method. I recommend you take a break and go look at cute puppies.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.text;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import com.mojang.math.Vector3f;
|
||||
import dan200.computercraft.client.FrameInfo;
|
||||
import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
/**
|
||||
* Handles rendering fixed width text and computer terminals.
|
||||
* <p>
|
||||
* This class has several modes of usage:
|
||||
* <ul>
|
||||
* <li>{@link #drawString}: Drawing basic text without a terminal (such as for printouts). Unlike the other methods,
|
||||
* this accepts a lightmap coordinate as, unlike terminals, printed pages render fullbright.</li>
|
||||
* <li>{@link #drawTerminal}: Draw a terminal with a cursor. This is used by the various computer GUIs to render the
|
||||
* whole term.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
|
||||
* {@link DirectFixedWidthFontRenderer}.
|
||||
*/
|
||||
public final class FixedWidthFontRenderer {
|
||||
public static final ResourceLocation FONT = new ResourceLocation("computercraft", "textures/gui/term_font.png");
|
||||
|
||||
public static final int FONT_HEIGHT = 9;
|
||||
public static final int FONT_WIDTH = 6;
|
||||
static final float WIDTH = 256.0f;
|
||||
|
||||
static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH;
|
||||
static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
|
||||
|
||||
private static final byte[] BLACK = new byte[]{ byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), (byte) 255 };
|
||||
private static final float Z_OFFSET = 1e-3f;
|
||||
|
||||
private FixedWidthFontRenderer() {
|
||||
}
|
||||
|
||||
private static byte byteColour(float c) {
|
||||
return (byte) (int) (c * 255);
|
||||
}
|
||||
|
||||
public static float toGreyscale(double[] rgb) {
|
||||
return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3);
|
||||
}
|
||||
|
||||
public static int getColour(char c, Colour def) {
|
||||
return 15 - Terminal.getColour(c, def);
|
||||
}
|
||||
|
||||
private static void drawChar(QuadEmitter emitter, float x, float y, int index, byte[] colour, int light) {
|
||||
// Short circuit to avoid the common case - the texture should be blank here after all.
|
||||
if (index == '\0' || index == ' ') return;
|
||||
|
||||
var column = index % 16;
|
||||
var row = index / 16;
|
||||
|
||||
var xStart = 1 + column * (FONT_WIDTH + 2);
|
||||
var yStart = 1 + row * (FONT_HEIGHT + 2);
|
||||
|
||||
quad(
|
||||
emitter, x, y, x + FONT_WIDTH, y + FONT_HEIGHT, 0, colour,
|
||||
xStart / WIDTH, yStart / WIDTH, (xStart + FONT_WIDTH) / WIDTH, (yStart + FONT_HEIGHT) / WIDTH, light
|
||||
);
|
||||
}
|
||||
|
||||
public static void drawQuad(QuadEmitter emitter, float x, float y, float z, float width, float height, byte[] colour, int light) {
|
||||
quad(emitter, x, y, x + width, y + height, z, colour, BACKGROUND_START, BACKGROUND_START, BACKGROUND_END, BACKGROUND_END, light);
|
||||
}
|
||||
|
||||
private static void drawQuad(QuadEmitter emitter, float x, float y, float width, float height, Palette palette, char colourIndex, int light) {
|
||||
var colour = palette.getRenderColours(getColour(colourIndex, Colour.BLACK));
|
||||
drawQuad(emitter, x, y, 0, width, height, colour, light);
|
||||
}
|
||||
|
||||
private static void drawBackground(
|
||||
QuadEmitter emitter, float x, float y, TextBuffer backgroundColour, Palette palette,
|
||||
float leftMarginSize, float rightMarginSize, float height, int light
|
||||
) {
|
||||
if (leftMarginSize > 0) {
|
||||
drawQuad(emitter, x - leftMarginSize, y, leftMarginSize, height, palette, backgroundColour.charAt(0), light);
|
||||
}
|
||||
|
||||
if (rightMarginSize > 0) {
|
||||
drawQuad(emitter, x + backgroundColour.length() * FONT_WIDTH, y, rightMarginSize, height, palette, backgroundColour.charAt(backgroundColour.length() - 1), light);
|
||||
}
|
||||
|
||||
// Batch together runs of identical background cells.
|
||||
var blockStart = 0;
|
||||
var blockColour = '\0';
|
||||
for (var i = 0; i < backgroundColour.length(); i++) {
|
||||
var colourIndex = backgroundColour.charAt(i);
|
||||
if (colourIndex == blockColour) continue;
|
||||
|
||||
if (blockColour != '\0') {
|
||||
drawQuad(emitter, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (i - blockStart), height, palette, blockColour, light);
|
||||
}
|
||||
|
||||
blockColour = colourIndex;
|
||||
blockStart = i;
|
||||
}
|
||||
|
||||
if (blockColour != '\0') {
|
||||
drawQuad(emitter, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (backgroundColour.length() - blockStart), height, palette, blockColour, light);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawString(QuadEmitter emitter, float x, float y, TextBuffer text, TextBuffer textColour, Palette palette, int light) {
|
||||
for (var i = 0; i < text.length(); i++) {
|
||||
var colour = palette.getRenderColours(getColour(textColour.charAt(i), Colour.BLACK));
|
||||
|
||||
int index = text.charAt(i);
|
||||
if (index > 255) index = '?';
|
||||
drawChar(emitter, x + i * FONT_WIDTH, y, index, colour, light);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void drawTerminalForeground(QuadEmitter emitter, float x, float y, Terminal terminal) {
|
||||
var palette = terminal.getPalette();
|
||||
var height = terminal.getHeight();
|
||||
|
||||
// The main text
|
||||
for (var i = 0; i < height; i++) {
|
||||
var rowY = y + FONT_HEIGHT * i;
|
||||
drawString(
|
||||
emitter, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
|
||||
palette, FULL_BRIGHT_LIGHTMAP
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawTerminalBackground(
|
||||
QuadEmitter emitter, float x, float y, Terminal terminal,
|
||||
float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize
|
||||
) {
|
||||
var palette = terminal.getPalette();
|
||||
var height = terminal.getHeight();
|
||||
|
||||
// Top and bottom margins
|
||||
drawBackground(
|
||||
emitter, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
|
||||
leftMarginSize, rightMarginSize, topMarginSize, FULL_BRIGHT_LIGHTMAP
|
||||
);
|
||||
|
||||
drawBackground(
|
||||
emitter, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
|
||||
leftMarginSize, rightMarginSize, bottomMarginSize, FULL_BRIGHT_LIGHTMAP
|
||||
);
|
||||
|
||||
// The main text
|
||||
for (var i = 0; i < height; i++) {
|
||||
var rowY = y + FONT_HEIGHT * i;
|
||||
drawBackground(
|
||||
emitter, x, rowY, terminal.getBackgroundColourLine(i), palette,
|
||||
leftMarginSize, rightMarginSize, FONT_HEIGHT, FULL_BRIGHT_LIGHTMAP
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isCursorVisible(Terminal terminal) {
|
||||
if (!terminal.getCursorBlink()) return false;
|
||||
|
||||
var cursorX = terminal.getCursorX();
|
||||
var cursorY = terminal.getCursorY();
|
||||
return cursorX >= 0 && cursorX < terminal.getWidth() && cursorY >= 0 && cursorY < terminal.getHeight();
|
||||
}
|
||||
|
||||
public static void drawCursor(QuadEmitter emitter, float x, float y, Terminal terminal) {
|
||||
if (isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()) {
|
||||
var colour = terminal.getPalette().getRenderColours(15 - terminal.getTextColour());
|
||||
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, FULL_BRIGHT_LIGHTMAP);
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawTerminal(
|
||||
QuadEmitter emitter, float x, float y, Terminal terminal,
|
||||
float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize
|
||||
) {
|
||||
drawTerminalBackground(
|
||||
emitter, x, y, terminal,
|
||||
topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize
|
||||
);
|
||||
|
||||
// Render the foreground with a slight offset. By calling .translate() on the matrix itself, we're translating
|
||||
// in screen space, rather than in model/view space.
|
||||
// It's definitely not perfect, but better than z fighting!
|
||||
var transformBackup = emitter.poseMatrix().copy();
|
||||
emitter.poseMatrix().translate(new Vector3f(0, 0, Z_OFFSET));
|
||||
|
||||
drawTerminalForeground(emitter, x, y, terminal);
|
||||
drawCursor(emitter, x, y, terminal);
|
||||
|
||||
emitter.poseMatrix().load(transformBackup);
|
||||
}
|
||||
|
||||
public static void drawEmptyTerminal(QuadEmitter emitter, float x, float y, float width, float height) {
|
||||
drawQuad(emitter, x, y, 0, width, height, BLACK, FULL_BRIGHT_LIGHTMAP);
|
||||
}
|
||||
|
||||
public record QuadEmitter(Matrix4f poseMatrix, VertexConsumer consumer) {
|
||||
}
|
||||
|
||||
public static QuadEmitter toVertexConsumer(PoseStack transform, VertexConsumer consumer) {
|
||||
return new QuadEmitter(transform.last().pose(), consumer);
|
||||
}
|
||||
|
||||
private static void quad(QuadEmitter c, float x1, float y1, float x2, float y2, float z, byte[] rgba, float u1, float v1, float u2, float v2, int light) {
|
||||
var poseMatrix = c.poseMatrix();
|
||||
var consumer = c.consumer();
|
||||
byte r = rgba[0], g = rgba[1], b = rgba[2], a = rgba[3];
|
||||
|
||||
consumer.vertex(poseMatrix, x1, y1, z).color(r, g, b, a).uv(u1, v1).uv2(light).endVertex();
|
||||
consumer.vertex(poseMatrix, x1, y2, z).color(r, g, b, a).uv(u1, v2).uv2(light).endVertex();
|
||||
consumer.vertex(poseMatrix, x2, y2, z).color(r, g, b, a).uv(u2, v2).uv2(light).endVertex();
|
||||
consumer.vertex(poseMatrix, x2, y1, z).color(r, g, b, a).uv(u2, v1).uv2(light).endVertex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.vbo;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
import net.minecraft.Util;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL15C;
|
||||
import org.lwjgl.opengl.GL45C;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Provides utilities to interact with OpenGL's buffer objects, either using direct state access or binding/unbinding
|
||||
* it.
|
||||
*/
|
||||
public class DirectBuffers {
|
||||
public static final boolean HAS_DSA;
|
||||
static final boolean ON_LINUX = Util.getPlatform() == Util.OS.LINUX;
|
||||
|
||||
static {
|
||||
var capabilities = GL.getCapabilities();
|
||||
HAS_DSA = capabilities.OpenGL45 || capabilities.GL_ARB_direct_state_access;
|
||||
}
|
||||
|
||||
public static int createBuffer() {
|
||||
return HAS_DSA ? GL45C.glCreateBuffers() : GL15C.glGenBuffers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a previously created buffer.
|
||||
* <p>
|
||||
* On Linux, {@link GlStateManager#_glDeleteBuffers(int)} clears a buffer before deleting it. However, this involves
|
||||
* binding and unbinding the buffer, conflicting with {@link BufferUploader}'s cache. This deletion method uses
|
||||
* our existing {@link #setEmptyBufferData(int, int, int)}, which correctly handles clearing the buffer.
|
||||
*
|
||||
* @param type The buffer's type.
|
||||
* @param id The buffer's ID.
|
||||
*/
|
||||
public static void deleteBuffer(int type, int id) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
if (ON_LINUX) DirectBuffers.setEmptyBufferData(type, id, GL15C.GL_DYNAMIC_DRAW);
|
||||
GL15C.glDeleteBuffers(id);
|
||||
}
|
||||
|
||||
public static void setBufferData(int type, int id, ByteBuffer buffer, int flags) {
|
||||
if (HAS_DSA) {
|
||||
GL45C.glNamedBufferData(id, buffer, flags);
|
||||
} else {
|
||||
if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
|
||||
GlStateManager._glBindBuffer(type, id);
|
||||
GlStateManager._glBufferData(type, buffer, flags);
|
||||
GlStateManager._glBindBuffer(type, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setEmptyBufferData(int type, int id, int flags) {
|
||||
if (HAS_DSA) {
|
||||
GL45C.glNamedBufferData(id, 0, flags);
|
||||
} else {
|
||||
if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
|
||||
GlStateManager._glBindBuffer(type, id);
|
||||
GlStateManager._glBufferData(type, 0, flags);
|
||||
GlStateManager._glBindBuffer(type, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.render.vbo;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
import com.mojang.blaze3d.vertex.VertexBuffer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import com.mojang.math.Matrix4f;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL15C;
|
||||
import org.lwjgl.opengl.GL45C;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A version of {@link VertexBuffer} which allows uploading {@link ByteBuffer}s directly.
|
||||
* <p>
|
||||
* This should probably be its own class (rather than subclassing), but I need access to {@link VertexBuffer#drawWithShader}.
|
||||
*/
|
||||
public class DirectVertexBuffer extends VertexBuffer {
|
||||
private int actualIndexCount;
|
||||
|
||||
public DirectVertexBuffer() {
|
||||
if (DirectBuffers.HAS_DSA) {
|
||||
RenderSystem.glDeleteBuffers(vertexBufferId);
|
||||
if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
|
||||
vertexBufferId = GL45C.glCreateBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
public void upload(int vertexCount, VertexFormat.Mode mode, VertexFormat format, ByteBuffer buffer) {
|
||||
bind();
|
||||
|
||||
this.mode = mode;
|
||||
actualIndexCount = indexCount = mode.indexCount(vertexCount);
|
||||
indexType = VertexFormat.IndexType.SHORT;
|
||||
|
||||
RenderSystem.assertOnRenderThread();
|
||||
|
||||
DirectBuffers.setBufferData(GL15.GL_ARRAY_BUFFER, vertexBufferId, buffer, GL15.GL_STATIC_DRAW);
|
||||
if (format != this.format) {
|
||||
if (this.format != null) this.format.clearBufferState();
|
||||
this.format = format;
|
||||
|
||||
GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, vertexBufferId);
|
||||
format.setupBufferState();
|
||||
GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
var indexBuffer = RenderSystem.getSequentialBuffer(mode);
|
||||
if (indexBuffer != sequentialIndices || !indexBuffer.hasStorage(indexCount)) {
|
||||
indexBuffer.bind(indexCount);
|
||||
sequentialIndices = indexBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawWithShader(Matrix4f modelView, Matrix4f projection, ShaderInstance shader, int indexCount) {
|
||||
this.indexCount = indexCount;
|
||||
drawWithShader(modelView, projection, shader);
|
||||
this.indexCount = actualIndexCount;
|
||||
}
|
||||
|
||||
public int getIndexCount() {
|
||||
return actualIndexCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import com.mojang.blaze3d.audio.Channel;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import org.lwjgl.BufferUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class DfpwmStream implements AudioStream {
|
||||
private static final int PREC = 10;
|
||||
private static final int LPF_STRENGTH = 140;
|
||||
|
||||
private static final AudioFormat MONO_8 = new AudioFormat(SpeakerPeripheral.SAMPLE_RATE, 8, 1, true, false);
|
||||
|
||||
private final Queue<ByteBuffer> buffers = new ArrayDeque<>(2);
|
||||
|
||||
/**
|
||||
* The {@link Channel} which this sound is playing on.
|
||||
*
|
||||
* @see SpeakerInstance#pushAudio(ByteBuf)
|
||||
*/
|
||||
@Nullable
|
||||
Channel channel;
|
||||
|
||||
/**
|
||||
* The underlying {@link SoundEngine} executor.
|
||||
*
|
||||
* @see SpeakerInstance#pushAudio(ByteBuf)
|
||||
* @see SoundEngine#executor
|
||||
*/
|
||||
@Nullable
|
||||
Executor executor;
|
||||
|
||||
private int charge = 0; // q
|
||||
private int strength = 0; // s
|
||||
private int lowPassCharge;
|
||||
private boolean previousBit = false;
|
||||
|
||||
DfpwmStream() {
|
||||
}
|
||||
|
||||
void push(ByteBuf input) {
|
||||
var readable = input.readableBytes();
|
||||
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
|
||||
|
||||
for (var i = 0; i < readable; i++) {
|
||||
var inputByte = input.readByte();
|
||||
for (var j = 0; j < 8; j++) {
|
||||
var currentBit = (inputByte & 1) != 0;
|
||||
var target = currentBit ? 127 : -128;
|
||||
|
||||
// q' <- q + (s * (t - q) + 128)/256
|
||||
var nextCharge = charge + ((strength * (target - charge) + (1 << (PREC - 1))) >> PREC);
|
||||
if (nextCharge == charge && nextCharge != target) nextCharge += currentBit ? 1 : -1;
|
||||
|
||||
var z = currentBit == previousBit ? (1 << PREC) - 1 : 0;
|
||||
|
||||
var nextStrength = strength;
|
||||
if (strength != z) nextStrength += currentBit == previousBit ? 1 : -1;
|
||||
if (nextStrength < 2 << (PREC - 8)) nextStrength = 2 << (PREC - 8);
|
||||
|
||||
// Apply antijerk
|
||||
var chargeWithAntijerk = currentBit == previousBit
|
||||
? nextCharge
|
||||
: nextCharge + charge + 1 >> 1;
|
||||
|
||||
// And low pass filter: outQ <- outQ + ((expectedOutput - outQ) x 140 / 256)
|
||||
lowPassCharge += ((chargeWithAntijerk - lowPassCharge) * LPF_STRENGTH + 0x80) >> 8;
|
||||
|
||||
charge = nextCharge;
|
||||
strength = nextStrength;
|
||||
previousBit = currentBit;
|
||||
|
||||
// OpenAL expects signed data ([0, 255]) while we produce unsigned ([-128, 127]). Do some bit twiddling
|
||||
// magic to convert.
|
||||
output.put((byte) ((lowPassCharge & 0xFF) ^ 0x80));
|
||||
|
||||
inputByte >>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
output.flip();
|
||||
synchronized (this) {
|
||||
buffers.add(output);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioFormat getFormat() {
|
||||
return MONO_8;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public synchronized ByteBuffer read(int capacity) {
|
||||
var result = BufferUtils.createByteBuffer(capacity);
|
||||
while (result.hasRemaining()) {
|
||||
var head = buffers.peek();
|
||||
if (head == null) break;
|
||||
|
||||
var toRead = Math.min(head.remaining(), result.remaining());
|
||||
result.put(result.position(), head, head.position(), toRead);
|
||||
result.position(result.position() + toRead);
|
||||
head.position(head.position() + toRead);
|
||||
|
||||
if (head.hasRemaining()) break;
|
||||
buffers.remove();
|
||||
}
|
||||
|
||||
result.flip();
|
||||
|
||||
// This is naughty, but ensures we're not enqueuing empty buffers when the stream is exhausted.
|
||||
return result.remaining() == 0 ? null : result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
buffers.clear();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return buffers.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
|
||||
*/
|
||||
public class SpeakerInstance {
|
||||
public static final ResourceLocation DFPWM_STREAM = new ResourceLocation(ComputerCraftAPI.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played");
|
||||
|
||||
private @Nullable DfpwmStream currentStream;
|
||||
private @Nullable SpeakerSound sound;
|
||||
|
||||
SpeakerInstance() {
|
||||
}
|
||||
|
||||
public synchronized void pushAudio(ByteBuf buffer) {
|
||||
var sound = this.sound;
|
||||
|
||||
var stream = currentStream;
|
||||
if (stream == null) stream = currentStream = new DfpwmStream();
|
||||
var exhausted = stream.isEmpty();
|
||||
stream.push(buffer);
|
||||
|
||||
// If we've got nothing left in the buffer, enqueue an additional one just in case.
|
||||
if (exhausted && sound != null && sound.stream == stream && stream.channel != null && stream.executor != null) {
|
||||
var actualStream = sound.stream;
|
||||
stream.executor.execute(() -> {
|
||||
var channel = Nullability.assertNonNull(actualStream.channel);
|
||||
if (!channel.stopped()) channel.pumpBuffers(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void playAudio(SpeakerPosition position, float volume) {
|
||||
var soundManager = Minecraft.getInstance().getSoundManager();
|
||||
|
||||
if (sound != null && sound.stream != currentStream) {
|
||||
soundManager.stop(sound);
|
||||
sound = null;
|
||||
}
|
||||
|
||||
if (sound != null && !soundManager.isActive(sound)) sound = null;
|
||||
|
||||
if (sound == null && currentStream != null) {
|
||||
sound = new SpeakerSound(DFPWM_STREAM, currentStream, position, volume, 1.0f);
|
||||
soundManager.play(sound);
|
||||
}
|
||||
}
|
||||
|
||||
public void playSound(SpeakerPosition position, ResourceLocation location, float volume, float pitch) {
|
||||
var soundManager = Minecraft.getInstance().getSoundManager();
|
||||
currentStream = null;
|
||||
|
||||
if (sound != null) {
|
||||
soundManager.stop(sound);
|
||||
sound = null;
|
||||
}
|
||||
|
||||
sound = new SpeakerSound(location, null, position, volume, pitch);
|
||||
soundManager.play(sound);
|
||||
}
|
||||
|
||||
void setPosition(SpeakerPosition position) {
|
||||
if (sound != null) sound.setPosition(position);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (sound != null) Minecraft.getInstance().getSoundManager().stop(sound);
|
||||
|
||||
currentStream = null;
|
||||
sound = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import com.mojang.blaze3d.audio.Channel;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Maps speakers source IDs to a {@link SpeakerInstance}.
|
||||
*/
|
||||
public class SpeakerManager {
|
||||
private static final Map<UUID, SpeakerInstance> sounds = new ConcurrentHashMap<>();
|
||||
|
||||
public static void onPlayStreaming(SoundEngine engine, Channel channel, AudioStream stream) {
|
||||
if (!(stream instanceof DfpwmStream dfpwmStream)) return;
|
||||
|
||||
// Associate the stream with the current channel, so SpeakerInstance.pushAudio can queue audio immediately.
|
||||
dfpwmStream.channel = channel;
|
||||
dfpwmStream.executor = engine.executor;
|
||||
}
|
||||
|
||||
public static SpeakerInstance getSound(UUID source) {
|
||||
return sounds.computeIfAbsent(source, x -> new SpeakerInstance());
|
||||
}
|
||||
|
||||
public static void stopSound(UUID source) {
|
||||
var sound = sounds.remove(source);
|
||||
if (sound != null) sound.stop();
|
||||
}
|
||||
|
||||
public static void moveSound(UUID source, SpeakerPosition position) {
|
||||
var sound = sounds.get(source);
|
||||
if (sound != null) sound.setPosition(position);
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
sounds.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.sound;
|
||||
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.client.resources.sounds.AbstractSoundInstance;
|
||||
import net.minecraft.client.resources.sounds.Sound;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.client.resources.sounds.TickableSoundInstance;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.SoundBufferLibrary;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class SpeakerSound extends AbstractSoundInstance implements TickableSoundInstance {
|
||||
@Nullable
|
||||
DfpwmStream stream;
|
||||
|
||||
private @Nullable Entity entity;
|
||||
|
||||
private boolean stopped = false;
|
||||
|
||||
SpeakerSound(ResourceLocation sound, @Nullable DfpwmStream stream, SpeakerPosition position, float volume, float pitch) {
|
||||
super(sound, SoundSource.RECORDS, SoundInstance.createUnseededRandom());
|
||||
setPosition(position);
|
||||
this.stream = stream;
|
||||
this.volume = volume;
|
||||
this.pitch = pitch;
|
||||
attenuation = Attenuation.LINEAR;
|
||||
}
|
||||
|
||||
void setPosition(SpeakerPosition position) {
|
||||
x = position.position().x;
|
||||
y = position.position().y;
|
||||
z = position.position().z;
|
||||
entity = position.entity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStopped() {
|
||||
return stopped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (entity == null) return;
|
||||
if (!entity.isAlive()) {
|
||||
stopped = true;
|
||||
looping = false;
|
||||
} else {
|
||||
x = entity.getX();
|
||||
y = entity.getY();
|
||||
z = entity.getZ();
|
||||
}
|
||||
}
|
||||
|
||||
@ForgeOverride
|
||||
public CompletableFuture<AudioStream> getStream(SoundBufferLibrary soundBuffers, Sound sound, boolean looping) {
|
||||
return stream != null ? CompletableFuture.completedFuture(stream) : soundBuffers.getStream(sound.getPath(), looping);
|
||||
}
|
||||
|
||||
public @Nullable AudioStream getStream() {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
|
||||
private final ResourceLocation leftOffModel;
|
||||
private final ResourceLocation rightOffModel;
|
||||
private final ResourceLocation leftOnModel;
|
||||
private final ResourceLocation rightOnModel;
|
||||
|
||||
public TurtleModemModeller(boolean advanced) {
|
||||
if (advanced) {
|
||||
leftOffModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_advanced_off_left");
|
||||
rightOffModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_advanced_off_right");
|
||||
leftOnModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_advanced_on_left");
|
||||
rightOnModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_advanced_on_right");
|
||||
} else {
|
||||
leftOffModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_normal_off_left");
|
||||
rightOffModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_normal_off_right");
|
||||
leftOnModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_normal_on_left");
|
||||
rightOnModel = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_normal_on_right");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransformedModel getModel(TurtleModem upgrade, @Nullable ITurtleAccess turtle, TurtleSide side) {
|
||||
var active = false;
|
||||
if (turtle != null) {
|
||||
var turtleNBT = turtle.getUpgradeNBTData(side);
|
||||
active = turtleNBT.contains("active") && turtleNBT.getBoolean("active");
|
||||
}
|
||||
|
||||
return side == TurtleSide.LEFT
|
||||
? TransformedModel.of(active ? leftOnModel : leftOffModel)
|
||||
: TransformedModel.of(active ? rightOnModel : rightOffModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.impl.UpgradeManager;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class TurtleUpgradeModellers {
|
||||
private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side) ->
|
||||
new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
|
||||
|
||||
private static final Map<TurtleUpgradeSerialiser<?>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* In order to avoid a double lookup of {@link ITurtleUpgrade} to {@link UpgradeManager.UpgradeWrapper} to
|
||||
* {@link TurtleUpgradeModeller}, we maintain a cache here.
|
||||
* <p>
|
||||
* Turtle upgrades may be removed as part of datapack reloads, so we use a weak map to avoid the memory leak.
|
||||
*/
|
||||
private static final WeakHashMap<ITurtleUpgrade, TurtleUpgradeModeller<?>> modelCache = new WeakHashMap<>();
|
||||
|
||||
private TurtleUpgradeModellers() {
|
||||
}
|
||||
|
||||
public static <T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
|
||||
synchronized (turtleModels) {
|
||||
if (turtleModels.containsKey(serialiser)) {
|
||||
throw new IllegalStateException("Modeller already registered for serialiser");
|
||||
}
|
||||
|
||||
turtleModels.put(serialiser, modeller);
|
||||
}
|
||||
}
|
||||
|
||||
public static TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess access, TurtleSide side) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var modeller = (TurtleUpgradeModeller<ITurtleUpgrade>) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller);
|
||||
return modeller.getModel(upgrade, access, side);
|
||||
}
|
||||
|
||||
private static TurtleUpgradeModeller<?> getModeller(ITurtleUpgrade upgradeA) {
|
||||
var wrapper = TurtleUpgrades.instance().getWrapper(upgradeA);
|
||||
if (wrapper == null) return NULL_TURTLE_MODELLER;
|
||||
|
||||
var modeller = turtleModels.get(wrapper.serialiser());
|
||||
return modeller == null ? NULL_TURTLE_MODELLER : modeller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Equivalent to {@link Override}, but for Forge-specific methods.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ForgeOverride {
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock;
|
||||
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.data.models.BlockModelGenerators;
|
||||
import net.minecraft.data.models.blockstates.*;
|
||||
import net.minecraft.data.models.model.*;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocation;
|
||||
import static net.minecraft.data.models.model.TextureMapping.getBlockTexture;
|
||||
|
||||
class BlockModelProvider {
|
||||
private static final ModelTemplate MONITOR_BASE = new ModelTemplate(
|
||||
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/monitor_base")),
|
||||
Optional.empty(),
|
||||
TextureSlot.FRONT, TextureSlot.SIDE, TextureSlot.TOP, TextureSlot.BACK
|
||||
);
|
||||
private static final ModelTemplate MODEM = new ModelTemplate(
|
||||
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/modem")),
|
||||
Optional.empty(),
|
||||
TextureSlot.FRONT, TextureSlot.BACK
|
||||
);
|
||||
private static final ModelTemplate TURTLE = new ModelTemplate(
|
||||
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_base")),
|
||||
Optional.empty(),
|
||||
TextureSlot.TEXTURE
|
||||
);
|
||||
private static final ModelTemplate TURTLE_UPGRADE_LEFT = new ModelTemplate(
|
||||
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_upgrade_base_left")),
|
||||
Optional.of("_left"),
|
||||
TextureSlot.TEXTURE
|
||||
);
|
||||
private static final ModelTemplate TURTLE_UPGRADE_RIGHT = new ModelTemplate(
|
||||
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_upgrade_base_right")),
|
||||
Optional.of("_left"),
|
||||
TextureSlot.TEXTURE
|
||||
);
|
||||
|
||||
public static void addBlockModels(BlockModelGenerators generators) {
|
||||
registerComputer(generators, ModRegistry.Blocks.COMPUTER_NORMAL.get());
|
||||
registerComputer(generators, ModRegistry.Blocks.COMPUTER_ADVANCED.get());
|
||||
registerComputer(generators, ModRegistry.Blocks.COMPUTER_COMMAND.get());
|
||||
|
||||
registerTurtle(generators, ModRegistry.Blocks.TURTLE_NORMAL.get());
|
||||
registerTurtle(generators, ModRegistry.Blocks.TURTLE_ADVANCED.get());
|
||||
|
||||
registerWirelessModem(generators, ModRegistry.Blocks.WIRELESS_MODEM_NORMAL.get());
|
||||
registerWirelessModem(generators, ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get());
|
||||
|
||||
registerWiredModems(generators);
|
||||
|
||||
registerMonitor(generators, ModRegistry.Blocks.MONITOR_NORMAL.get());
|
||||
registerMonitor(generators, ModRegistry.Blocks.MONITOR_ADVANCED.get());
|
||||
|
||||
generators.createHorizontallyRotatedBlock(ModRegistry.Blocks.SPEAKER.get(), TexturedModel.ORIENTABLE_ONLY_TOP);
|
||||
registerDiskDrive(generators);
|
||||
registerPrinter(generators);
|
||||
|
||||
registerCable(generators);
|
||||
|
||||
registerTurtleUpgrade(generators, "block/turtle_crafting_table", "block/turtle_crafty_face");
|
||||
registerTurtleUpgrade(generators, "block/turtle_speaker", "block/turtle_speaker_face");
|
||||
registerTurtleModem(generators, "block/turtle_modem_normal", "block/wireless_modem_normal_face");
|
||||
registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
|
||||
}
|
||||
|
||||
private static void registerDiskDrive(BlockModelGenerators generators) {
|
||||
var diskDrive = ModRegistry.Blocks.DISK_DRIVE.get();
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(diskDrive)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
.with(createModelDispatch(DiskDriveBlock.STATE, value -> {
|
||||
var textureSuffix = switch (value) {
|
||||
case EMPTY -> "_front";
|
||||
case INVALID -> "_front_rejected";
|
||||
case FULL -> "_front_accepted";
|
||||
};
|
||||
return ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
|
||||
diskDrive, "_" + value.getSerializedName(),
|
||||
TextureMapping.orientableCube(diskDrive).put(TextureSlot.FRONT, getBlockTexture(diskDrive, textureSuffix)),
|
||||
generators.modelOutput
|
||||
);
|
||||
}))
|
||||
);
|
||||
generators.delegateItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
|
||||
}
|
||||
|
||||
private static void registerPrinter(BlockModelGenerators generators) {
|
||||
var printer = ModRegistry.Blocks.PRINTER.get();
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(printer)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
.with(createModelDispatch(PrinterBlock.TOP, PrinterBlock.BOTTOM, (top, bottom) -> {
|
||||
String model, texture;
|
||||
if (top && bottom) {
|
||||
model = "_both_full";
|
||||
texture = "_both_trays";
|
||||
} else if (top) {
|
||||
model = "_top_full";
|
||||
texture = "_top_tray";
|
||||
} else if (bottom) {
|
||||
model = "_bottom_full";
|
||||
texture = "_bottom_tray";
|
||||
} else {
|
||||
texture = model = "_empty";
|
||||
}
|
||||
|
||||
return ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(printer, model,
|
||||
TextureMapping.orientableCube(printer).put(TextureSlot.FRONT, getBlockTexture(printer, "_front" + texture)),
|
||||
generators.modelOutput
|
||||
);
|
||||
}))
|
||||
);
|
||||
generators.delegateItemModel(printer, getModelLocation(printer, "_empty"));
|
||||
}
|
||||
|
||||
private static void registerComputer(BlockModelGenerators generators, ComputerBlock<?> block) {
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
.with(createModelDispatch(ComputerBlock.STATE, state -> ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
|
||||
block, "_" + state.getSerializedName(),
|
||||
TextureMapping.orientableCube(block).put(TextureSlot.FRONT, getBlockTexture(block, "_front" + state.getTexture())),
|
||||
generators.modelOutput
|
||||
)))
|
||||
);
|
||||
generators.delegateItemModel(block, getModelLocation(block, "_blinking"));
|
||||
}
|
||||
|
||||
private static void registerTurtle(BlockModelGenerators generators, TurtleBlock block) {
|
||||
var model = TURTLE.create(block, TextureMapping.defaultTexture(block), generators.modelOutput);
|
||||
generators.blockStateOutput.accept(
|
||||
MultiVariantGenerator.multiVariant(block, Variant.variant().with(VariantProperties.MODEL, model))
|
||||
.with(createHorizontalFacingDispatch())
|
||||
);
|
||||
|
||||
generators.modelOutput.accept(getModelLocation(block.asItem()), () -> {
|
||||
var out = new JsonObject();
|
||||
out.addProperty("loader", "computercraft:turtle");
|
||||
out.addProperty("model", model.toString());
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
private static void registerWirelessModem(BlockModelGenerators generators, WirelessModemBlock block) {
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
|
||||
.with(createFacingDispatch())
|
||||
.with(createModelDispatch(WirelessModemBlock.ON,
|
||||
on -> modemModel(generators, getModelLocation(block, on ? "_on" : "_off"), getBlockTexture(block, "_face" + (on ? "_on" : "")))
|
||||
)));
|
||||
generators.delegateItemModel(block, getModelLocation(block, "_off"));
|
||||
}
|
||||
|
||||
private static void registerWiredModems(BlockModelGenerators generators) {
|
||||
var fullBlock = ModRegistry.Blocks.WIRED_MODEM_FULL.get();
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(fullBlock)
|
||||
.with(createModelDispatch(WiredModemFullBlock.MODEM_ON, WiredModemFullBlock.PERIPHERAL_ON, (on, peripheral) -> {
|
||||
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
|
||||
var faceTexture = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/wired_modem_face" + (peripheral ? "_peripheral" : "") + (on ? "_on" : ""));
|
||||
|
||||
// TODO: Do this somewhere more elegant!
|
||||
modemModel(generators, new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix), faceTexture);
|
||||
|
||||
return ModelTemplates.CUBE_ALL.create(
|
||||
getModelLocation(fullBlock, suffix),
|
||||
new TextureMapping().put(TextureSlot.ALL, faceTexture),
|
||||
generators.modelOutput
|
||||
);
|
||||
})));
|
||||
|
||||
generators.delegateItemModel(fullBlock, getModelLocation(fullBlock, "_off"));
|
||||
generators.delegateItemModel(ModRegistry.Items.WIRED_MODEM.get(), new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/wired_modem_off"));
|
||||
}
|
||||
|
||||
private static ResourceLocation modemModel(BlockModelGenerators generators, ResourceLocation name, ResourceLocation texture) {
|
||||
return MODEM.create(
|
||||
name,
|
||||
new TextureMapping()
|
||||
.put(TextureSlot.FRONT, texture)
|
||||
.put(TextureSlot.BACK, new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/modem_back")),
|
||||
generators.modelOutput
|
||||
);
|
||||
}
|
||||
|
||||
private static void registerMonitor(BlockModelGenerators generators, MonitorBlock block) {
|
||||
monitorModel(generators, block, "", 16, 4, 0, 32);
|
||||
monitorModel(generators, block, "_d", 20, 7, 0, 36);
|
||||
monitorModel(generators, block, "_l", 19, 4, 1, 33);
|
||||
monitorModel(generators, block, "_ld", 31, 7, 1, 45);
|
||||
monitorModel(generators, block, "_lr", 18, 4, 2, 34);
|
||||
monitorModel(generators, block, "_lrd", 30, 7, 2, 46);
|
||||
monitorModel(generators, block, "_lru", 24, 5, 2, 40);
|
||||
monitorModel(generators, block, "_lrud", 27, 6, 2, 43);
|
||||
monitorModel(generators, block, "_lu", 25, 5, 1, 39);
|
||||
monitorModel(generators, block, "_lud", 28, 6, 1, 42);
|
||||
monitorModel(generators, block, "_r", 17, 4, 3, 35);
|
||||
monitorModel(generators, block, "_rd", 29, 7, 3, 47);
|
||||
monitorModel(generators, block, "_ru", 23, 5, 3, 41);
|
||||
monitorModel(generators, block, "_rud", 26, 6, 3, 44);
|
||||
monitorModel(generators, block, "_u", 22, 5, 0, 38);
|
||||
monitorModel(generators, block, "_ud", 21, 6, 0, 37);
|
||||
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
.with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
|
||||
.with(createModelDispatch(MonitorBlock.STATE, edge -> getModelLocation(block, edge == MonitorEdgeState.NONE ? "" : "_" + edge.getSerializedName())))
|
||||
);
|
||||
generators.delegateItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
|
||||
}
|
||||
|
||||
private static ResourceLocation monitorModel(BlockModelGenerators generators, MonitorBlock block, String corners, int front, int side, int top, int back) {
|
||||
return MONITOR_BASE.create(
|
||||
getModelLocation(block, corners),
|
||||
new TextureMapping()
|
||||
.put(TextureSlot.FRONT, getBlockTexture(block, "_" + front))
|
||||
.put(TextureSlot.SIDE, getBlockTexture(block, "_" + side))
|
||||
.put(TextureSlot.TOP, getBlockTexture(block, "_" + top))
|
||||
.put(TextureSlot.BACK, getBlockTexture(block, "_" + back)),
|
||||
generators.modelOutput
|
||||
);
|
||||
}
|
||||
|
||||
private static void registerCable(BlockModelGenerators generators) {
|
||||
var generator = MultiPartGenerator.multiPart(ModRegistry.Blocks.CABLE.get());
|
||||
|
||||
// When a cable only has a neighbour in a single direction, we redirect the core to face that direction.
|
||||
var coreFacing = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/cable_core_facing");
|
||||
// Up/Down
|
||||
generator.with(
|
||||
Condition.or(
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.UP, true),
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.DOWN, true)
|
||||
),
|
||||
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
|
||||
);
|
||||
|
||||
// North/South and no neighbours
|
||||
generator.with(
|
||||
Condition.or(
|
||||
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST),
|
||||
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.NORTH, true),
|
||||
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.SOUTH, true)
|
||||
),
|
||||
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R0)
|
||||
);
|
||||
|
||||
// East/West
|
||||
generator.with(
|
||||
Condition.or(
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.EAST, true),
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.WEST, true)
|
||||
),
|
||||
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R90)
|
||||
);
|
||||
|
||||
// Find all other possibilities and emit a "solid" core which doesn't have a facing direction.
|
||||
var core = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/cable_core_any");
|
||||
List<Condition.TerminalCondition> rightAngles = new ArrayList<>();
|
||||
for (var i = 0; i < DirectionUtil.FACINGS.length; i++) {
|
||||
for (var j = i; j < DirectionUtil.FACINGS.length; j++) {
|
||||
if (DirectionUtil.FACINGS[i].getAxis() == DirectionUtil.FACINGS[j].getAxis()) continue;
|
||||
|
||||
rightAngles.add(new Condition.TerminalCondition()
|
||||
.term(CableBlock.CABLE, true).term(CABLE_DIRECTIONS[i], true).term(CABLE_DIRECTIONS[j], true)
|
||||
);
|
||||
}
|
||||
}
|
||||
generator.with(Condition.or(rightAngles.toArray(new Condition[0])), Variant.variant().with(VariantProperties.MODEL, core));
|
||||
|
||||
// Then emit the actual cable arms
|
||||
var arm = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/cable_arm");
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
generator.with(
|
||||
new Condition.TerminalCondition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
|
||||
Variant.variant()
|
||||
.with(VariantProperties.MODEL, arm)
|
||||
.with(VariantProperties.X_ROT, toXAngle(direction.getOpposite()))
|
||||
.with(VariantProperties.Y_ROT, toYAngle(direction.getOpposite()))
|
||||
);
|
||||
}
|
||||
|
||||
// And the modems!
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
for (var on : BOOLEANS) {
|
||||
for (var peripheral : BOOLEANS) {
|
||||
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
|
||||
generator.with(
|
||||
new Condition.TerminalCondition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
|
||||
Variant.variant()
|
||||
.with(VariantProperties.MODEL, new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
|
||||
.with(VariantProperties.X_ROT, toXAngle(direction))
|
||||
.with(VariantProperties.Y_ROT, toYAngle(direction))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generators.blockStateOutput.accept(generator);
|
||||
}
|
||||
|
||||
private static final BooleanProperty[] CABLE_DIRECTIONS = { CableBlock.DOWN, CableBlock.UP, CableBlock.NORTH, CableBlock.SOUTH, CableBlock.WEST, CableBlock.EAST };
|
||||
private static final boolean[] BOOLEANS = new boolean[]{ false, true };
|
||||
|
||||
private static Condition.TerminalCondition cableNoNeighbour(Direction... directions) {
|
||||
var condition = new Condition.TerminalCondition().term(CableBlock.CABLE, true);
|
||||
for (var direction : directions) condition.term(CABLE_DIRECTIONS[direction.ordinal()], false);
|
||||
return condition;
|
||||
}
|
||||
|
||||
private static void registerTurtleUpgrade(BlockModelGenerators generators, String name, String texture) {
|
||||
TURTLE_UPGRADE_LEFT.create(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, name + "_left"),
|
||||
TextureMapping.defaultTexture(new ResourceLocation(ComputerCraftAPI.MOD_ID, texture)),
|
||||
generators.modelOutput
|
||||
);
|
||||
TURTLE_UPGRADE_RIGHT.create(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, name + "_right"),
|
||||
TextureMapping.defaultTexture(new ResourceLocation(ComputerCraftAPI.MOD_ID, texture)),
|
||||
generators.modelOutput
|
||||
);
|
||||
}
|
||||
|
||||
private static void registerTurtleModem(BlockModelGenerators generators, String name, String texture) {
|
||||
registerTurtleUpgrade(generators, name + "_off", texture);
|
||||
registerTurtleUpgrade(generators, name + "_on", texture + "_on");
|
||||
}
|
||||
|
||||
private static VariantProperties.Rotation toXAngle(Direction direction) {
|
||||
return switch (direction) {
|
||||
default -> VariantProperties.Rotation.R0;
|
||||
case UP -> VariantProperties.Rotation.R270;
|
||||
case DOWN -> VariantProperties.Rotation.R90;
|
||||
};
|
||||
}
|
||||
|
||||
private static VariantProperties.Rotation toYAngle(Direction direction) {
|
||||
return switch (direction) {
|
||||
default -> VariantProperties.Rotation.R0;
|
||||
case NORTH -> VariantProperties.Rotation.R0;
|
||||
case SOUTH -> VariantProperties.Rotation.R180;
|
||||
case EAST -> VariantProperties.Rotation.R90;
|
||||
case WEST -> VariantProperties.Rotation.R270;
|
||||
};
|
||||
}
|
||||
|
||||
private static PropertyDispatch createHorizontalFacingDispatch() {
|
||||
var dispatch = PropertyDispatch.property(BlockStateProperties.HORIZONTAL_FACING);
|
||||
for (var direction : BlockStateProperties.HORIZONTAL_FACING.getPossibleValues()) {
|
||||
dispatch.select(direction, Variant.variant().with(VariantProperties.Y_ROT, toYAngle(direction)));
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
private static PropertyDispatch createVerticalFacingDispatch(Property<Direction> property) {
|
||||
var dispatch = PropertyDispatch.property(property);
|
||||
for (var direction : property.getPossibleValues()) {
|
||||
dispatch.select(direction, Variant.variant().with(VariantProperties.X_ROT, toXAngle(direction)));
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
private static PropertyDispatch createFacingDispatch() {
|
||||
var dispatch = PropertyDispatch.property(BlockStateProperties.FACING);
|
||||
for (var direction : BlockStateProperties.FACING.getPossibleValues()) {
|
||||
dispatch.select(direction, Variant.variant()
|
||||
.with(VariantProperties.Y_ROT, toYAngle(direction))
|
||||
.with(VariantProperties.X_ROT, toXAngle(direction))
|
||||
);
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> PropertyDispatch createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
|
||||
var variant = PropertyDispatch.property(property);
|
||||
for (var value : property.getPossibleValues()) {
|
||||
variant.select(value, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(value)));
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch createModelDispatch(
|
||||
Property<T> propertyT, Property<U> propertyU, BiFunction<T, U, ResourceLocation> makeModel
|
||||
) {
|
||||
var variant = PropertyDispatch.properties(propertyT, propertyU);
|
||||
for (var valueT : propertyT.getPossibleValues()) {
|
||||
for (var valueU : propertyU.getPossibleValues()) {
|
||||
variant.select(valueT, valueU, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(valueT, valueU)));
|
||||
}
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.data.DataGenerator;
|
||||
import net.minecraft.data.DataProvider;
|
||||
import net.minecraft.data.models.BlockModelGenerators;
|
||||
import net.minecraft.data.models.ItemModelGenerators;
|
||||
import net.minecraft.data.recipes.FinishedRecipe;
|
||||
import net.minecraft.data.tags.TagsProvider;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSet;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* All data providers for ComputerCraft. We require a mod-loader abstraction {@link DataProviders.GeneratorFactory} to
|
||||
* handle the slight differences between how Forge and Fabric expose Minecraft's data providers.
|
||||
*/
|
||||
public final class DataProviders {
|
||||
private DataProviders() {
|
||||
}
|
||||
|
||||
public static void add(DataGenerator generator, GeneratorFactory generators, boolean includeServer, boolean includeClient) {
|
||||
var turtleUpgrades = new TurtleUpgradeProvider(generator);
|
||||
var pocketUpgrades = new PocketUpgradeProvider(generator);
|
||||
|
||||
generator.addProvider(includeServer, turtleUpgrades);
|
||||
generator.addProvider(includeServer, pocketUpgrades);
|
||||
generator.addProvider(includeServer, generators.recipes(new RecipeProvider(turtleUpgrades, pocketUpgrades)::addRecipes));
|
||||
|
||||
var blockTags = generators.blockTags(TagProvider::blockTags);
|
||||
generator.addProvider(includeServer, blockTags);
|
||||
generator.addProvider(includeServer, generators.itemTags(TagProvider::itemTags, blockTags));
|
||||
|
||||
for (var provider : generators.lootTable(LootTableProvider.getTables())) {
|
||||
generator.addProvider(includeServer, provider);
|
||||
}
|
||||
|
||||
generator.addProvider(includeClient, generators.models(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels));
|
||||
}
|
||||
|
||||
interface GeneratorFactory {
|
||||
DataProvider recipes(Consumer<Consumer<FinishedRecipe>> recipes);
|
||||
|
||||
List<DataProvider> lootTable(List<Pair<Supplier<Consumer<BiConsumer<ResourceLocation, LootTable.Builder>>>, LootContextParamSet>> tables);
|
||||
|
||||
TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags);
|
||||
|
||||
TagsProvider<Item> itemTags(Consumer<TagProvider.ItemTagConsumer> tags, TagsProvider<Block> blocks);
|
||||
|
||||
DataProvider models(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.data.models.ItemModelGenerators;
|
||||
import net.minecraft.data.models.model.ModelTemplate;
|
||||
import net.minecraft.data.models.model.ModelTemplates;
|
||||
import net.minecraft.data.models.model.TextureMapping;
|
||||
import net.minecraft.data.models.model.TextureSlot;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocation;
|
||||
|
||||
public final class ItemModelProvider {
|
||||
private ItemModelProvider() {
|
||||
}
|
||||
|
||||
public static void addItemModels(ItemModelGenerators generators) {
|
||||
registerDisk(generators, ModRegistry.Items.DISK.get());
|
||||
registerDisk(generators, ModRegistry.Items.TREASURE_DISK.get());
|
||||
|
||||
registerPocketComputer(generators, getModelLocation(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), false);
|
||||
registerPocketComputer(generators, getModelLocation(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get()), false);
|
||||
registerPocketComputer(generators, new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/pocket_computer_colour"), true);
|
||||
|
||||
generators.generateFlatItem(ModRegistry.Items.PRINTED_BOOK.get(), ModelTemplates.FLAT_ITEM);
|
||||
generators.generateFlatItem(ModRegistry.Items.PRINTED_PAGE.get(), ModelTemplates.FLAT_ITEM);
|
||||
generators.generateFlatItem(ModRegistry.Items.PRINTED_PAGES.get(), ModelTemplates.FLAT_ITEM);
|
||||
}
|
||||
|
||||
private static void registerPocketComputer(ItemModelGenerators generators, ResourceLocation id, boolean off) {
|
||||
createFlatItem(generators, addSuffix(id, "_blinking"),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/pocket_computer_blink"),
|
||||
id,
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/pocket_computer_light")
|
||||
);
|
||||
|
||||
createFlatItem(generators, addSuffix(id, "_on"),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/pocket_computer_on"),
|
||||
id,
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/pocket_computer_light")
|
||||
);
|
||||
|
||||
// Don't emit the default/off state for advanced/normal pocket computers, as they have item overrides.
|
||||
if (off) {
|
||||
createFlatItem(generators, id,
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/pocket_computer_frame"),
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerDisk(ItemModelGenerators generators, Item item) {
|
||||
createFlatItem(generators, item,
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/disk_frame"),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "item/disk_colour")
|
||||
);
|
||||
}
|
||||
|
||||
private static void createFlatItem(ItemModelGenerators generators, Item item, ResourceLocation... ids) {
|
||||
createFlatItem(generators, getModelLocation(item), ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a flat item from an arbitrary number of layers.
|
||||
*
|
||||
* @param generators The current item generator helper.
|
||||
* @param model The model we're writing to.
|
||||
* @param textures The textures which make up this model.
|
||||
* @see net.minecraft.client.renderer.block.model.ItemModelGenerator The parser for this file format.
|
||||
*/
|
||||
private static void createFlatItem(ItemModelGenerators generators, ResourceLocation model, ResourceLocation... textures) {
|
||||
if (textures.length > 5) throw new IndexOutOfBoundsException("Too many layers");
|
||||
if (textures.length == 0) throw new IndexOutOfBoundsException("Must have at least one texture");
|
||||
if (textures.length == 1) {
|
||||
ModelTemplates.FLAT_ITEM.create(model, TextureMapping.layer0(textures[0]), generators.output);
|
||||
return;
|
||||
}
|
||||
|
||||
var slots = new TextureSlot[textures.length];
|
||||
var mapping = new TextureMapping();
|
||||
for (var i = 0; i < textures.length; i++) {
|
||||
var slot = slots[i] = TextureSlot.create("layer" + i);
|
||||
mapping.put(slot, textures[i]);
|
||||
}
|
||||
|
||||
new ModelTemplate(Optional.of(new ResourceLocation("item/generated")), Optional.empty(), slots)
|
||||
.create(model, mapping, generators.output);
|
||||
}
|
||||
|
||||
private static ResourceLocation addSuffix(ResourceLocation location, String suffix) {
|
||||
return new ResourceLocation(location.getNamespace(), location.getPath() + suffix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.CommonHooks;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
|
||||
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
|
||||
import dan200.computercraft.shared.data.PlayerCreativeLootCondition;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
|
||||
import net.minecraft.advancements.critereon.StatePropertiesPredicate;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.storage.loot.LootPool;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.entries.DynamicLoot;
|
||||
import net.minecraft.world.level.storage.loot.entries.LootItem;
|
||||
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
|
||||
import net.minecraft.world.level.storage.loot.functions.CopyNameFunction;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSet;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.level.storage.loot.predicates.AlternativeLootItemCondition;
|
||||
import net.minecraft.world.level.storage.loot.predicates.ExplosionCondition;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemBlockStatePropertyCondition;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
|
||||
import net.minecraft.world.level.storage.loot.providers.number.ConstantValue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
class LootTableProvider {
|
||||
public static List<Pair<Supplier<Consumer<BiConsumer<ResourceLocation, LootTable.Builder>>>, LootContextParamSet>> getTables() {
|
||||
return List.of(
|
||||
Pair.of(() -> LootTableProvider::registerBlocks, LootContextParamSets.BLOCK),
|
||||
Pair.of(() -> LootTableProvider::registerGeneric, LootContextParamSets.ALL_PARAMS)
|
||||
);
|
||||
}
|
||||
|
||||
private static void registerBlocks(BiConsumer<ResourceLocation, LootTable.Builder> add) {
|
||||
namedBlockDrop(add, ModRegistry.Blocks.DISK_DRIVE);
|
||||
selfDrop(add, ModRegistry.Blocks.MONITOR_NORMAL);
|
||||
selfDrop(add, ModRegistry.Blocks.MONITOR_ADVANCED);
|
||||
namedBlockDrop(add, ModRegistry.Blocks.PRINTER);
|
||||
selfDrop(add, ModRegistry.Blocks.SPEAKER);
|
||||
selfDrop(add, ModRegistry.Blocks.WIRED_MODEM_FULL);
|
||||
selfDrop(add, ModRegistry.Blocks.WIRELESS_MODEM_NORMAL);
|
||||
selfDrop(add, ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED);
|
||||
|
||||
computerDrop(add, ModRegistry.Blocks.COMPUTER_NORMAL);
|
||||
computerDrop(add, ModRegistry.Blocks.COMPUTER_ADVANCED);
|
||||
computerDrop(add, ModRegistry.Blocks.COMPUTER_COMMAND);
|
||||
computerDrop(add, ModRegistry.Blocks.TURTLE_NORMAL);
|
||||
computerDrop(add, ModRegistry.Blocks.TURTLE_ADVANCED);
|
||||
|
||||
add.accept(ModRegistry.Blocks.CABLE.get().getLootTable(), LootTable
|
||||
.lootTable()
|
||||
.withPool(LootPool.lootPool()
|
||||
.setRolls(ConstantValue.exactly(1))
|
||||
.add(LootItem.lootTableItem(ModRegistry.Items.CABLE.get()))
|
||||
.when(ExplosionCondition.survivesExplosion())
|
||||
.when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModRegistry.Blocks.CABLE.get())
|
||||
.setProperties(StatePropertiesPredicate.Builder.properties().hasProperty(CableBlock.CABLE, true))
|
||||
)
|
||||
)
|
||||
.withPool(LootPool.lootPool()
|
||||
.setRolls(ConstantValue.exactly(1))
|
||||
.add(LootItem.lootTableItem(ModRegistry.Items.WIRED_MODEM.get()))
|
||||
.when(ExplosionCondition.survivesExplosion())
|
||||
.when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModRegistry.Blocks.CABLE.get())
|
||||
.setProperties(StatePropertiesPredicate.Builder.properties().hasProperty(CableBlock.MODEM, CableModemVariant.None))
|
||||
.invert()
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
private static void registerGeneric(BiConsumer<ResourceLocation, LootTable.Builder> add) {
|
||||
add.accept(CommonHooks.LOOT_TREASURE_DISK, LootTable.lootTable());
|
||||
}
|
||||
|
||||
private static void selfDrop(BiConsumer<ResourceLocation, LootTable.Builder> add, Supplier<? extends Block> wrapper) {
|
||||
blockDrop(add, wrapper, LootItem.lootTableItem(wrapper.get()), ExplosionCondition.survivesExplosion());
|
||||
}
|
||||
|
||||
private static void namedBlockDrop(BiConsumer<ResourceLocation, LootTable.Builder> add, Supplier<? extends Block> wrapper) {
|
||||
blockDrop(
|
||||
add, wrapper,
|
||||
LootItem.lootTableItem(wrapper.get()).apply(CopyNameFunction.copyName(CopyNameFunction.NameSource.BLOCK_ENTITY)),
|
||||
ExplosionCondition.survivesExplosion()
|
||||
);
|
||||
}
|
||||
|
||||
private static void computerDrop(BiConsumer<ResourceLocation, LootTable.Builder> add, Supplier<? extends Block> block) {
|
||||
blockDrop(
|
||||
add, block,
|
||||
DynamicLoot.dynamicEntry(new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer")),
|
||||
AlternativeLootItemCondition.alternative(
|
||||
BlockNamedEntityLootCondition.BUILDER,
|
||||
HasComputerIdLootCondition.BUILDER,
|
||||
PlayerCreativeLootCondition.BUILDER.invert()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static void blockDrop(
|
||||
BiConsumer<ResourceLocation, LootTable.Builder> add, Supplier<? extends Block> wrapper,
|
||||
LootPoolEntryContainer.Builder<?> drop,
|
||||
LootItemCondition.Builder condition
|
||||
) {
|
||||
var block = wrapper.get();
|
||||
add.accept(block.getLootTable(), LootTable
|
||||
.lootTable()
|
||||
.withPool(LootPool.lootPool()
|
||||
.setRolls(ConstantValue.exactly(1))
|
||||
.add(drop)
|
||||
.when(condition)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import dan200.computercraft.shared.platform.Registries;
|
||||
import net.minecraft.data.CachedOutput;
|
||||
import net.minecraft.data.DataGenerator;
|
||||
import net.minecraft.data.DataProvider;
|
||||
import net.minecraft.data.models.BlockModelGenerators;
|
||||
import net.minecraft.data.models.ItemModelGenerators;
|
||||
import net.minecraft.data.models.blockstates.BlockStateGenerator;
|
||||
import net.minecraft.data.models.model.DelegatedModel;
|
||||
import net.minecraft.data.models.model.ModelLocationUtils;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A copy of {@link net.minecraft.data.models.ModelProvider} which accepts a custom generator.
|
||||
* <p>
|
||||
* Please don't sue me Mojang. Or at least make these changes to vanilla before doing so!
|
||||
*/
|
||||
public class ModelProvider implements DataProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ModelProvider.class);
|
||||
|
||||
private final DataGenerator.PathProvider blockStatePath;
|
||||
private final DataGenerator.PathProvider modelPath;
|
||||
|
||||
private final Consumer<BlockModelGenerators> blocks;
|
||||
private final Consumer<ItemModelGenerators> items;
|
||||
|
||||
public ModelProvider(DataGenerator generator, Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) {
|
||||
blockStatePath = generator.createPathProvider(DataGenerator.Target.RESOURCE_PACK, "blockstates");
|
||||
modelPath = generator.createPathProvider(DataGenerator.Target.RESOURCE_PACK, "models");
|
||||
|
||||
this.blocks = blocks;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(CachedOutput output) {
|
||||
Map<Block, BlockStateGenerator> blockStates = new HashMap<>();
|
||||
Consumer<BlockStateGenerator> addBlockState = generator -> {
|
||||
var block = generator.getBlock();
|
||||
if (blockStates.containsKey(block)) {
|
||||
throw new IllegalStateException("Duplicate blockstate definition for " + block);
|
||||
}
|
||||
blockStates.put(block, generator);
|
||||
};
|
||||
|
||||
Map<ResourceLocation, Supplier<JsonElement>> models = new HashMap<>();
|
||||
BiConsumer<ResourceLocation, Supplier<JsonElement>> addModel = (id, contents) -> {
|
||||
if (models.containsKey(id)) throw new IllegalStateException("Duplicate model definition for " + id);
|
||||
models.put(id, contents);
|
||||
};
|
||||
Set<Item> explicitItems = new HashSet<>();
|
||||
blocks.accept(new BlockModelGenerators(addBlockState, addModel, explicitItems::add));
|
||||
items.accept(new ItemModelGenerators(addModel));
|
||||
|
||||
for (var block : Registries.BLOCKS) {
|
||||
if (!blockStates.containsKey(block)) continue;
|
||||
|
||||
var item = Item.BY_BLOCK.get(block);
|
||||
if (item == null || explicitItems.contains(item)) continue;
|
||||
|
||||
var model = ModelLocationUtils.getModelLocation(item);
|
||||
if (!models.containsKey(model)) {
|
||||
models.put(model, new DelegatedModel(ModelLocationUtils.getModelLocation(block)));
|
||||
}
|
||||
}
|
||||
|
||||
saveCollection(output, blockStates, x -> blockStatePath.json(Registries.BLOCKS.getKey(x)));
|
||||
saveCollection(output, models, modelPath::json);
|
||||
}
|
||||
|
||||
private <T> void saveCollection(CachedOutput output, Map<T, ? extends Supplier<JsonElement>> items, Function<T, Path> getLocation) {
|
||||
for (Map.Entry<T, ? extends Supplier<JsonElement>> entry : items.entrySet()) {
|
||||
var path = getLocation.apply(entry.getKey());
|
||||
try {
|
||||
DataProvider.saveStable(output, entry.getValue().get(), path);
|
||||
} catch (Exception exception) {
|
||||
LOG.error("Couldn't save {}", path, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Block State Definitions";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
|
||||
import net.minecraft.data.DataGenerator;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static dan200.computercraft.shared.ModRegistry.Items;
|
||||
import static dan200.computercraft.shared.ModRegistry.PocketUpgradeSerialisers;
|
||||
|
||||
class PocketUpgradeProvider extends PocketUpgradeDataProvider {
|
||||
PocketUpgradeProvider(DataGenerator generator) {
|
||||
super(generator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addUpgrades(Consumer<Upgrade<PocketUpgradeSerialiser<?>>> addUpgrade) {
|
||||
addUpgrade.accept(simpleWithCustomItem(id("speaker"), PocketUpgradeSerialisers.SPEAKER.get(), Items.SPEAKER.get()));
|
||||
simpleWithCustomItem(id("wireless_modem_normal"), PocketUpgradeSerialisers.WIRELESS_MODEM_NORMAL.get(), Items.WIRELESS_MODEM_NORMAL.get()).add(addUpgrade);
|
||||
simpleWithCustomItem(id("wireless_modem_advanced"), PocketUpgradeSerialisers.WIRELESS_MODEM_ADVANCED.get(), Items.WIRELESS_MODEM_ADVANCED.get()).add(addUpgrade);
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String id) {
|
||||
return new ResourceLocation(ComputerCraftAPI.MOD_ID, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import net.minecraft.data.DataProvider;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Alternative version of {@link JsonWriter} which attempts to lay out the JSON in a more compact format.
|
||||
* <p>
|
||||
* Yes, this is at least a little deranged.
|
||||
*/
|
||||
public class PrettyJsonWriter extends JsonWriter {
|
||||
public static final boolean ENABLED = System.getProperty("cct.pretty-json") != null;
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
private static final int MAX_WIDTH = 120;
|
||||
|
||||
private final Writer out;
|
||||
|
||||
/**
|
||||
* A stack of objects. This is either a {@link String} (in which case we've received an object key but no value)
|
||||
* or a {@link DocList} (which either represents an array or object).
|
||||
*/
|
||||
private final Deque<Object> stack = new ArrayDeque<>();
|
||||
|
||||
public PrettyJsonWriter(Writer out) {
|
||||
super(out);
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSON writer. This will either be a pretty or normal version, depending on whether the global flag is
|
||||
* set.
|
||||
*
|
||||
* @param out The writer to emit to.
|
||||
* @return The constructed JSON writer.
|
||||
*/
|
||||
public static JsonWriter createWriter(Writer out) {
|
||||
return ENABLED ? new PrettyJsonWriter(out) : new JsonWriter(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reformat a JSON string with our pretty printer.
|
||||
*
|
||||
* @param contents The string to reformat.
|
||||
* @return The reformatted string.
|
||||
*/
|
||||
public static byte[] reformat(byte[] contents) {
|
||||
if (!ENABLED) return contents;
|
||||
|
||||
JsonElement object;
|
||||
try (var reader = new InputStreamReader(new ByteArrayInputStream(contents), StandardCharsets.UTF_8)) {
|
||||
object = GSON.fromJson(reader, JsonElement.class);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (JsonSyntaxException e) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
var out = new ByteArrayOutputStream();
|
||||
try (var writer = new PrettyJsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||
GsonHelper.writeValue(writer, object, DataProvider.KEY_COMPARATOR);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
out.write('\n');
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private void pushValue(Object object) throws IOException {
|
||||
// We've popped our top object, just write a value.
|
||||
if (stack.isEmpty()) {
|
||||
write(out, object, MAX_WIDTH, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise we either need to push to our list or finish a record pair.
|
||||
var head = stack.getLast();
|
||||
if (head instanceof DocList) {
|
||||
((DocList) head).add(object);
|
||||
} else {
|
||||
stack.removeLast();
|
||||
((DocList) stack.getLast()).add(new Pair((String) head, object));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter beginArray() {
|
||||
stack.add(new DocList("[", "]"));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter endArray() throws IOException {
|
||||
var list = (DocList) stack.removeLast();
|
||||
pushValue(list);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter beginObject() {
|
||||
stack.add(new DocList("{", "}"));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter endObject() throws IOException {
|
||||
return endArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter name(String name) throws IOException {
|
||||
stack.add(escapeString(name));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter jsonValue(String value) throws IOException {
|
||||
pushValue(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(@Nullable String value) throws IOException {
|
||||
return value == null ? nullValue() : jsonValue(escapeString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter nullValue() throws IOException {
|
||||
if (!getSerializeNulls() && stack.peekLast() instanceof String) {
|
||||
stack.removeLast();
|
||||
return this;
|
||||
}
|
||||
|
||||
return jsonValue("null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(boolean value) throws IOException {
|
||||
return jsonValue(Boolean.toString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(@Nullable Boolean value) throws IOException {
|
||||
return value == null ? nullValue() : jsonValue(Boolean.toString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(double value) throws IOException {
|
||||
return jsonValue(Double.toString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(long value) throws IOException {
|
||||
return jsonValue(Long.toString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(@Nullable Number value) throws IOException {
|
||||
return value == null ? nullValue() : jsonValue(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!stack.isEmpty()) throw new IllegalArgumentException("Object is remaining on the stack");
|
||||
out.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* A key/value pair inside a JSON object.
|
||||
*
|
||||
* @param key The escaped object key.
|
||||
* @param value The object value.
|
||||
*/
|
||||
private record Pair(String key, Object value) {
|
||||
int width() {
|
||||
return key.length() + 2 + PrettyJsonWriter.width(value);
|
||||
}
|
||||
|
||||
int write(Writer out, int space, int indent) throws IOException {
|
||||
out.write(key);
|
||||
out.write(": ");
|
||||
return PrettyJsonWriter.write(out, value, space - key.length() - 2, indent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of terms inside a JSON document. Either an array or a JSON object.
|
||||
*/
|
||||
private static class DocList {
|
||||
final String prefix;
|
||||
final String suffix;
|
||||
final List<Object> contents = new ArrayList<>();
|
||||
int width;
|
||||
|
||||
DocList(String prefix, String suffix) {
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
width = prefix.length() + suffix.length();
|
||||
}
|
||||
|
||||
void add(Object value) {
|
||||
contents.add(value);
|
||||
width += width(value) + (contents.isEmpty() ? 0 : 2);
|
||||
}
|
||||
|
||||
int write(Writer writer, int space, int indent) throws IOException {
|
||||
writer.append(prefix);
|
||||
if (width <= space) {
|
||||
// We've sufficient room on this line, so write everything on one line.
|
||||
|
||||
// Take into account the suffix length here, as we ignore it the case we wrap.
|
||||
space -= prefix.length() + suffix.length();
|
||||
|
||||
var comma = false;
|
||||
for (var value : contents) {
|
||||
if (comma) {
|
||||
writer.append(", ");
|
||||
space -= 2;
|
||||
}
|
||||
comma = true;
|
||||
|
||||
space = PrettyJsonWriter.write(writer, value, space, indent);
|
||||
}
|
||||
} else {
|
||||
// We've run out of room, so write each value on separate lines.
|
||||
var indentStr = " ".repeat(indent);
|
||||
writer.append("\n ").append(indentStr);
|
||||
|
||||
var comma = false;
|
||||
for (var value : contents) {
|
||||
if (comma) {
|
||||
writer.append(",\n ").append(indentStr);
|
||||
}
|
||||
comma = true;
|
||||
|
||||
PrettyJsonWriter.write(writer, value, MAX_WIDTH - indent - 2, indent + 2);
|
||||
}
|
||||
writer.append("\n").append(indentStr);
|
||||
}
|
||||
|
||||
writer.append(suffix);
|
||||
return space;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the width of an object.
|
||||
*
|
||||
* @param object The object to emit.
|
||||
* @return The computed width.
|
||||
*/
|
||||
private static int width(Object object) {
|
||||
if (object instanceof String string) return string.length();
|
||||
if (object instanceof DocList list) return list.width;
|
||||
if (object instanceof Pair pair) return pair.width();
|
||||
throw new IllegalArgumentException("Not a valid document");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the output stream.
|
||||
*
|
||||
* @param writer The writer to emit to.
|
||||
* @param object The object to write.
|
||||
* @param space The amount of space left on this line. Will be no larger than {@link #MAX_WIDTH}, but may be negative.
|
||||
* @param indent The current indent.
|
||||
* @return The new amount of space left on this line. This is undefined if the writer wraps.
|
||||
* @throws IOException If the underlying writer fails.
|
||||
*/
|
||||
private static int write(Writer writer, Object object, int space, int indent) throws IOException {
|
||||
if (object instanceof String str) {
|
||||
writer.write(str);
|
||||
return space - str.length();
|
||||
} else if (object instanceof DocList list) {
|
||||
return list.write(writer, space, indent);
|
||||
} else if (object instanceof Pair pair) {
|
||||
return pair.write(writer, space, indent);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Not a valid document");
|
||||
}
|
||||
}
|
||||
|
||||
private static String escapeString(String value) {
|
||||
var builder = new StringBuilder();
|
||||
builder.append('\"');
|
||||
|
||||
var length = value.length();
|
||||
for (var i = 0; i < length; i++) {
|
||||
var c = value.charAt(i);
|
||||
String replacement = null;
|
||||
if (c < STRING_REPLACE.length) {
|
||||
replacement = STRING_REPLACE[c];
|
||||
} else if (c == '\u2028') {
|
||||
replacement = "\\u2028";
|
||||
} else if (c == '\u2029') {
|
||||
replacement = "\\u2029";
|
||||
}
|
||||
|
||||
if (replacement == null) {
|
||||
builder.append(c);
|
||||
} else {
|
||||
builder.append(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
builder.append('\"');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static final String[] STRING_REPLACE = new String[128];
|
||||
|
||||
static {
|
||||
for (var i = 0; i <= 0x1f; i++) STRING_REPLACE[i] = String.format("\\u%04x", i);
|
||||
STRING_REPLACE['"'] = "\\\"";
|
||||
STRING_REPLACE['\\'] = "\\\\";
|
||||
STRING_REPLACE['\t'] = "\\t";
|
||||
STRING_REPLACE['\b'] = "\\b";
|
||||
STRING_REPLACE['\n'] = "\\n";
|
||||
STRING_REPLACE['\r'] = "\\r";
|
||||
STRING_REPLACE['\f'] = "\\f";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.platform.RecipeIngredients;
|
||||
import dan200.computercraft.shared.platform.Registries;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItemFactory;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItemFactory;
|
||||
import net.minecraft.advancements.critereon.InventoryChangeTrigger;
|
||||
import net.minecraft.advancements.critereon.ItemPredicate;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.data.recipes.FinishedRecipe;
|
||||
import net.minecraft.data.recipes.ShapedRecipeBuilder;
|
||||
import net.minecraft.data.recipes.ShapelessRecipeBuilder;
|
||||
import net.minecraft.data.recipes.SpecialRecipeBuilder;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.item.DyeItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.crafting.Ingredient;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.ShapedRecipe;
|
||||
import net.minecraft.world.item.crafting.SimpleRecipeSerializer;
|
||||
import net.minecraft.world.level.ItemLike;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
|
||||
import static dan200.computercraft.api.ComputerCraftTags.Items.WIRED_MODEM;
|
||||
|
||||
class RecipeProvider {
|
||||
private final RecipeIngredients ingredients = PlatformHelper.get().getRecipeIngredients();
|
||||
private final TurtleUpgradeDataProvider turtleUpgrades;
|
||||
private final PocketUpgradeDataProvider pocketUpgrades;
|
||||
|
||||
RecipeProvider(TurtleUpgradeDataProvider turtleUpgrades, PocketUpgradeDataProvider pocketUpgrades) {
|
||||
this.turtleUpgrades = turtleUpgrades;
|
||||
this.pocketUpgrades = pocketUpgrades;
|
||||
}
|
||||
|
||||
public void addRecipes(Consumer<FinishedRecipe> add) {
|
||||
basicRecipes(add);
|
||||
diskColours(add);
|
||||
pocketUpgrades(add);
|
||||
turtleUpgrades(add);
|
||||
|
||||
addSpecial(add, ModRegistry.RecipeSerializers.PRINTOUT.get());
|
||||
addSpecial(add, ModRegistry.RecipeSerializers.DISK.get());
|
||||
addSpecial(add, ModRegistry.RecipeSerializers.DYEABLE_ITEM.get());
|
||||
addSpecial(add, ModRegistry.RecipeSerializers.TURTLE_UPGRADE.get());
|
||||
addSpecial(add, ModRegistry.RecipeSerializers.POCKET_COMPUTER_UPGRADE.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a crafting recipe for a disk of every dye colour.
|
||||
*
|
||||
* @param add The callback to add recipes.
|
||||
*/
|
||||
private void diskColours(Consumer<FinishedRecipe> add) {
|
||||
for (var colour : Colour.VALUES) {
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(ModRegistry.Items.DISK.get())
|
||||
.requires(ingredients.redstone())
|
||||
.requires(Items.PAPER)
|
||||
.requires(DyeItem.byColor(ofColour(colour)))
|
||||
.group("computercraft:disk")
|
||||
.unlockedBy("has_drive", inventoryChange(ModRegistry.Blocks.DISK_DRIVE.get()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get(), add)
|
||||
.withResultTag(x -> x.putInt(IColouredItem.NBT_COLOUR, colour.getHex())),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "disk_" + (colour.ordinal() + 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a crafting recipe for each turtle upgrade.
|
||||
*
|
||||
* @param add The callback to add recipes.
|
||||
*/
|
||||
private void turtleUpgrades(Consumer<FinishedRecipe> add) {
|
||||
for (var family : ComputerFamily.values()) {
|
||||
var base = TurtleItemFactory.create(-1, null, -1, family, null, null, 0, null);
|
||||
if (base.isEmpty()) continue;
|
||||
|
||||
var nameId = family.name().toLowerCase(Locale.ROOT);
|
||||
|
||||
for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) {
|
||||
var result = TurtleItemFactory.create(-1, null, -1, family, null, upgrade, -1, null);
|
||||
ShapedRecipeBuilder
|
||||
.shaped(result.getItem())
|
||||
.group(String.format("%s:turtle_%s", ComputerCraftAPI.MOD_ID, nameId))
|
||||
.pattern("#T")
|
||||
.define('T', base.getItem())
|
||||
.define('#', upgrade.getCraftingItem().getItem())
|
||||
.unlockedBy("has_items",
|
||||
inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("turtle_%s/%s/%s",
|
||||
nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a crafting recipe for each pocket upgrade.
|
||||
*
|
||||
* @param add The callback to add recipes.
|
||||
*/
|
||||
private void pocketUpgrades(Consumer<FinishedRecipe> add) {
|
||||
for (var family : ComputerFamily.values()) {
|
||||
var base = PocketComputerItemFactory.create(-1, null, -1, family, null);
|
||||
if (base.isEmpty()) continue;
|
||||
|
||||
var nameId = family.name().toLowerCase(Locale.ROOT);
|
||||
|
||||
for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) {
|
||||
var result = PocketComputerItemFactory.create(-1, null, -1, family, upgrade);
|
||||
ShapedRecipeBuilder
|
||||
.shaped(result.getItem())
|
||||
.group(String.format("%s:pocket_%s", ComputerCraftAPI.MOD_ID, nameId))
|
||||
.pattern("#")
|
||||
.pattern("P")
|
||||
.define('P', base.getItem())
|
||||
.define('#', upgrade.getCraftingItem().getItem())
|
||||
.unlockedBy("has_items",
|
||||
inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("pocket_%s/%s/%s",
|
||||
nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void basicRecipes(Consumer<FinishedRecipe> add) {
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Items.CABLE.get(), 6)
|
||||
.pattern(" # ")
|
||||
.pattern("#R#")
|
||||
.pattern(" # ")
|
||||
.define('#', ingredients.stone())
|
||||
.define('R', ingredients.redstone())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.COMPUTER_NORMAL.get())
|
||||
.pattern("###")
|
||||
.pattern("#R#")
|
||||
.pattern("#G#")
|
||||
.define('#', ingredients.stone())
|
||||
.define('R', ingredients.redstone())
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_redstone", inventoryChange(itemPredicate(ingredients.redstone())))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.COMPUTER_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#R#")
|
||||
.pattern("#G#")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('R', ingredients.redstone())
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_components", inventoryChange(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#C#")
|
||||
.pattern("# #")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
|
||||
.save(
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade")
|
||||
);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.COMPUTER_COMMAND.get())
|
||||
.pattern("###")
|
||||
.pattern("#R#")
|
||||
.pattern("#G#")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('R', Blocks.COMMAND_BLOCK)
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_components", inventoryChange(Blocks.COMMAND_BLOCK))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.TURTLE_NORMAL.get())
|
||||
.pattern("###")
|
||||
.pattern("#C#")
|
||||
.pattern("#I#")
|
||||
.define('#', ingredients.ironIngot())
|
||||
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
|
||||
.define('I', ingredients.woodenChest())
|
||||
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
|
||||
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.NORMAL)));
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.TURTLE_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#C#")
|
||||
.pattern("#I#")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||
.define('I', ingredients.woodenChest())
|
||||
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
|
||||
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)));
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.TURTLE_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#C#")
|
||||
.pattern(" B ")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||
.define('B', ingredients.goldBlock())
|
||||
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
|
||||
.save(
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade")
|
||||
);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.DISK_DRIVE.get())
|
||||
.pattern("###")
|
||||
.pattern("#R#")
|
||||
.pattern("#R#")
|
||||
.define('#', ingredients.stone())
|
||||
.define('R', ingredients.redstone())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.MONITOR_NORMAL.get())
|
||||
.pattern("###")
|
||||
.pattern("#G#")
|
||||
.pattern("###")
|
||||
.define('#', ingredients.stone())
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.MONITOR_ADVANCED.get(), 4)
|
||||
.pattern("###")
|
||||
.pattern("#G#")
|
||||
.pattern("###")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
|
||||
.pattern("###")
|
||||
.pattern("#A#")
|
||||
.pattern("#G#")
|
||||
.define('#', ingredients.stone())
|
||||
.define('A', Items.GOLDEN_APPLE)
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#A#")
|
||||
.pattern("#G#")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('A', Items.GOLDEN_APPLE)
|
||||
.define('G', ingredients.glassPane())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#C#")
|
||||
.pattern("# #")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
|
||||
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
|
||||
.save(
|
||||
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade")
|
||||
);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.PRINTER.get())
|
||||
.pattern("###")
|
||||
.pattern("#R#")
|
||||
.pattern("#D#")
|
||||
.define('#', ingredients.stone())
|
||||
.define('R', ingredients.redstone())
|
||||
.define('D', ingredients.dye())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.SPEAKER.get())
|
||||
.pattern("###")
|
||||
.pattern("#N#")
|
||||
.pattern("#R#")
|
||||
.define('#', ingredients.stone())
|
||||
.define('N', Blocks.NOTE_BLOCK)
|
||||
.define('R', ingredients.redstone())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Items.WIRED_MODEM.get())
|
||||
.pattern("###")
|
||||
.pattern("#R#")
|
||||
.pattern("###")
|
||||
.define('#', ingredients.stone())
|
||||
.define('R', ingredients.redstone())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.unlockedBy("has_cable", inventoryChange(ModRegistry.Items.CABLE.get()))
|
||||
.save(add);
|
||||
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(ModRegistry.Blocks.WIRED_MODEM_FULL.get())
|
||||
.requires(ModRegistry.Items.WIRED_MODEM.get())
|
||||
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
|
||||
.save(add, new ResourceLocation(ComputerCraftAPI.MOD_ID, "wired_modem_full_from"));
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(ModRegistry.Items.WIRED_MODEM.get())
|
||||
.requires(ModRegistry.Blocks.WIRED_MODEM_FULL.get())
|
||||
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
|
||||
.save(add, new ResourceLocation(ComputerCraftAPI.MOD_ID, "wired_modem_full_to"));
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.WIRELESS_MODEM_NORMAL.get())
|
||||
.pattern("###")
|
||||
.pattern("#E#")
|
||||
.pattern("###")
|
||||
.define('#', ingredients.stone())
|
||||
.define('E', ingredients.enderPearl())
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.save(add);
|
||||
|
||||
ShapedRecipeBuilder
|
||||
.shaped(ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get())
|
||||
.pattern("###")
|
||||
.pattern("#E#")
|
||||
.pattern("###")
|
||||
.define('#', ingredients.goldIngot())
|
||||
.define('E', Items.ENDER_EYE)
|
||||
.unlockedBy("has_computer", inventoryChange(COMPUTER))
|
||||
.unlockedBy("has_wireless", inventoryChange(ModRegistry.Blocks.WIRELESS_MODEM_NORMAL.get()))
|
||||
.save(add);
|
||||
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(Items.PLAYER_HEAD)
|
||||
.requires(ingredients.head())
|
||||
.requires(ModRegistry.Items.MONITOR_NORMAL.get())
|
||||
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
|
||||
.withResultTag(playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c")),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_cloudy")
|
||||
);
|
||||
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(Items.PLAYER_HEAD)
|
||||
.requires(ingredients.head())
|
||||
.requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
|
||||
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
|
||||
.save(
|
||||
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
|
||||
.withResultTag(playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb")),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_dan200")
|
||||
);
|
||||
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(ModRegistry.Items.PRINTED_PAGES.get())
|
||||
.requires(ModRegistry.Items.PRINTED_PAGE.get(), 2)
|
||||
.requires(ingredients.string())
|
||||
.unlockedBy("has_printer", inventoryChange(ModRegistry.Blocks.PRINTER.get()))
|
||||
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get(), add));
|
||||
|
||||
ShapelessRecipeBuilder
|
||||
.shapeless(ModRegistry.Items.PRINTED_BOOK.get())
|
||||
.requires(ingredients.leather())
|
||||
.requires(ModRegistry.Items.PRINTED_PAGE.get(), 1)
|
||||
.requires(ingredients.string())
|
||||
.unlockedBy("has_printer", inventoryChange(ModRegistry.Blocks.PRINTER.get()))
|
||||
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get(), add));
|
||||
}
|
||||
|
||||
private static DyeColor ofColour(Colour colour) {
|
||||
return DyeColor.byId(15 - colour.ordinal());
|
||||
}
|
||||
|
||||
private static InventoryChangeTrigger.TriggerInstance inventoryChange(TagKey<Item> stack) {
|
||||
return InventoryChangeTrigger.TriggerInstance.hasItems(itemPredicate(stack));
|
||||
}
|
||||
|
||||
private static InventoryChangeTrigger.TriggerInstance inventoryChange(ItemLike... stack) {
|
||||
return InventoryChangeTrigger.TriggerInstance.hasItems(stack);
|
||||
}
|
||||
|
||||
private static InventoryChangeTrigger.TriggerInstance inventoryChange(ItemPredicate... items) {
|
||||
return InventoryChangeTrigger.TriggerInstance.hasItems(items);
|
||||
}
|
||||
|
||||
private static ItemPredicate itemPredicate(ItemLike item) {
|
||||
return ItemPredicate.Builder.item().of(item).build();
|
||||
}
|
||||
|
||||
private static ItemPredicate itemPredicate(TagKey<Item> item) {
|
||||
return ItemPredicate.Builder.item().of(item).build();
|
||||
}
|
||||
|
||||
private static ItemPredicate itemPredicate(Ingredient ingredient) {
|
||||
var json = ingredient.toJson();
|
||||
if (!(json instanceof JsonObject object)) throw new IllegalStateException("Unknown ingredient " + json);
|
||||
|
||||
if (object.has("item")) {
|
||||
return itemPredicate(ShapedRecipe.itemFromJson(object));
|
||||
} else if (object.has("tag")) {
|
||||
return itemPredicate(TagKey.create(Registry.ITEM_REGISTRY, new ResourceLocation(GsonHelper.getAsString(object, "tag"))));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown ingredient " + json);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompoundTag playerHead(String name, String uuid) {
|
||||
var owner = new CompoundTag();
|
||||
owner.putString("Name", name);
|
||||
owner.putString("Id", uuid);
|
||||
|
||||
var tag = new CompoundTag();
|
||||
tag.put("SkullOwner", owner);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static Consumer<JsonObject> family(ComputerFamily family) {
|
||||
return json -> json.addProperty("family", family.toString());
|
||||
}
|
||||
|
||||
private static void addSpecial(Consumer<FinishedRecipe> add, SimpleRecipeSerializer<?> special) {
|
||||
SpecialRecipeBuilder.special(special).save(add, Registries.RECIPE_SERIALIZERS.getKey(special).toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.minecraft.data.recipes.FinishedRecipe;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Adapter for recipes which overrides the serializer and adds custom item NBT.
|
||||
*/
|
||||
final class RecipeWrapper implements Consumer<FinishedRecipe> {
|
||||
private final Consumer<FinishedRecipe> add;
|
||||
private final RecipeSerializer<?> serializer;
|
||||
private final List<Consumer<JsonObject>> extend = new ArrayList<>(0);
|
||||
|
||||
RecipeWrapper(Consumer<FinishedRecipe> add, RecipeSerializer<?> serializer) {
|
||||
this.add = add;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
public static RecipeWrapper wrap(RecipeSerializer<?> serializer, Consumer<FinishedRecipe> original) {
|
||||
return new RecipeWrapper(original, serializer);
|
||||
}
|
||||
|
||||
public RecipeWrapper withExtraData(Consumer<JsonObject> extra) {
|
||||
extend.add(extra);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RecipeWrapper withResultTag(@Nullable CompoundTag resultTag) {
|
||||
if (resultTag == null) return this;
|
||||
|
||||
extend.add(json -> {
|
||||
var object = GsonHelper.getAsJsonObject(json, "result");
|
||||
object.addProperty("nbt", resultTag.toString());
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public RecipeWrapper withResultTag(Consumer<CompoundTag> resultTag) {
|
||||
var tag = new CompoundTag();
|
||||
resultTag.accept(tag);
|
||||
return withResultTag(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(FinishedRecipe finishedRecipe) {
|
||||
add.accept(new RecipeImpl(finishedRecipe, serializer, extend));
|
||||
}
|
||||
|
||||
private record RecipeImpl(
|
||||
FinishedRecipe recipe, RecipeSerializer<?> serializer, List<Consumer<JsonObject>> extend
|
||||
) implements FinishedRecipe {
|
||||
@Override
|
||||
public void serializeRecipeData(JsonObject jsonObject) {
|
||||
recipe.serializeRecipeData(jsonObject);
|
||||
for (var extender : extend) extender.accept(jsonObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getId() {
|
||||
return recipe.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getType() {
|
||||
return serializer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public JsonObject serializeAdvancement() {
|
||||
return recipe.serializeAdvancement();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ResourceLocation getAdvancementId() {
|
||||
return recipe.getAdvancementId();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import net.minecraft.data.tags.ItemTagsProvider;
|
||||
import net.minecraft.data.tags.TagsProvider;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/**
|
||||
* Generators for block and item tags.
|
||||
* <p>
|
||||
* We cannot trivially extend {@link TagsProvider}, as Forge requires an {@code ExistingFileHelper} as a constructor
|
||||
* argument. Instead, we write our tags to the wrapper interface {@link TagConsumer}.
|
||||
*/
|
||||
class TagProvider {
|
||||
public static void blockTags(TagConsumer<Block> tags) {
|
||||
tags.tag(ComputerCraftTags.Blocks.COMPUTER).add(
|
||||
ModRegistry.Blocks.COMPUTER_NORMAL.get(),
|
||||
ModRegistry.Blocks.COMPUTER_ADVANCED.get(),
|
||||
ModRegistry.Blocks.COMPUTER_COMMAND.get()
|
||||
);
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE).add(ModRegistry.Blocks.TURTLE_NORMAL.get(), ModRegistry.Blocks.TURTLE_ADVANCED.get());
|
||||
tags.tag(ComputerCraftTags.Blocks.WIRED_MODEM).add(ModRegistry.Blocks.CABLE.get(), ModRegistry.Blocks.WIRED_MODEM_FULL.get());
|
||||
tags.tag(ComputerCraftTags.Blocks.MONITOR).add(ModRegistry.Blocks.MONITOR_NORMAL.get(), ModRegistry.Blocks.MONITOR_ADVANCED.get());
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE).addTag(BlockTags.LEAVES).add(
|
||||
Blocks.BAMBOO, Blocks.BAMBOO_SAPLING // Bamboo isn't instabreak for some odd reason.
|
||||
);
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).addTag(BlockTags.MINEABLE_WITH_SHOVEL).add(
|
||||
Blocks.MELON,
|
||||
Blocks.PUMPKIN,
|
||||
Blocks.CARVED_PUMPKIN,
|
||||
Blocks.JACK_O_LANTERN
|
||||
);
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).addTag(BlockTags.CROPS).addTag(BlockTags.MINEABLE_WITH_HOE).add(
|
||||
Blocks.CACTUS,
|
||||
Blocks.MELON,
|
||||
Blocks.PUMPKIN,
|
||||
Blocks.CARVED_PUMPKIN,
|
||||
Blocks.JACK_O_LANTERN
|
||||
);
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).addTag(BlockTags.WOOL).add(Blocks.COBWEB);
|
||||
|
||||
// Make all blocks aside from command computer mineable.
|
||||
tags.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(
|
||||
ModRegistry.Blocks.COMPUTER_NORMAL.get(),
|
||||
ModRegistry.Blocks.COMPUTER_ADVANCED.get(),
|
||||
ModRegistry.Blocks.TURTLE_NORMAL.get(),
|
||||
ModRegistry.Blocks.TURTLE_ADVANCED.get(),
|
||||
ModRegistry.Blocks.SPEAKER.get(),
|
||||
ModRegistry.Blocks.DISK_DRIVE.get(),
|
||||
ModRegistry.Blocks.PRINTER.get(),
|
||||
ModRegistry.Blocks.MONITOR_NORMAL.get(),
|
||||
ModRegistry.Blocks.MONITOR_ADVANCED.get(),
|
||||
ModRegistry.Blocks.WIRELESS_MODEM_NORMAL.get(),
|
||||
ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get(),
|
||||
ModRegistry.Blocks.WIRED_MODEM_FULL.get(),
|
||||
ModRegistry.Blocks.CABLE.get()
|
||||
);
|
||||
}
|
||||
|
||||
public static void itemTags(ItemTagConsumer tags) {
|
||||
tags.copy(ComputerCraftTags.Blocks.COMPUTER, ComputerCraftTags.Items.COMPUTER);
|
||||
tags.copy(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE);
|
||||
tags.tag(ComputerCraftTags.Items.WIRED_MODEM).add(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get());
|
||||
tags.copy(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR);
|
||||
|
||||
tags.tag(ItemTags.PIGLIN_LOVED).add(
|
||||
ModRegistry.Items.COMPUTER_ADVANCED.get(), ModRegistry.Items.TURTLE_ADVANCED.get(),
|
||||
ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(),
|
||||
ModRegistry.Items.MONITOR_ADVANCED.get()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper over {@link TagsProvider}.
|
||||
*
|
||||
* @param <T> The type of object we're providing tags for.
|
||||
*/
|
||||
public interface TagConsumer<T> {
|
||||
TagsProvider.TagAppender<T> tag(TagKey<T> tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper over {@link ItemTagsProvider}.
|
||||
*/
|
||||
interface ItemTagConsumer extends TagConsumer<Item> {
|
||||
void copy(TagKey<Block> block, TagKey<Item> item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.ComputerCraftTags.Blocks;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
import net.minecraft.data.DataGenerator;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static dan200.computercraft.shared.ModRegistry.Items;
|
||||
import static dan200.computercraft.shared.ModRegistry.TurtleSerialisers;
|
||||
|
||||
class TurtleUpgradeProvider extends TurtleUpgradeDataProvider {
|
||||
TurtleUpgradeProvider(DataGenerator generator) {
|
||||
super(generator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addUpgrades(Consumer<Upgrade<TurtleUpgradeSerialiser<?>>> addUpgrade) {
|
||||
simpleWithCustomItem(id("speaker"), TurtleSerialisers.SPEAKER.get(), Items.SPEAKER.get()).add(addUpgrade);
|
||||
simpleWithCustomItem(vanilla("crafting_table"), TurtleSerialisers.WORKBENCH.get(), net.minecraft.world.item.Items.CRAFTING_TABLE).add(addUpgrade);
|
||||
simpleWithCustomItem(id("wireless_modem_normal"), TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), Items.WIRELESS_MODEM_NORMAL.get()).add(addUpgrade);
|
||||
simpleWithCustomItem(id("wireless_modem_advanced"), TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), Items.WIRELESS_MODEM_ADVANCED.get()).add(addUpgrade);
|
||||
|
||||
tool(vanilla("diamond_axe"), net.minecraft.world.item.Items.DIAMOND_AXE).damageMultiplier(6.0f).add(addUpgrade);
|
||||
tool(vanilla("diamond_pickaxe"), net.minecraft.world.item.Items.DIAMOND_PICKAXE).add(addUpgrade);
|
||||
tool(vanilla("diamond_hoe"), net.minecraft.world.item.Items.DIAMOND_HOE).breakable(Blocks.TURTLE_HOE_BREAKABLE).add(addUpgrade);
|
||||
tool(vanilla("diamond_shovel"), net.minecraft.world.item.Items.DIAMOND_SHOVEL).breakable(Blocks.TURTLE_SHOVEL_BREAKABLE).add(addUpgrade);
|
||||
tool(vanilla("diamond_sword"), net.minecraft.world.item.Items.DIAMOND_SWORD).breakable(Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).add(addUpgrade);
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String id) {
|
||||
return new ResourceLocation(ComputerCraftAPI.MOD_ID, id);
|
||||
}
|
||||
|
||||
private static ResourceLocation vanilla(String id) {
|
||||
// Naughty, please don't do this. Mostly here for some semblance of backwards compatibility.
|
||||
return new ResourceLocation("minecraft", id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.detail.BlockReference;
|
||||
import dan200.computercraft.api.detail.DetailRegistry;
|
||||
import dan200.computercraft.api.detail.IDetailProvider;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.api.media.IMediaProvider;
|
||||
import dan200.computercraft.api.network.IPacketNetwork;
|
||||
import dan200.computercraft.api.network.wired.IWiredElement;
|
||||
import dan200.computercraft.api.network.wired.IWiredNode;
|
||||
import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
|
||||
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
|
||||
import dan200.computercraft.core.apis.ApiFactories;
|
||||
import dan200.computercraft.core.asm.GenericMethod;
|
||||
import dan200.computercraft.core.filesystem.FileMount;
|
||||
import dan200.computercraft.impl.detail.DetailRegistryImpl;
|
||||
import dan200.computercraft.impl.network.wired.WiredNode;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.details.BlockDetails;
|
||||
import dan200.computercraft.shared.details.ItemDetails;
|
||||
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIService {
|
||||
private final DetailRegistry<ItemStack> itemStackDetails = new DetailRegistryImpl<>(ItemDetails::fillBasic);
|
||||
private final DetailRegistry<BlockReference> blockDetails = new DetailRegistryImpl<>(BlockDetails::fillBasic);
|
||||
|
||||
public static @Nullable InputStream getResourceFile(MinecraftServer server, String domain, String subPath) {
|
||||
var manager = server.getResourceManager();
|
||||
var resource = manager.getResource(new ResourceLocation(domain, subPath)).orElse(null);
|
||||
if (resource == null) return null;
|
||||
try {
|
||||
return resource.open();
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int createUniqueNumberedSaveDir(Level world, String parentSubPath) {
|
||||
var server = world.getServer();
|
||||
if (server == null) throw new IllegalArgumentException("Cannot find server from provided level");
|
||||
return ServerContext.get(server).getNextId(parentSubPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final @Nullable IWritableMount createSaveDirMount(Level world, String subPath, long capacity) {
|
||||
var server = world.getServer();
|
||||
if (server == null) throw new IllegalArgumentException("Cannot find server from provided level");
|
||||
|
||||
try {
|
||||
return new FileMount(new File(ServerContext.get(server).storageDir().toFile(), subPath), capacity);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void registerGenericSource(GenericSource source) {
|
||||
GenericMethod.register(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void registerBundledRedstoneProvider(IBundledRedstoneProvider provider) {
|
||||
BundledRedstone.register(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getBundledRedstoneOutput(Level world, BlockPos pos, Direction side) {
|
||||
return BundledRedstone.getDefaultOutput(world, pos, side);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void registerMediaProvider(IMediaProvider provider) {
|
||||
MediaProviders.register(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final IPacketNetwork getWirelessNetwork() {
|
||||
return WirelessNetwork.getUniversal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void registerAPIFactory(ILuaAPIFactory factory) {
|
||||
ApiFactories.register(factory);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public final IWiredNode createWiredNodeForElement(IWiredElement element) {
|
||||
return new WiredNode(element);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void registerRefuelHandler(TurtleRefuelHandler handler) {
|
||||
TurtleRefuelHandlers.register(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final DetailRegistry<ItemStack> getItemStackDetailRegistry() {
|
||||
return itemStackDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final DetailRegistry<BlockReference> getBlockInWorldDetailRegistry() {
|
||||
return blockDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> void registerDetailProvider(Class<T> type, IDetailProvider<T> provider) {
|
||||
if (type == ItemStack.class) {
|
||||
itemStackDetails.addProvider((IDetailProvider<ItemStack>) provider);
|
||||
} else if (type == BlockReference.class) {
|
||||
blockDetails.addProvider((IDetailProvider<BlockReference>) provider);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown detail provider " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
|
||||
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class BundledRedstone {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BundledRedstone.class);
|
||||
|
||||
private static final ArrayList<IBundledRedstoneProvider> providers = new ArrayList<>();
|
||||
|
||||
private BundledRedstone() {
|
||||
}
|
||||
|
||||
public static synchronized void register(IBundledRedstoneProvider provider) {
|
||||
Objects.requireNonNull(provider, "provider cannot be null");
|
||||
if (!providers.contains(provider)) providers.add(provider);
|
||||
}
|
||||
|
||||
public static int getDefaultOutput(Level world, BlockPos pos, Direction side) {
|
||||
return world.isInWorldBounds(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1;
|
||||
}
|
||||
|
||||
private static int getUnmaskedOutput(Level world, BlockPos pos, Direction side) {
|
||||
if (!world.isInWorldBounds(pos)) return -1;
|
||||
|
||||
// Try the providers in order:
|
||||
var combinedSignal = -1;
|
||||
for (var bundledRedstoneProvider : providers) {
|
||||
try {
|
||||
var signal = bundledRedstoneProvider.getBundledRedstoneOutput(world, pos, side);
|
||||
if (signal >= 0) {
|
||||
combinedSignal = combinedSignal < 0 ? signal & 0xffff : combinedSignal | (signal & 0xffff);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Bundled redstone provider " + bundledRedstoneProvider + " errored.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return combinedSignal;
|
||||
}
|
||||
|
||||
public static int getOutput(Level world, BlockPos pos, Direction side) {
|
||||
var signal = getUnmaskedOutput(world, pos, side);
|
||||
return signal >= 0 ? signal : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.media.IMedia;
|
||||
import dan200.computercraft.api.media.IMediaProvider;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class MediaProviders {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MediaProviders.class);
|
||||
|
||||
private static final Set<IMediaProvider> providers = new LinkedHashSet<>();
|
||||
|
||||
private MediaProviders() {
|
||||
}
|
||||
|
||||
public static synchronized void register(IMediaProvider provider) {
|
||||
Objects.requireNonNull(provider, "provider cannot be null");
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
public static @Nullable IMedia get(ItemStack stack) {
|
||||
if (stack.isEmpty()) return null;
|
||||
|
||||
// Try the handlers in order:
|
||||
for (var mediaProvider : providers) {
|
||||
try {
|
||||
var media = mediaProvider.getMedia(stack);
|
||||
if (media != null) return media;
|
||||
} catch (Exception e) {
|
||||
// Mod misbehaved, ignore it
|
||||
LOG.error("Media provider " + mediaProvider + " errored.", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class PocketUpgrades {
|
||||
private static final UpgradeManager<PocketUpgradeSerialiser<?>, IPocketUpgrade> registry = new UpgradeManager<>(
|
||||
"pocket computer upgrade", "computercraft/pocket_upgrades", PocketUpgradeSerialiser.REGISTRY_ID
|
||||
);
|
||||
|
||||
private PocketUpgrades() {
|
||||
}
|
||||
|
||||
public static UpgradeManager<PocketUpgradeSerialiser<?>, IPocketUpgrade> instance() {
|
||||
return registry;
|
||||
}
|
||||
|
||||
public static Stream<IPocketUpgrade> getVanillaUpgrades() {
|
||||
return instance().getUpgradeWrappers().values().stream()
|
||||
.filter(x -> x.modId().equals(ComputerCraftAPI.MOD_ID))
|
||||
.map(UpgradeManager.UpgradeWrapper::upgrade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Registry of {@link TurtleRefuelHandler}s.
|
||||
*/
|
||||
public final class TurtleRefuelHandlers {
|
||||
private static final List<TurtleRefuelHandler> handlers = new CopyOnWriteArrayList<>();
|
||||
|
||||
private TurtleRefuelHandlers() {
|
||||
}
|
||||
|
||||
public static synchronized void register(TurtleRefuelHandler handler) {
|
||||
Objects.requireNonNull(handler, "handler cannot be null");
|
||||
handlers.add(handler);
|
||||
}
|
||||
|
||||
public static OptionalInt refuel(ITurtleAccess turtle, ItemStack stack, int slot, int limit) {
|
||||
for (var handler : handlers) {
|
||||
var fuel = handler.refuel(turtle, stack, slot, limit);
|
||||
if (fuel.isPresent()) {
|
||||
var refuelled = fuel.getAsInt();
|
||||
if (refuelled < 0) throw new IllegalStateException(handler + " returned a negative value");
|
||||
if (limit == 0 && refuelled != 0) {
|
||||
throw new IllegalStateException(handler + " refuelled despite given a limit of 0");
|
||||
}
|
||||
|
||||
return fuel;
|
||||
}
|
||||
}
|
||||
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class TurtleUpgrades {
|
||||
private static final UpgradeManager<TurtleUpgradeSerialiser<?>, ITurtleUpgrade> registry = new UpgradeManager<>(
|
||||
"turtle upgrade", "computercraft/turtle_upgrades", TurtleUpgradeSerialiser.REGISTRY_ID
|
||||
);
|
||||
|
||||
private TurtleUpgrades() {
|
||||
}
|
||||
|
||||
public static UpgradeManager<TurtleUpgradeSerialiser<?>, ITurtleUpgrade> instance() {
|
||||
return registry;
|
||||
}
|
||||
|
||||
public static Stream<ITurtleUpgrade> getVanillaUpgrades() {
|
||||
return instance().getUpgradeWrappers().values().stream()
|
||||
.filter(x -> x.modId().equals(ComputerCraftAPI.MOD_ID))
|
||||
.map(UpgradeManager.UpgradeWrapper::upgrade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import com.google.gson.*;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.upgrades.IUpgradeBase;
|
||||
import dan200.computercraft.api.upgrades.UpgradeSerialiser;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Manages turtle and pocket computer upgrades.
|
||||
*
|
||||
* @param <R> The type of upgrade serialisers.
|
||||
* @param <T> The type of upgrade.
|
||||
* @see TurtleUpgrades
|
||||
* @see PocketUpgrades
|
||||
*/
|
||||
public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends IUpgradeBase> extends SimpleJsonResourceReloadListener {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
public record UpgradeWrapper<R extends UpgradeSerialiser<? extends T>, T extends IUpgradeBase>(
|
||||
String id, T upgrade, R serialiser, String modId
|
||||
) {
|
||||
}
|
||||
|
||||
private final String kind;
|
||||
private final ResourceKey<Registry<R>> registry;
|
||||
|
||||
private Map<String, UpgradeWrapper<R, T>> current = Collections.emptyMap();
|
||||
private Map<T, UpgradeWrapper<R, T>> currentWrappers = Collections.emptyMap();
|
||||
|
||||
public UpgradeManager(String kind, String path, ResourceKey<Registry<R>> registry) {
|
||||
super(GSON, path);
|
||||
this.kind = kind;
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T get(String id) {
|
||||
var wrapper = current.get(id);
|
||||
return wrapper == null ? null : wrapper.upgrade();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UpgradeWrapper<R, T> getWrapper(T upgrade) {
|
||||
return currentWrappers.get(upgrade);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getOwner(T upgrade) {
|
||||
var wrapper = currentWrappers.get(upgrade);
|
||||
return wrapper != null ? wrapper.modId() : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T get(ItemStack stack) {
|
||||
if (stack.isEmpty()) return null;
|
||||
|
||||
for (var wrapper : current.values()) {
|
||||
var craftingStack = wrapper.upgrade().getCraftingItem();
|
||||
if (!craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade().isItemSuitable(stack)) {
|
||||
return wrapper.upgrade();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Collection<T> getUpgrades() {
|
||||
return currentWrappers.keySet();
|
||||
}
|
||||
|
||||
public Map<String, UpgradeWrapper<R, T>> getUpgradeWrappers() {
|
||||
return current;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(Map<ResourceLocation, JsonElement> upgrades, ResourceManager manager, ProfilerFiller profiler) {
|
||||
Map<String, UpgradeWrapper<R, T>> newUpgrades = new HashMap<>();
|
||||
for (var element : upgrades.entrySet()) {
|
||||
try {
|
||||
loadUpgrade(newUpgrades, element.getKey(), element.getValue());
|
||||
} catch (IllegalArgumentException | JsonParseException e) {
|
||||
LOGGER.error("Error loading {} {} from JSON file", kind, element.getKey(), e);
|
||||
}
|
||||
}
|
||||
|
||||
current = Collections.unmodifiableMap(newUpgrades);
|
||||
currentWrappers = newUpgrades.values().stream().collect(Collectors.toUnmodifiableMap(UpgradeWrapper::upgrade, x -> x));
|
||||
LOGGER.info("Loaded {} {}s", current.size(), kind);
|
||||
}
|
||||
|
||||
private void loadUpgrade(Map<String, UpgradeWrapper<R, T>> current, ResourceLocation id, JsonElement json) {
|
||||
var root = GsonHelper.convertToJsonObject(json, "top element");
|
||||
var serialiserId = new ResourceLocation(GsonHelper.getAsString(root, "type"));
|
||||
|
||||
var serialiser = PlatformHelper.get().tryGetRegistryObject(registry, serialiserId);
|
||||
if (serialiser == null) throw new JsonSyntaxException("Unknown upgrade type '" + serialiserId + "'");
|
||||
|
||||
// TODO: Can we track which mod this resource came from and use that instead? It's theoretically possible,
|
||||
// but maybe not ideal for datapacks.
|
||||
var modId = id.getNamespace();
|
||||
if (modId.equals("minecraft") || modId.equals("")) modId = ComputerCraftAPI.MOD_ID;
|
||||
|
||||
var upgrade = serialiser.fromJson(id, root);
|
||||
if (!upgrade.getUpgradeID().equals(id)) {
|
||||
throw new IllegalArgumentException("Upgrade " + id + " from " + serialiser + " was incorrectly given id " + upgrade.getUpgradeID());
|
||||
}
|
||||
|
||||
var result = new UpgradeWrapper<R, T>(id.toString(), upgrade, serialiser, modId);
|
||||
current.put(result.id(), result);
|
||||
}
|
||||
|
||||
public void loadFromNetwork(Map<String, UpgradeWrapper<R, T>> newUpgrades) {
|
||||
current = Collections.unmodifiableMap(newUpgrades);
|
||||
currentWrappers = newUpgrades.values().stream().collect(Collectors.toUnmodifiableMap(UpgradeWrapper::upgrade, x -> x));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl.detail;
|
||||
|
||||
import dan200.computercraft.api.detail.DetailRegistry;
|
||||
import dan200.computercraft.api.detail.IDetailProvider;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Concrete implementation of {@link DetailRegistry}.
|
||||
*
|
||||
* @param <T> The type of object that this registry provides details for.
|
||||
*/
|
||||
public class DetailRegistryImpl<T> implements DetailRegistry<T> {
|
||||
private final Collection<IDetailProvider<T>> providers = new ArrayList<>();
|
||||
private final IDetailProvider<T> basic;
|
||||
|
||||
public DetailRegistryImpl(IDetailProvider<T> basic) {
|
||||
this.basic = basic;
|
||||
providers.add(basic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addProvider(IDetailProvider<T> provider) {
|
||||
Objects.requireNonNull(provider, "provider cannot be null");
|
||||
if (!providers.contains(provider)) providers.add(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getBasicDetails(T object) {
|
||||
Objects.requireNonNull(object, "object cannot be null");
|
||||
|
||||
Map<String, Object> map = new HashMap<>(4);
|
||||
basic.provideDetails(map, object);
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDetails(T object) {
|
||||
Objects.requireNonNull(object, "object cannot be null");
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (var provider : providers) provider.provideDetails(map, object);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Verifies certain elements of a network are "well formed".
|
||||
* <p>
|
||||
* This adds substantial overhead to network modification, and so should only be enabled
|
||||
* in a development environment.
|
||||
*/
|
||||
public final class InvariantChecker {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InvariantChecker.class);
|
||||
private static final boolean ENABLED = false;
|
||||
|
||||
private InvariantChecker() {
|
||||
}
|
||||
|
||||
public static void checkNode(WiredNode node) {
|
||||
if (!ENABLED) return;
|
||||
|
||||
var network = node.network;
|
||||
if (network == null) {
|
||||
LOG.error("Node's network is null", new Exception());
|
||||
return;
|
||||
}
|
||||
|
||||
if (network.nodes == null || !network.nodes.contains(node)) {
|
||||
LOG.error("Node's network does not contain node", new Exception());
|
||||
}
|
||||
|
||||
for (var neighbour : node.neighbours) {
|
||||
if (!neighbour.neighbours.contains(node)) {
|
||||
LOG.error("Neighbour is missing node", new Exception());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkNetwork(WiredNetwork network) {
|
||||
if (!ENABLED) return;
|
||||
|
||||
for (var node : network.nodes) checkNode(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import dan200.computercraft.api.network.Packet;
|
||||
import dan200.computercraft.api.network.wired.IWiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.IWiredNode;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public final class WiredNetwork implements IWiredNetwork {
|
||||
final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
Set<WiredNode> nodes;
|
||||
private Map<String, IPeripheral> peripherals = new HashMap<>();
|
||||
|
||||
WiredNetwork(WiredNode node) {
|
||||
nodes = new HashSet<>(1);
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
private WiredNetwork(HashSet<WiredNode> nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean connect(IWiredNode nodeU, IWiredNode nodeV) {
|
||||
var wiredU = checkNode(nodeU);
|
||||
var wiredV = checkNode(nodeV);
|
||||
if (nodeU == nodeV) throw new IllegalArgumentException("Cannot add a connection to oneself.");
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (nodes.isEmpty()) throw new IllegalStateException("Cannot add a connection to an empty network.");
|
||||
|
||||
var hasU = wiredU.network == this;
|
||||
var hasV = wiredV.network == this;
|
||||
if (!hasU && !hasV) throw new IllegalArgumentException("Neither node is in the network.");
|
||||
|
||||
// We're going to assimilate a node. Copy across all edges and vertices.
|
||||
if (!hasU || !hasV) {
|
||||
var other = hasU ? wiredV.network : wiredU.network;
|
||||
other.lock.writeLock().lock();
|
||||
try {
|
||||
// Cache several properties for iterating over later
|
||||
var otherPeripherals = other.peripherals;
|
||||
var thisPeripherals = otherPeripherals.isEmpty() ? peripherals : new HashMap<>(peripherals);
|
||||
|
||||
var thisNodes = otherPeripherals.isEmpty() ? nodes : new ArrayList<>(nodes);
|
||||
var otherNodes = other.nodes;
|
||||
|
||||
// Move all nodes across into this network, destroying the original nodes.
|
||||
nodes.addAll(otherNodes);
|
||||
for (var node : otherNodes) node.network = this;
|
||||
other.nodes = Collections.emptySet();
|
||||
|
||||
// Move all peripherals across,
|
||||
other.peripherals = Collections.emptyMap();
|
||||
peripherals.putAll(otherPeripherals);
|
||||
|
||||
if (!thisPeripherals.isEmpty()) {
|
||||
WiredNetworkChange.added(thisPeripherals).broadcast(otherNodes);
|
||||
}
|
||||
|
||||
if (!otherPeripherals.isEmpty()) {
|
||||
WiredNetworkChange.added(otherPeripherals).broadcast(thisNodes);
|
||||
}
|
||||
} finally {
|
||||
other.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
var added = wiredU.neighbours.add(wiredV);
|
||||
if (added) wiredV.neighbours.add(wiredU);
|
||||
|
||||
InvariantChecker.checkNetwork(this);
|
||||
InvariantChecker.checkNode(wiredU);
|
||||
InvariantChecker.checkNode(wiredV);
|
||||
|
||||
return added;
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disconnect(IWiredNode nodeU, IWiredNode nodeV) {
|
||||
var wiredU = checkNode(nodeU);
|
||||
var wiredV = checkNode(nodeV);
|
||||
if (nodeU == nodeV) throw new IllegalArgumentException("Cannot remove a connection to oneself.");
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
var hasU = wiredU.network == this;
|
||||
var hasV = wiredV.network == this;
|
||||
if (!hasU || !hasV) throw new IllegalArgumentException("One node is not in the network.");
|
||||
|
||||
// If there was no connection to remove then split.
|
||||
if (!wiredU.neighbours.remove(wiredV)) return false;
|
||||
wiredV.neighbours.remove(wiredU);
|
||||
|
||||
// Determine if there is still some connection from u to v.
|
||||
// Note this is an inlining of reachableNodes which short-circuits
|
||||
// if all nodes are reachable.
|
||||
Queue<WiredNode> enqueued = new ArrayDeque<>();
|
||||
var reachableU = new HashSet<WiredNode>();
|
||||
|
||||
reachableU.add(wiredU);
|
||||
enqueued.add(wiredU);
|
||||
|
||||
while (!enqueued.isEmpty()) {
|
||||
var node = enqueued.remove();
|
||||
for (var neighbour : node.neighbours) {
|
||||
// If we can reach wiredV from wiredU then abort.
|
||||
if (neighbour == wiredV) return true;
|
||||
|
||||
// Otherwise attempt to enqueue this neighbour as well.
|
||||
if (reachableU.add(neighbour)) enqueued.add(neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new network with all U-reachable nodes/edges and remove them
|
||||
// from the existing graph.
|
||||
var networkU = new WiredNetwork(reachableU);
|
||||
networkU.lock.writeLock().lock();
|
||||
try {
|
||||
// Remove nodes from this network
|
||||
nodes.removeAll(reachableU);
|
||||
|
||||
// Set network and transfer peripherals
|
||||
for (var node : reachableU) {
|
||||
node.network = networkU;
|
||||
networkU.peripherals.putAll(node.peripherals);
|
||||
peripherals.keySet().removeAll(node.peripherals.keySet());
|
||||
}
|
||||
|
||||
// Broadcast changes
|
||||
if (!peripherals.isEmpty()) WiredNetworkChange.removed(peripherals).broadcast(networkU.nodes);
|
||||
if (!networkU.peripherals.isEmpty()) {
|
||||
WiredNetworkChange.removed(networkU.peripherals).broadcast(nodes);
|
||||
}
|
||||
|
||||
InvariantChecker.checkNetwork(this);
|
||||
InvariantChecker.checkNetwork(networkU);
|
||||
InvariantChecker.checkNode(wiredU);
|
||||
InvariantChecker.checkNode(wiredV);
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
networkU.lock.writeLock().unlock();
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(IWiredNode node) {
|
||||
var wired = checkNode(node);
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
// If we're the empty graph then just abort: nodes must have _some_ network.
|
||||
if (nodes.isEmpty()) return false;
|
||||
if (nodes.size() <= 1) return false;
|
||||
if (wired.network != this) return false;
|
||||
|
||||
var neighbours = wired.neighbours;
|
||||
|
||||
// Remove this node and move into a separate network.
|
||||
nodes.remove(wired);
|
||||
for (var neighbour : neighbours) neighbour.neighbours.remove(wired);
|
||||
|
||||
var wiredNetwork = new WiredNetwork(wired);
|
||||
|
||||
// If we're a leaf node in the graph (only one neighbour) then we don't need to
|
||||
// check for network splitting
|
||||
if (neighbours.size() == 1) {
|
||||
// Broadcast our simple peripheral changes
|
||||
removeSingleNode(wired, wiredNetwork);
|
||||
InvariantChecker.checkNode(wired);
|
||||
InvariantChecker.checkNetwork(wiredNetwork);
|
||||
return true;
|
||||
}
|
||||
|
||||
var reachable = reachableNodes(neighbours.iterator().next());
|
||||
|
||||
// If all nodes are reachable then exit.
|
||||
if (reachable.size() == nodes.size()) {
|
||||
// Broadcast our simple peripheral changes
|
||||
removeSingleNode(wired, wiredNetwork);
|
||||
InvariantChecker.checkNode(wired);
|
||||
InvariantChecker.checkNetwork(wiredNetwork);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A split may cause 2..neighbours.size() separate networks, so we
|
||||
// iterate through our neighbour list, generating child networks.
|
||||
neighbours.removeAll(reachable);
|
||||
var maximals = new ArrayList<WiredNetwork>(neighbours.size() + 1);
|
||||
maximals.add(wiredNetwork);
|
||||
maximals.add(new WiredNetwork(reachable));
|
||||
|
||||
while (!neighbours.isEmpty()) {
|
||||
reachable = reachableNodes(neighbours.iterator().next());
|
||||
neighbours.removeAll(reachable);
|
||||
maximals.add(new WiredNetwork(reachable));
|
||||
}
|
||||
|
||||
for (var network : maximals) network.lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
// We special case the original node: detaching all peripherals when needed.
|
||||
wired.network = wiredNetwork;
|
||||
wired.peripherals = Collections.emptyMap();
|
||||
|
||||
// Ensure every network is finalised
|
||||
for (var network : maximals) {
|
||||
for (var child : network.nodes) {
|
||||
child.network = network;
|
||||
network.peripherals.putAll(child.peripherals);
|
||||
}
|
||||
}
|
||||
|
||||
for (var network : maximals) InvariantChecker.checkNetwork(network);
|
||||
InvariantChecker.checkNode(wired);
|
||||
|
||||
// Then broadcast network changes once all nodes are finalised
|
||||
for (var network : maximals) {
|
||||
WiredNetworkChange.changeOf(peripherals, network.peripherals).broadcast(network.nodes);
|
||||
}
|
||||
} finally {
|
||||
for (var network : maximals) network.lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
nodes.clear();
|
||||
peripherals.clear();
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePeripherals(IWiredNode node, Map<String, IPeripheral> newPeripherals) {
|
||||
var wired = checkNode(node);
|
||||
Objects.requireNonNull(peripherals, "peripherals cannot be null");
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
if (wired.network != this) throw new IllegalStateException("Node is not on this network");
|
||||
|
||||
var oldPeripherals = wired.peripherals;
|
||||
var change = WiredNetworkChange.changeOf(oldPeripherals, newPeripherals);
|
||||
if (change.isEmpty()) return;
|
||||
|
||||
wired.peripherals = ImmutableMap.copyOf(newPeripherals);
|
||||
|
||||
// Detach the old peripherals then remove them.
|
||||
peripherals.keySet().removeAll(change.peripheralsRemoved().keySet());
|
||||
|
||||
// Add the new peripherals and attach them
|
||||
peripherals.putAll(change.peripheralsAdded());
|
||||
|
||||
change.broadcast(nodes);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void transmitPacket(WiredNode start, Packet packet, double range, boolean interdimensional) {
|
||||
Map<WiredNode, TransmitPoint> points = new HashMap<>();
|
||||
var transmitTo = new TreeSet<TransmitPoint>();
|
||||
|
||||
{
|
||||
var startEntry = start.element.getLevel() != packet.sender().getLevel()
|
||||
? new TransmitPoint(start, Double.POSITIVE_INFINITY, true)
|
||||
: new TransmitPoint(start, start.element.getPosition().distanceTo(packet.sender().getPosition()), false);
|
||||
points.put(start, startEntry);
|
||||
transmitTo.add(startEntry);
|
||||
}
|
||||
|
||||
{
|
||||
TransmitPoint point;
|
||||
while ((point = transmitTo.pollFirst()) != null) {
|
||||
var world = point.node.element.getLevel();
|
||||
var position = point.node.element.getPosition();
|
||||
for (var neighbour : point.node.neighbours) {
|
||||
var neighbourPoint = points.get(neighbour);
|
||||
|
||||
boolean newInterdimensional;
|
||||
double newDistance;
|
||||
if (world != neighbour.element.getLevel()) {
|
||||
newInterdimensional = true;
|
||||
newDistance = Double.POSITIVE_INFINITY;
|
||||
} else {
|
||||
newInterdimensional = false;
|
||||
newDistance = point.distance + position.distanceTo(neighbour.element.getPosition());
|
||||
}
|
||||
|
||||
if (neighbourPoint == null) {
|
||||
var nextPoint = new TransmitPoint(neighbour, newDistance, newInterdimensional);
|
||||
points.put(neighbour, nextPoint);
|
||||
transmitTo.add(nextPoint);
|
||||
} else if (newDistance < neighbourPoint.distance) {
|
||||
transmitTo.remove(neighbourPoint);
|
||||
neighbourPoint.distance = newDistance;
|
||||
neighbourPoint.interdimensional = newInterdimensional;
|
||||
transmitTo.add(neighbourPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var point : points.values()) {
|
||||
point.node.tryTransmit(packet, point.distance, point.interdimensional, range, interdimensional);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeSingleNode(WiredNode wired, WiredNetwork wiredNetwork) {
|
||||
wiredNetwork.lock.writeLock().lock();
|
||||
try {
|
||||
// Cache all the old nodes.
|
||||
Map<String, IPeripheral> wiredPeripherals = new HashMap<>(wired.peripherals);
|
||||
|
||||
// Setup the new node's network
|
||||
// Detach the old peripherals then remove them from the old network
|
||||
wired.network = wiredNetwork;
|
||||
wired.neighbours.clear();
|
||||
wired.peripherals = Collections.emptyMap();
|
||||
|
||||
// Broadcast the change
|
||||
if (!peripherals.isEmpty()) WiredNetworkChange.removed(peripherals).broadcast(wired);
|
||||
|
||||
// Now remove all peripherals from this network and broadcast the change.
|
||||
peripherals.keySet().removeAll(wiredPeripherals.keySet());
|
||||
if (!wiredPeripherals.isEmpty()) WiredNetworkChange.removed(wiredPeripherals).broadcast(nodes);
|
||||
|
||||
} finally {
|
||||
wiredNetwork.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransmitPoint implements Comparable<TransmitPoint> {
|
||||
final WiredNode node;
|
||||
double distance;
|
||||
boolean interdimensional;
|
||||
|
||||
TransmitPoint(WiredNode node, double distance, boolean interdimensional) {
|
||||
this.node = node;
|
||||
this.distance = distance;
|
||||
this.interdimensional = interdimensional;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TransmitPoint o) {
|
||||
// Objects with the same distance are not the same object, so we must add an additional layer of ordering.
|
||||
return distance == o.distance
|
||||
? Integer.compare(node.hashCode(), o.node.hashCode())
|
||||
: Double.compare(distance, o.distance);
|
||||
}
|
||||
}
|
||||
|
||||
private static WiredNode checkNode(IWiredNode node) {
|
||||
if (node instanceof WiredNode) {
|
||||
return (WiredNode) node;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown implementation of IWiredNode: " + node);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<WiredNode> reachableNodes(WiredNode start) {
|
||||
Queue<WiredNode> enqueued = new ArrayDeque<>();
|
||||
var reachable = new HashSet<WiredNode>();
|
||||
|
||||
reachable.add(start);
|
||||
enqueued.add(start);
|
||||
|
||||
WiredNode node;
|
||||
while ((node = enqueued.poll()) != null) {
|
||||
for (var neighbour : node.neighbours) {
|
||||
// Otherwise attempt to enqueue this neighbour as well.
|
||||
if (reachable.add(neighbour)) enqueued.add(neighbour);
|
||||
}
|
||||
}
|
||||
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.wired.IWiredNetworkChange;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class WiredNetworkChange implements IWiredNetworkChange {
|
||||
private static final WiredNetworkChange EMPTY = new WiredNetworkChange(Collections.emptyMap(), Collections.emptyMap());
|
||||
|
||||
private final Map<String, IPeripheral> removed;
|
||||
private final Map<String, IPeripheral> added;
|
||||
|
||||
private WiredNetworkChange(Map<String, IPeripheral> removed, Map<String, IPeripheral> added) {
|
||||
this.removed = removed;
|
||||
this.added = added;
|
||||
}
|
||||
|
||||
public static WiredNetworkChange changed(Map<String, IPeripheral> removed, Map<String, IPeripheral> added) {
|
||||
return new WiredNetworkChange(Collections.unmodifiableMap(removed), Collections.unmodifiableMap(added));
|
||||
}
|
||||
|
||||
public static WiredNetworkChange added(Map<String, IPeripheral> added) {
|
||||
return added.isEmpty() ? EMPTY : new WiredNetworkChange(Collections.emptyMap(), Collections.unmodifiableMap(added));
|
||||
}
|
||||
|
||||
public static WiredNetworkChange removed(Map<String, IPeripheral> removed) {
|
||||
return removed.isEmpty() ? EMPTY : new WiredNetworkChange(Collections.unmodifiableMap(removed), Collections.emptyMap());
|
||||
}
|
||||
|
||||
public static WiredNetworkChange changeOf(Map<String, IPeripheral> oldPeripherals, Map<String, IPeripheral> newPeripherals) {
|
||||
// Handle the trivial cases, where all peripherals have been added or removed.
|
||||
if (oldPeripherals.isEmpty() && newPeripherals.isEmpty()) {
|
||||
return EMPTY;
|
||||
} else if (oldPeripherals.isEmpty()) {
|
||||
return new WiredNetworkChange(Collections.emptyMap(), newPeripherals);
|
||||
} else if (newPeripherals.isEmpty()) {
|
||||
return new WiredNetworkChange(oldPeripherals, Collections.emptyMap());
|
||||
}
|
||||
|
||||
Map<String, IPeripheral> added = new HashMap<>(newPeripherals);
|
||||
Map<String, IPeripheral> removed = new HashMap<>();
|
||||
|
||||
for (var entry : oldPeripherals.entrySet()) {
|
||||
var oldKey = entry.getKey();
|
||||
var oldValue = entry.getValue();
|
||||
if (newPeripherals.containsKey(oldKey)) {
|
||||
var rightValue = added.get(oldKey);
|
||||
if (oldValue.equals(rightValue)) {
|
||||
added.remove(oldKey);
|
||||
} else {
|
||||
removed.put(oldKey, oldValue);
|
||||
}
|
||||
} else {
|
||||
removed.put(oldKey, oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
return changed(removed, added);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IPeripheral> peripheralsAdded() {
|
||||
return added;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IPeripheral> peripheralsRemoved() {
|
||||
return removed;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return added.isEmpty() && removed.isEmpty();
|
||||
}
|
||||
|
||||
void broadcast(Iterable<WiredNode> nodes) {
|
||||
if (!isEmpty()) {
|
||||
for (var node : nodes) node.element.networkChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
void broadcast(WiredNode node) {
|
||||
if (!isEmpty()) {
|
||||
node.element.networkChanged(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.impl.network.wired;
|
||||
|
||||
import dan200.computercraft.api.network.IPacketReceiver;
|
||||
import dan200.computercraft.api.network.Packet;
|
||||
import dan200.computercraft.api.network.wired.IWiredElement;
|
||||
import dan200.computercraft.api.network.wired.IWiredNetwork;
|
||||
import dan200.computercraft.api.network.wired.IWiredNode;
|
||||
import dan200.computercraft.api.network.wired.IWiredSender;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
public final class WiredNode implements IWiredNode {
|
||||
private @Nullable Set<IPacketReceiver> receivers;
|
||||
|
||||
final IWiredElement element;
|
||||
Map<String, IPeripheral> peripherals = Collections.emptyMap();
|
||||
|
||||
final HashSet<WiredNode> neighbours = new HashSet<>();
|
||||
volatile WiredNetwork network;
|
||||
|
||||
public WiredNode(IWiredElement element) {
|
||||
this.element = element;
|
||||
network = new WiredNetwork(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addReceiver(IPacketReceiver receiver) {
|
||||
if (receivers == null) receivers = new HashSet<>();
|
||||
receivers.add(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removeReceiver(IPacketReceiver receiver) {
|
||||
if (receivers != null) receivers.remove(receiver);
|
||||
}
|
||||
|
||||
synchronized void tryTransmit(Packet packet, double packetDistance, boolean packetInterdimensional, double range, boolean interdimensional) {
|
||||
if (receivers == null) return;
|
||||
|
||||
for (var receiver : receivers) {
|
||||
if (!packetInterdimensional) {
|
||||
var receiveRange = Math.max(range, receiver.getRange()); // Ensure range is symmetrical
|
||||
if (interdimensional || receiver.isInterdimensional() || packetDistance < receiveRange) {
|
||||
receiver.receiveSameDimension(packet, packetDistance + element.getPosition().distanceTo(receiver.getPosition()));
|
||||
}
|
||||
} else {
|
||||
if (interdimensional || receiver.isInterdimensional()) {
|
||||
receiver.receiveDifferentDimension(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWireless() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transmitSameDimension(Packet packet, double range) {
|
||||
Objects.requireNonNull(packet, "packet cannot be null");
|
||||
if (!(packet.sender() instanceof IWiredSender) || ((IWiredSender) packet.sender()).getNode() != this) {
|
||||
throw new IllegalArgumentException("Sender is not in the network");
|
||||
}
|
||||
|
||||
acquireReadLock();
|
||||
try {
|
||||
WiredNetwork.transmitPacket(this, packet, range, false);
|
||||
} finally {
|
||||
network.lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transmitInterdimensional(Packet packet) {
|
||||
Objects.requireNonNull(packet, "packet cannot be null");
|
||||
if (!(packet.sender() instanceof IWiredSender) || ((IWiredSender) packet.sender()).getNode() != this) {
|
||||
throw new IllegalArgumentException("Sender is not in the network");
|
||||
}
|
||||
|
||||
acquireReadLock();
|
||||
try {
|
||||
WiredNetwork.transmitPacket(this, packet, 0, true);
|
||||
} finally {
|
||||
network.lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IWiredElement getElement() {
|
||||
return element;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IWiredNetwork getNetwork() {
|
||||
return network;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WiredNode{@" + element.getPosition() + " (" + element.getClass().getSimpleName() + ")}";
|
||||
}
|
||||
|
||||
private void acquireReadLock() {
|
||||
var currentNetwork = network;
|
||||
while (true) {
|
||||
var lock = currentNetwork.lock.readLock();
|
||||
lock.lock();
|
||||
if (currentNetwork == network) return;
|
||||
|
||||
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.mixin;
|
||||
|
||||
import dan200.computercraft.data.PrettyJsonWriter;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||
|
||||
@Mixin(targets = "net/minecraft/data/HashCache$CacheUpdater")
|
||||
public class CacheUpdaterMixin {
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
@ModifyArg(
|
||||
method = "writeIfNeeded",
|
||||
at = @At(value = "INVOKE", target = "Ljava/nio/file/Files;write(Ljava/nio/file/Path;[B[Ljava/nio/file/OpenOption;)Ljava/nio/file/Path;"),
|
||||
require = 0
|
||||
)
|
||||
private byte[] reformatJson(byte[] contents) {
|
||||
// It would be cleaner to do this inside DataProvider.saveStable, but Forge's version of Mixin doesn't allow us
|
||||
// to inject into interfaces.
|
||||
return PrettyJsonWriter.reformat(contents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.mixin;
|
||||
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(CreativeModeTab.class)
|
||||
public interface CreativeModeTabAccessor {
|
||||
@Accessor("langId")
|
||||
String computercraft$langId();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.mixin;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Explosion;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Mixin(Explosion.class)
|
||||
public interface ExplosionAccessor {
|
||||
@Nullable
|
||||
@Accessor("source")
|
||||
Entity computercraft$getExploder();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
/**
|
||||
* ComputerCraft's core Lua runtime and APIs.
|
||||
* <p>
|
||||
* This is not considered part of the stable API, and so should not be consumed by other Minecraft mods. However,
|
||||
* emulators or other CC-tooling may find this useful.
|
||||
*/
|
||||
@DefaultQualifier(value = NonNull.class, locations = {
|
||||
TypeUseLocation.RETURN,
|
||||
TypeUseLocation.PARAMETER,
|
||||
TypeUseLocation.FIELD,
|
||||
})
|
||||
package dan200.computercraft;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
import org.checkerframework.framework.qual.TypeUseLocation;
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.impl.PocketUpgrades;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.shared.computer.core.ResourceMount;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.metrics.ComputerMBean;
|
||||
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorWatcher;
|
||||
import dan200.computercraft.shared.util.DropConsumer;
|
||||
import dan200.computercraft.shared.util.TickScheduler;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.dedicated.DedicatedServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
import net.minecraft.world.level.storage.loot.LootPool;
|
||||
import net.minecraft.world.level.storage.loot.entries.LootTableReference;
|
||||
import net.minecraft.world.level.storage.loot.providers.number.ConstantValue;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Event listeners for server/common code.
|
||||
* <p>
|
||||
* All event handlers should be defined in this class, and then invoked from a loader-specific event handler. This means
|
||||
* it's much easier to ensure that each hook is called in all loader source sets.
|
||||
*/
|
||||
public final class CommonHooks {
|
||||
private CommonHooks() {
|
||||
}
|
||||
|
||||
public static void onServerTickStart(MinecraftServer server) {
|
||||
ServerContext.get(server).tick();
|
||||
TickScheduler.tick();
|
||||
}
|
||||
|
||||
public static void onServerTickEnd() {
|
||||
MonitorWatcher.onTick();
|
||||
}
|
||||
|
||||
public static void onServerStarting(MinecraftServer server) {
|
||||
if (server instanceof DedicatedServer dediServer && dediServer.getProperties().enableJmxMonitoring) {
|
||||
ComputerMBean.register();
|
||||
}
|
||||
|
||||
resetState();
|
||||
ServerContext.create(server);
|
||||
ComputerMBean.start(server);
|
||||
}
|
||||
|
||||
public static void onServerStopped() {
|
||||
resetState();
|
||||
}
|
||||
|
||||
private static void resetState() {
|
||||
ServerContext.close();
|
||||
WirelessNetwork.resetNetworks();
|
||||
NetworkUtils.reset();
|
||||
}
|
||||
|
||||
public static void onChunkWatch(LevelChunk chunk, ServerPlayer player) {
|
||||
MonitorWatcher.onWatch(chunk, player);
|
||||
}
|
||||
|
||||
public static final ResourceLocation LOOT_TREASURE_DISK = new ResourceLocation(ComputerCraftAPI.MOD_ID, "treasure_disk");
|
||||
|
||||
private static final Set<ResourceLocation> TABLES = new HashSet<>(Arrays.asList(
|
||||
BuiltInLootTables.SIMPLE_DUNGEON,
|
||||
BuiltInLootTables.ABANDONED_MINESHAFT,
|
||||
BuiltInLootTables.STRONGHOLD_CORRIDOR,
|
||||
BuiltInLootTables.STRONGHOLD_CROSSING,
|
||||
BuiltInLootTables.STRONGHOLD_LIBRARY,
|
||||
BuiltInLootTables.DESERT_PYRAMID,
|
||||
BuiltInLootTables.JUNGLE_TEMPLE,
|
||||
BuiltInLootTables.IGLOO_CHEST,
|
||||
BuiltInLootTables.WOODLAND_MANSION,
|
||||
BuiltInLootTables.VILLAGE_CARTOGRAPHER
|
||||
));
|
||||
|
||||
|
||||
public static @Nullable LootPool.Builder getExtraLootPool(ResourceLocation lootTable) {
|
||||
if (!lootTable.getNamespace().equals("minecraft") || !TABLES.contains(lootTable)) return null;
|
||||
|
||||
return LootPool.lootPool()
|
||||
.add(LootTableReference.lootTableReference(LOOT_TREASURE_DISK))
|
||||
.setRolls(ConstantValue.exactly(1));
|
||||
}
|
||||
|
||||
public static void onDatapackReload(BiConsumer<String, PreparableReloadListener> addReload) {
|
||||
addReload.accept("mounts", ResourceMount.RELOAD_LISTENER);
|
||||
addReload.accept("turtle_upgrades", TurtleUpgrades.instance());
|
||||
addReload.accept("pocket_upgrades", PocketUpgrades.instance());
|
||||
}
|
||||
|
||||
public static boolean onEntitySpawn(Entity entity) {
|
||||
return DropConsumer.onEntitySpawn(entity);
|
||||
}
|
||||
|
||||
public static boolean onLivingDrop(Entity entity, ItemStack stack) {
|
||||
return DropConsumer.onLivingDrop(entity, stack);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared;
|
||||
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.detail.IDetailProvider;
|
||||
import dan200.computercraft.api.detail.VanillaDetailRegistries;
|
||||
import dan200.computercraft.api.media.IMedia;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
|
||||
import dan200.computercraft.shared.common.ColourableRecipe;
|
||||
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
|
||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
||||
import dan200.computercraft.shared.computer.blocks.CommandComputerBlockEntity;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
|
||||
import dan200.computercraft.shared.computer.items.ComputerItem;
|
||||
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
|
||||
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
|
||||
import dan200.computercraft.shared.data.ConstantLootConditionSerializer;
|
||||
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
|
||||
import dan200.computercraft.shared.data.PlayerCreativeLootCondition;
|
||||
import dan200.computercraft.shared.details.BlockDetails;
|
||||
import dan200.computercraft.shared.details.ItemDetails;
|
||||
import dan200.computercraft.shared.media.items.DiskItem;
|
||||
import dan200.computercraft.shared.media.items.ItemTreasureDisk;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.media.items.RecordMedia;
|
||||
import dan200.computercraft.shared.media.recipes.DiskRecipe;
|
||||
import dan200.computercraft.shared.media.recipes.PrintoutRecipe;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import dan200.computercraft.shared.network.container.ContainerData;
|
||||
import dan200.computercraft.shared.network.container.HeldItemContainerData;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
|
||||
import dan200.computercraft.shared.peripheral.modem.wired.*;
|
||||
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
|
||||
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterMenu;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlock;
|
||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.platform.RegistrationHelper;
|
||||
import dan200.computercraft.shared.platform.RegistryEntry;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.pocket.peripherals.PocketModem;
|
||||
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
|
||||
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
|
||||
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
||||
import dan200.computercraft.shared.turtle.upgrades.*;
|
||||
import dan200.computercraft.shared.util.ImpostorRecipe;
|
||||
import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.cauldron.CauldronInteraction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.RecordItem;
|
||||
import net.minecraft.world.item.crafting.CustomRecipe;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.SimpleRecipeSerializer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
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.material.Material;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Registers ComputerCraft's registry entries and additional objects, such as {@link CauldronInteraction}s and
|
||||
* {@link IDetailProvider}s
|
||||
* <p>
|
||||
* The functions in this class should be called from a loader-specific class.
|
||||
*/
|
||||
public final class ModRegistry {
|
||||
private ModRegistry() {
|
||||
}
|
||||
|
||||
public static final class Blocks {
|
||||
static final RegistrationHelper<Block> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.BLOCK_REGISTRY);
|
||||
|
||||
private static BlockBehaviour.Properties properties() {
|
||||
return BlockBehaviour.Properties.of(Material.STONE).strength(2);
|
||||
}
|
||||
|
||||
private static BlockBehaviour.Properties computerProperties() {
|
||||
// Computers shouldn't conduct redstone through them, so set isRedstoneConductor to false. This still allows
|
||||
// redstone to connect to computers though as it's a signal source.
|
||||
return properties().isRedstoneConductor((block, level, blockPos) -> false);
|
||||
}
|
||||
|
||||
private static BlockBehaviour.Properties turtleProperties() {
|
||||
return BlockBehaviour.Properties.of(Material.STONE).strength(2.5f);
|
||||
}
|
||||
|
||||
private static BlockBehaviour.Properties modemProperties() {
|
||||
return BlockBehaviour.Properties.of(Material.STONE).strength(1.5f);
|
||||
}
|
||||
|
||||
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_NORMAL = REGISTRY.register("computer_normal",
|
||||
() -> new ComputerBlock<>(computerProperties(), ComputerFamily.NORMAL, BlockEntities.COMPUTER_NORMAL));
|
||||
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
|
||||
() -> new ComputerBlock<>(computerProperties(), ComputerFamily.ADVANCED, BlockEntities.COMPUTER_ADVANCED));
|
||||
|
||||
public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command", () -> new ComputerBlock<>(
|
||||
computerProperties().strength(-1, 6000000.0F),
|
||||
ComputerFamily.COMMAND, BlockEntities.COMPUTER_COMMAND
|
||||
));
|
||||
|
||||
public static final RegistryEntry<TurtleBlock> TURTLE_NORMAL = REGISTRY.register("turtle_normal",
|
||||
() -> new TurtleBlock(turtleProperties(), ComputerFamily.NORMAL, BlockEntities.TURTLE_NORMAL));
|
||||
public static final RegistryEntry<TurtleBlock> TURTLE_ADVANCED = REGISTRY.register("turtle_advanced",
|
||||
() -> new TurtleBlock(turtleProperties(), ComputerFamily.ADVANCED, BlockEntities.TURTLE_ADVANCED));
|
||||
|
||||
public static final RegistryEntry<SpeakerBlock> SPEAKER = REGISTRY.register("speaker", () -> new SpeakerBlock(properties()));
|
||||
public static final RegistryEntry<DiskDriveBlock> DISK_DRIVE = REGISTRY.register("disk_drive", () -> new DiskDriveBlock(properties()));
|
||||
public static final RegistryEntry<PrinterBlock> PRINTER = REGISTRY.register("printer", () -> new PrinterBlock(properties()));
|
||||
|
||||
public static final RegistryEntry<MonitorBlock> MONITOR_NORMAL = REGISTRY.register("monitor_normal",
|
||||
() -> new MonitorBlock(properties(), BlockEntities.MONITOR_NORMAL));
|
||||
public static final RegistryEntry<MonitorBlock> MONITOR_ADVANCED = REGISTRY.register("monitor_advanced",
|
||||
() -> new MonitorBlock(properties(), BlockEntities.MONITOR_ADVANCED));
|
||||
|
||||
public static final RegistryEntry<WirelessModemBlock> WIRELESS_MODEM_NORMAL = REGISTRY.register("wireless_modem_normal",
|
||||
() -> new WirelessModemBlock(properties(), BlockEntities.WIRELESS_MODEM_NORMAL));
|
||||
public static final RegistryEntry<WirelessModemBlock> WIRELESS_MODEM_ADVANCED = REGISTRY.register("wireless_modem_advanced",
|
||||
() -> new WirelessModemBlock(properties(), BlockEntities.WIRELESS_MODEM_ADVANCED));
|
||||
|
||||
public static final RegistryEntry<WiredModemFullBlock> WIRED_MODEM_FULL = REGISTRY.register("wired_modem_full",
|
||||
() -> new WiredModemFullBlock(modemProperties()));
|
||||
public static final RegistryEntry<CableBlock> CABLE = REGISTRY.register("cable", () -> new CableBlock(modemProperties()));
|
||||
}
|
||||
|
||||
public static class BlockEntities {
|
||||
static final RegistrationHelper<BlockEntityType<?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.BLOCK_ENTITY_TYPE_REGISTRY);
|
||||
|
||||
private static <T extends BlockEntity> RegistryEntry<BlockEntityType<T>> ofBlock(RegistryEntry<? extends Block> block, BiFunction<BlockPos, BlockState, T> factory) {
|
||||
return REGISTRY.register(block.id().getPath(), () -> PlatformHelper.get().createBlockEntityType(factory, block.get()));
|
||||
}
|
||||
|
||||
public static final RegistryEntry<BlockEntityType<MonitorBlockEntity>> MONITOR_NORMAL =
|
||||
ofBlock(Blocks.MONITOR_NORMAL, (p, s) -> new MonitorBlockEntity(BlockEntities.MONITOR_NORMAL.get(), p, s, false));
|
||||
public static final RegistryEntry<BlockEntityType<MonitorBlockEntity>> MONITOR_ADVANCED =
|
||||
ofBlock(Blocks.MONITOR_ADVANCED, (p, s) -> new MonitorBlockEntity(BlockEntities.MONITOR_ADVANCED.get(), p, s, true));
|
||||
|
||||
public static final RegistryEntry<BlockEntityType<ComputerBlockEntity>> COMPUTER_NORMAL =
|
||||
ofBlock(Blocks.COMPUTER_NORMAL, (p, s) -> new ComputerBlockEntity(BlockEntities.COMPUTER_NORMAL.get(), p, s, ComputerFamily.NORMAL));
|
||||
public static final RegistryEntry<BlockEntityType<ComputerBlockEntity>> COMPUTER_ADVANCED =
|
||||
ofBlock(Blocks.COMPUTER_ADVANCED, (p, s) -> new ComputerBlockEntity(BlockEntities.COMPUTER_ADVANCED.get(), p, s, ComputerFamily.ADVANCED));
|
||||
public static final RegistryEntry<BlockEntityType<CommandComputerBlockEntity>> COMPUTER_COMMAND =
|
||||
ofBlock(Blocks.COMPUTER_COMMAND, (p, s) -> new CommandComputerBlockEntity(BlockEntities.COMPUTER_COMMAND.get(), p, s));
|
||||
|
||||
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_NORMAL =
|
||||
ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, ComputerFamily.NORMAL));
|
||||
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_ADVANCED =
|
||||
ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, ComputerFamily.ADVANCED));
|
||||
|
||||
public static final RegistryEntry<BlockEntityType<SpeakerBlockEntity>> SPEAKER =
|
||||
ofBlock(Blocks.SPEAKER, (p, s) -> new SpeakerBlockEntity(BlockEntities.SPEAKER.get(), p, s));
|
||||
public static final RegistryEntry<BlockEntityType<DiskDriveBlockEntity>> DISK_DRIVE =
|
||||
ofBlock(Blocks.DISK_DRIVE, (p, s) -> new DiskDriveBlockEntity(BlockEntities.DISK_DRIVE.get(), p, s));
|
||||
public static final RegistryEntry<BlockEntityType<PrinterBlockEntity>> PRINTER =
|
||||
ofBlock(Blocks.PRINTER, (p, s) -> new PrinterBlockEntity(BlockEntities.PRINTER.get(), p, s));
|
||||
public static final RegistryEntry<BlockEntityType<WiredModemFullBlockEntity>> WIRED_MODEM_FULL =
|
||||
ofBlock(Blocks.WIRED_MODEM_FULL, (p, s) -> new WiredModemFullBlockEntity(BlockEntities.WIRED_MODEM_FULL.get(), p, s));
|
||||
public static final RegistryEntry<BlockEntityType<CableBlockEntity>> CABLE =
|
||||
ofBlock(Blocks.CABLE, (p, s) -> new CableBlockEntity(BlockEntities.CABLE.get(), p, s));
|
||||
|
||||
public static final RegistryEntry<BlockEntityType<WirelessModemBlockEntity>> WIRELESS_MODEM_NORMAL =
|
||||
ofBlock(Blocks.WIRELESS_MODEM_NORMAL, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_NORMAL.get(), p, s, false));
|
||||
public static final RegistryEntry<BlockEntityType<WirelessModemBlockEntity>> WIRELESS_MODEM_ADVANCED =
|
||||
ofBlock(Blocks.WIRELESS_MODEM_ADVANCED, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_ADVANCED.get(), p, s, true));
|
||||
}
|
||||
|
||||
public static final class Items {
|
||||
static final RegistrationHelper<Item> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.ITEM_REGISTRY);
|
||||
|
||||
private static Item.Properties properties() {
|
||||
return new Item.Properties().tab(PlatformHelper.get().getCreativeTab());
|
||||
}
|
||||
|
||||
private static <B extends Block, I extends Item> RegistryEntry<I> ofBlock(RegistryEntry<B> parent, BiFunction<B, Item.Properties, I> supplier) {
|
||||
return REGISTRY.register(parent.id().getPath(), () -> supplier.apply(parent.get(), properties()));
|
||||
}
|
||||
|
||||
public static final RegistryEntry<ComputerItem> COMPUTER_NORMAL = ofBlock(Blocks.COMPUTER_NORMAL, ComputerItem::new);
|
||||
public static final RegistryEntry<ComputerItem> COMPUTER_ADVANCED = ofBlock(Blocks.COMPUTER_ADVANCED, ComputerItem::new);
|
||||
public static final RegistryEntry<ComputerItem> COMPUTER_COMMAND = ofBlock(Blocks.COMPUTER_COMMAND, ComputerItem::new);
|
||||
|
||||
public static final RegistryEntry<PocketComputerItem> POCKET_COMPUTER_NORMAL = REGISTRY.register("pocket_computer_normal",
|
||||
() -> new PocketComputerItem(properties().stacksTo(1), ComputerFamily.NORMAL));
|
||||
public static final RegistryEntry<PocketComputerItem> POCKET_COMPUTER_ADVANCED = REGISTRY.register("pocket_computer_advanced",
|
||||
() -> new PocketComputerItem(properties().stacksTo(1), ComputerFamily.ADVANCED));
|
||||
|
||||
public static final RegistryEntry<TurtleItem> TURTLE_NORMAL = ofBlock(Blocks.TURTLE_NORMAL, TurtleItem::new);
|
||||
public static final RegistryEntry<TurtleItem> TURTLE_ADVANCED = ofBlock(Blocks.TURTLE_ADVANCED, TurtleItem::new);
|
||||
|
||||
public static final RegistryEntry<DiskItem> DISK =
|
||||
REGISTRY.register("disk", () -> new DiskItem(properties().stacksTo(1)));
|
||||
public static final RegistryEntry<ItemTreasureDisk> TREASURE_DISK =
|
||||
REGISTRY.register("treasure_disk", () -> new ItemTreasureDisk(properties().stacksTo(1)));
|
||||
|
||||
public static final RegistryEntry<PrintoutItem> PRINTED_PAGE = REGISTRY.register("printed_page",
|
||||
() -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGE));
|
||||
public static final RegistryEntry<PrintoutItem> PRINTED_PAGES = REGISTRY.register("printed_pages",
|
||||
() -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGES));
|
||||
public static final RegistryEntry<PrintoutItem> PRINTED_BOOK = REGISTRY.register("printed_book",
|
||||
() -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.BOOK));
|
||||
|
||||
public static final RegistryEntry<BlockItem> SPEAKER = ofBlock(Blocks.SPEAKER, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> DISK_DRIVE = ofBlock(Blocks.DISK_DRIVE, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> PRINTER = ofBlock(Blocks.PRINTER, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> MONITOR_NORMAL = ofBlock(Blocks.MONITOR_NORMAL, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> MONITOR_ADVANCED = ofBlock(Blocks.MONITOR_ADVANCED, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> WIRELESS_MODEM_NORMAL = ofBlock(Blocks.WIRELESS_MODEM_NORMAL, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> WIRELESS_MODEM_ADVANCED = ofBlock(Blocks.WIRELESS_MODEM_ADVANCED, BlockItem::new);
|
||||
public static final RegistryEntry<BlockItem> WIRED_MODEM_FULL = ofBlock(Blocks.WIRED_MODEM_FULL, BlockItem::new);
|
||||
|
||||
public static final RegistryEntry<CableBlockItem.Cable> CABLE = REGISTRY.register("cable",
|
||||
() -> new CableBlockItem.Cable(Blocks.CABLE.get(), properties()));
|
||||
public static final RegistryEntry<CableBlockItem.WiredModem> WIRED_MODEM = REGISTRY.register("wired_modem",
|
||||
() -> new CableBlockItem.WiredModem(Blocks.CABLE.get(), properties()));
|
||||
}
|
||||
|
||||
public static class TurtleSerialisers {
|
||||
static final RegistrationHelper<TurtleUpgradeSerialiser<?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(TurtleUpgradeSerialiser.REGISTRY_ID);
|
||||
|
||||
public static final RegistryEntry<TurtleUpgradeSerialiser<TurtleSpeaker>> SPEAKER =
|
||||
REGISTRY.register("speaker", () -> TurtleUpgradeSerialiser.simpleWithCustomItem(TurtleSpeaker::new));
|
||||
public static final RegistryEntry<TurtleUpgradeSerialiser<TurtleCraftingTable>> WORKBENCH =
|
||||
REGISTRY.register("workbench", () -> TurtleUpgradeSerialiser.simpleWithCustomItem(TurtleCraftingTable::new));
|
||||
public static final RegistryEntry<TurtleUpgradeSerialiser<TurtleModem>> WIRELESS_MODEM_NORMAL =
|
||||
REGISTRY.register("wireless_modem_normal", () -> TurtleUpgradeSerialiser.simpleWithCustomItem((id, item) -> new TurtleModem(id, item, false)));
|
||||
public static final RegistryEntry<TurtleUpgradeSerialiser<TurtleModem>> WIRELESS_MODEM_ADVANCED =
|
||||
REGISTRY.register("wireless_modem_advanced", () -> TurtleUpgradeSerialiser.simpleWithCustomItem((id, item) -> new TurtleModem(id, item, true)));
|
||||
|
||||
public static final RegistryEntry<TurtleUpgradeSerialiser<TurtleTool>> TOOL = REGISTRY.register("tool", () -> TurtleToolSerialiser.INSTANCE);
|
||||
}
|
||||
|
||||
public static class PocketUpgradeSerialisers {
|
||||
static final RegistrationHelper<PocketUpgradeSerialiser<?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(PocketUpgradeSerialiser.REGISTRY_ID);
|
||||
|
||||
public static final RegistryEntry<PocketUpgradeSerialiser<PocketSpeaker>> SPEAKER =
|
||||
REGISTRY.register("speaker", () -> PocketUpgradeSerialiser.simpleWithCustomItem(PocketSpeaker::new));
|
||||
public static final RegistryEntry<PocketUpgradeSerialiser<PocketModem>> WIRELESS_MODEM_NORMAL =
|
||||
REGISTRY.register("wireless_modem_normal", () -> PocketUpgradeSerialiser.simpleWithCustomItem((id, item) -> new PocketModem(id, item, false)));
|
||||
public static final RegistryEntry<PocketUpgradeSerialiser<PocketModem>> WIRELESS_MODEM_ADVANCED =
|
||||
REGISTRY.register("wireless_modem_advanced", () -> PocketUpgradeSerialiser.simpleWithCustomItem((id, item) -> new PocketModem(id, item, true)));
|
||||
}
|
||||
|
||||
public static class Menus {
|
||||
static final RegistrationHelper<MenuType<?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.MENU_REGISTRY);
|
||||
|
||||
public static final RegistryEntry<MenuType<ComputerMenuWithoutInventory>> COMPUTER = REGISTRY.register("computer",
|
||||
() -> ContainerData.toType(ComputerContainerData::new, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.COMPUTER.get(), id, inv, data)));
|
||||
|
||||
public static final RegistryEntry<MenuType<ComputerMenuWithoutInventory>> POCKET_COMPUTER = REGISTRY.register("pocket_computer",
|
||||
() -> ContainerData.toType(ComputerContainerData::new, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.POCKET_COMPUTER.get(), id, inv, data)));
|
||||
|
||||
public static final RegistryEntry<MenuType<ComputerMenuWithoutInventory>> POCKET_COMPUTER_NO_TERM = REGISTRY.register("pocket_computer_no_term",
|
||||
() -> ContainerData.toType(ComputerContainerData::new, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.POCKET_COMPUTER_NO_TERM.get(), id, inv, data)));
|
||||
|
||||
public static final RegistryEntry<MenuType<TurtleMenu>> TURTLE = REGISTRY.register("turtle",
|
||||
() -> ContainerData.toType(ComputerContainerData::new, TurtleMenu::ofMenuData));
|
||||
|
||||
public static final RegistryEntry<MenuType<DiskDriveMenu>> DISK_DRIVE = REGISTRY.register("disk_drive",
|
||||
() -> new MenuType<>(DiskDriveMenu::new));
|
||||
|
||||
public static final RegistryEntry<MenuType<PrinterMenu>> PRINTER = REGISTRY.register("printer",
|
||||
() -> new MenuType<>(PrinterMenu::new));
|
||||
|
||||
public static final RegistryEntry<MenuType<HeldItemMenu>> PRINTOUT = REGISTRY.register("printout",
|
||||
() -> ContainerData.toType(HeldItemContainerData::new, HeldItemMenu::createPrintout));
|
||||
|
||||
public static final RegistryEntry<MenuType<ViewComputerMenu>> VIEW_COMPUTER = REGISTRY.register("view_computer",
|
||||
() -> ContainerData.toType(ComputerContainerData::new, ViewComputerMenu::new));
|
||||
}
|
||||
|
||||
static class ArgumentTypes {
|
||||
static final RegistrationHelper<ArgumentTypeInfo<?, ?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.COMMAND_ARGUMENT_TYPE_REGISTRY);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends ArgumentType<?>> void registerUnsafe(String name, Class<T> type, ArgumentTypeInfo<?, ?> serializer) {
|
||||
REGISTRY.register(name, () -> PlatformHelper.get().registerArgumentTypeInfo(type, (ArgumentTypeInfo<T, ?>) serializer));
|
||||
}
|
||||
|
||||
private static <T extends ArgumentType<?>> void register(String name, Class<T> type, ArgumentTypeInfo<T, ?> serializer) {
|
||||
REGISTRY.register(name, () -> PlatformHelper.get().registerArgumentTypeInfo(type, serializer));
|
||||
}
|
||||
|
||||
private static <T extends ArgumentType<?>> void register(String name, Class<T> type, T instance) {
|
||||
register(name, type, SingletonArgumentInfo.contextFree(() -> instance));
|
||||
}
|
||||
|
||||
static {
|
||||
register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
|
||||
register("computer", ComputerArgumentType.class, ComputerArgumentType.oneComputer());
|
||||
register("computers", ComputersArgumentType.class, new ComputersArgumentType.Info());
|
||||
registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.Info());
|
||||
}
|
||||
}
|
||||
|
||||
public static class LootItemConditionTypes {
|
||||
static final RegistrationHelper<LootItemConditionType> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.LOOT_ITEM_REGISTRY);
|
||||
|
||||
public static final RegistryEntry<LootItemConditionType> BLOCK_NAMED = REGISTRY.register("block_named",
|
||||
() -> ConstantLootConditionSerializer.type(BlockNamedEntityLootCondition.INSTANCE));
|
||||
|
||||
public static final RegistryEntry<LootItemConditionType> PLAYER_CREATIVE = REGISTRY.register("player_creative",
|
||||
() -> ConstantLootConditionSerializer.type(PlayerCreativeLootCondition.INSTANCE));
|
||||
|
||||
public static final RegistryEntry<LootItemConditionType> HAS_ID = REGISTRY.register("has_id",
|
||||
() -> ConstantLootConditionSerializer.type(HasComputerIdLootCondition.INSTANCE));
|
||||
}
|
||||
|
||||
public static class RecipeSerializers {
|
||||
static final RegistrationHelper<RecipeSerializer<?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registry.RECIPE_SERIALIZER_REGISTRY);
|
||||
|
||||
private static <T extends CustomRecipe> RegistryEntry<SimpleRecipeSerializer<T>> simple(String name, Function<ResourceLocation, T> factory) {
|
||||
return REGISTRY.register(name, () -> new SimpleRecipeSerializer<>(factory));
|
||||
}
|
||||
|
||||
public static final RegistryEntry<SimpleRecipeSerializer<ColourableRecipe>> DYEABLE_ITEM = simple("colour", ColourableRecipe::new);
|
||||
public static final RegistryEntry<TurtleRecipe.Serializer> TURTLE = REGISTRY.register("turtle", TurtleRecipe.Serializer::new);
|
||||
public static final RegistryEntry<SimpleRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
|
||||
public static final RegistryEntry<SimpleRecipeSerializer<PocketComputerUpgradeRecipe>> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new);
|
||||
public static final RegistryEntry<SimpleRecipeSerializer<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
|
||||
public static final RegistryEntry<SimpleRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::new);
|
||||
public static final RegistryEntry<ComputerUpgradeRecipe.Serializer> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
|
||||
public static final RegistryEntry<ImpostorRecipe.Serializer> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", ImpostorRecipe.Serializer::new);
|
||||
public static final RegistryEntry<ImpostorShapelessRecipe.Serializer> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any objects which don't have to be done on the main thread.
|
||||
*/
|
||||
public static void register() {
|
||||
Blocks.REGISTRY.register();
|
||||
BlockEntities.REGISTRY.register();
|
||||
Items.REGISTRY.register();
|
||||
TurtleSerialisers.REGISTRY.register();
|
||||
PocketUpgradeSerialisers.REGISTRY.register();
|
||||
Menus.REGISTRY.register();
|
||||
ArgumentTypes.REGISTRY.register();
|
||||
LootItemConditionTypes.REGISTRY.register();
|
||||
RecipeSerializers.REGISTRY.register();
|
||||
|
||||
// Register bundled power providers
|
||||
ComputerCraftAPI.registerBundledRedstoneProvider(new DefaultBundledRedstoneProvider());
|
||||
ComputerCraftAPI.registerRefuelHandler(new FurnaceRefuelHandler());
|
||||
ComputerCraftAPI.registerMediaProvider(stack -> {
|
||||
var item = stack.getItem();
|
||||
if (item instanceof IMedia media) return media;
|
||||
if (item instanceof RecordItem) return RecordMedia.INSTANCE;
|
||||
return null;
|
||||
});
|
||||
|
||||
VanillaDetailRegistries.ITEM_STACK.addProvider(ItemDetails::fill);
|
||||
VanillaDetailRegistries.BLOCK_IN_WORLD.addProvider(BlockDetails::fill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any objects which must be done on the main thread.
|
||||
*/
|
||||
public static void registerMainThread() {
|
||||
CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
|
||||
CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.shared.command.CommandUtils.isPlayer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.*;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
|
||||
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
|
||||
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.metric;
|
||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
|
||||
import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
|
||||
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.*;
|
||||
import static net.minecraft.commands.Commands.literal;
|
||||
|
||||
public final class CommandComputerCraft {
|
||||
public static final UUID SYSTEM_UUID = new UUID(0, 0);
|
||||
public static final String OPEN_COMPUTER = "/computercraft open-computer ";
|
||||
|
||||
private CommandComputerCraft() {
|
||||
}
|
||||
|
||||
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(choice("computercraft")
|
||||
.then(literal("dump")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.executes(context -> {
|
||||
var table = new TableBuilder("DumpAll", "Computer", "On", "Position");
|
||||
|
||||
var source = context.getSource();
|
||||
List<ServerComputer> computers = new ArrayList<>(ServerContext.get(source.getServer()).registry().getComputers());
|
||||
|
||||
// Unless we're on a server, limit the number of rows we can send.
|
||||
Level world = source.getLevel();
|
||||
var pos = new BlockPos(source.getPosition());
|
||||
|
||||
computers.sort((a, b) -> {
|
||||
if (a.getLevel() == b.getLevel() && a.getLevel() == world) {
|
||||
return Double.compare(a.getPosition().distSqr(pos), b.getPosition().distSqr(pos));
|
||||
} else if (a.getLevel() == world) {
|
||||
return -1;
|
||||
} else if (b.getLevel() == world) {
|
||||
return 1;
|
||||
} else {
|
||||
return Integer.compare(a.getInstanceID(), b.getInstanceID());
|
||||
}
|
||||
});
|
||||
|
||||
for (var computer : computers) {
|
||||
table.row(
|
||||
linkComputer(source, computer, computer.getID()),
|
||||
bool(computer.isOn()),
|
||||
linkPosition(source, computer)
|
||||
);
|
||||
}
|
||||
|
||||
table.display(context.getSource());
|
||||
return computers.size();
|
||||
})
|
||||
.then(args()
|
||||
.arg("computer", oneComputer())
|
||||
.executes(context -> {
|
||||
var computer = getComputerArgument(context, "computer");
|
||||
|
||||
var table = new TableBuilder("Dump");
|
||||
table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
|
||||
table.row(header("Id"), text(Integer.toString(computer.getID())));
|
||||
table.row(header("Label"), text(computer.getLabel()));
|
||||
table.row(header("On"), bool(computer.isOn()));
|
||||
table.row(header("Position"), linkPosition(context.getSource(), computer));
|
||||
table.row(header("Family"), text(computer.getFamily().toString()));
|
||||
|
||||
for (var side : ComputerSide.values()) {
|
||||
var peripheral = computer.getPeripheral(side);
|
||||
if (peripheral != null) {
|
||||
table.row(header("Peripheral " + side.getName()), text(peripheral.getType()));
|
||||
}
|
||||
}
|
||||
|
||||
table.display(context.getSource());
|
||||
return 1;
|
||||
})))
|
||||
|
||||
.then(command("shutdown")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.executes((context, computerSelectors) -> {
|
||||
var shutdown = 0;
|
||||
var computers = unwrap(context.getSource(), computerSelectors);
|
||||
for (var computer : computers) {
|
||||
if (computer.isOn()) shutdown++;
|
||||
computer.shutdown();
|
||||
}
|
||||
context.getSource().sendSuccess(translate("commands.computercraft.shutdown.done", shutdown, computers.size()), false);
|
||||
return shutdown;
|
||||
}))
|
||||
|
||||
.then(command("turn-on")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.executes((context, computerSelectors) -> {
|
||||
var on = 0;
|
||||
var computers = unwrap(context.getSource(), computerSelectors);
|
||||
for (var computer : computers) {
|
||||
if (!computer.isOn()) on++;
|
||||
computer.turnOn();
|
||||
}
|
||||
context.getSource().sendSuccess(translate("commands.computercraft.turn_on.done", on, computers.size()), false);
|
||||
return on;
|
||||
}))
|
||||
|
||||
.then(command("tp")
|
||||
.requires(UserLevel.OP)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(context -> {
|
||||
var computer = getComputerArgument(context, "computer");
|
||||
var world = computer.getLevel();
|
||||
var pos = computer.getPosition();
|
||||
|
||||
var entity = context.getSource().getEntityOrException();
|
||||
if (!(entity instanceof ServerPlayer player)) throw TP_NOT_PLAYER.create();
|
||||
|
||||
if (player.getCommandSenderWorld() == world) {
|
||||
player.connection.teleport(
|
||||
pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0,
|
||||
EnumSet.noneOf(ClientboundPlayerPositionPacket.RelativeArgument.class)
|
||||
);
|
||||
} else {
|
||||
player.teleportTo(world,
|
||||
pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}))
|
||||
|
||||
.then(command("queue")
|
||||
.requires(UserLevel.ANYONE)
|
||||
.arg("computer", manyComputers())
|
||||
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
|
||||
.executes((ctx, args) -> {
|
||||
var computers = getComputersArgument(ctx, "computer");
|
||||
var rest = args.toArray();
|
||||
|
||||
var queued = 0;
|
||||
for (var computer : computers) {
|
||||
if (computer.getFamily() == ComputerFamily.COMMAND && computer.isOn()) {
|
||||
computer.queueEvent("computer_command", rest);
|
||||
queued++;
|
||||
}
|
||||
}
|
||||
|
||||
return queued;
|
||||
}))
|
||||
|
||||
.then(command("view")
|
||||
.requires(UserLevel.OP)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(context -> {
|
||||
var player = context.getSource().getPlayerOrException();
|
||||
var computer = getComputerArgument(context, "computer");
|
||||
new ComputerContainerData(computer, ItemStack.EMPTY).open(player, new MenuProvider() {
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return Component.translatable("gui.computercraft.view_computer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
|
||||
return new ViewComputerMenu(id, player, computer);
|
||||
}
|
||||
});
|
||||
return 1;
|
||||
}))
|
||||
|
||||
.then(choice("track")
|
||||
.then(command("start")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.executes(context -> {
|
||||
getMetricsInstance(context.getSource()).start();
|
||||
|
||||
var stopCommand = "/computercraft track stop";
|
||||
context.getSource().sendSuccess(translate("commands.computercraft.track.start.stop",
|
||||
link(text(stopCommand), stopCommand, translate("commands.computercraft.track.stop.action"))), false);
|
||||
return 1;
|
||||
}))
|
||||
|
||||
.then(command("stop")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.executes(context -> {
|
||||
var timings = getMetricsInstance(context.getSource());
|
||||
if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create();
|
||||
displayTimings(context.getSource(), timings.getSnapshot(), new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG), DEFAULT_FIELDS);
|
||||
return 1;
|
||||
}))
|
||||
|
||||
.then(command("dump")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.argManyValue("fields", metric(), DEFAULT_FIELDS)
|
||||
.executes((context, fields) -> {
|
||||
AggregatedMetric sort;
|
||||
if (fields.size() == 1 && DEFAULT_FIELDS.contains(fields.get(0))) {
|
||||
sort = fields.get(0);
|
||||
fields = DEFAULT_FIELDS;
|
||||
} else {
|
||||
sort = fields.get(0);
|
||||
}
|
||||
|
||||
return displayTimings(context.getSource(), sort, fields);
|
||||
})))
|
||||
);
|
||||
}
|
||||
|
||||
private static Component linkComputer(CommandSourceStack source, @Nullable ServerComputer serverComputer, int computerId) {
|
||||
var out = Component.literal("");
|
||||
|
||||
// Append the computer instance
|
||||
if (serverComputer == null) {
|
||||
out.append(text("?"));
|
||||
} else {
|
||||
out.append(link(
|
||||
text(Integer.toString(serverComputer.getInstanceID())),
|
||||
"/computercraft dump " + serverComputer.getInstanceID(),
|
||||
translate("commands.computercraft.dump.action")
|
||||
));
|
||||
}
|
||||
|
||||
// And ID
|
||||
out.append(" (id " + computerId + ")");
|
||||
|
||||
// And, if we're a player, some useful links
|
||||
if (serverComputer != null && UserLevel.OP.test(source) && isPlayer(source)) {
|
||||
out
|
||||
.append(" ")
|
||||
.append(link(
|
||||
text("\u261b"),
|
||||
"/computercraft tp " + serverComputer.getInstanceID(),
|
||||
translate("commands.computercraft.tp.action")
|
||||
))
|
||||
.append(" ")
|
||||
.append(link(
|
||||
text("\u20e2"),
|
||||
"/computercraft view " + serverComputer.getInstanceID(),
|
||||
translate("commands.computercraft.view.action")
|
||||
));
|
||||
}
|
||||
|
||||
if (UserLevel.OWNER.test(source) && isPlayer(source)) {
|
||||
var linkPath = linkStorage(source, computerId);
|
||||
if (linkPath != null) out.append(" ").append(linkPath);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Component linkPosition(CommandSourceStack context, ServerComputer computer) {
|
||||
if (UserLevel.OP.test(context)) {
|
||||
return link(
|
||||
position(computer.getPosition()),
|
||||
"/computercraft tp " + computer.getInstanceID(),
|
||||
translate("commands.computercraft.tp.action")
|
||||
);
|
||||
} else {
|
||||
return position(computer.getPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Component linkStorage(CommandSourceStack source, int id) {
|
||||
var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id);
|
||||
if (!file.isDirectory()) return null;
|
||||
|
||||
return link(
|
||||
text("\u270E"),
|
||||
OPEN_COMPUTER + id,
|
||||
translate("commands.computercraft.dump.open_path")
|
||||
);
|
||||
}
|
||||
|
||||
private static BasicComputerMetricsObserver getMetricsInstance(CommandSourceStack source) {
|
||||
var entity = source.getEntity();
|
||||
return ServerContext.get(source.getServer()).metrics().getMetricsInstance(entity instanceof Player ? entity.getUUID() : SYSTEM_UUID);
|
||||
}
|
||||
|
||||
private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
|
||||
new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.COUNT),
|
||||
new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.NONE),
|
||||
new AggregatedMetric(Metrics.COMPUTER_TASKS, Aggregate.AVG)
|
||||
);
|
||||
|
||||
private static int displayTimings(CommandSourceStack source, AggregatedMetric sortField, List<AggregatedMetric> fields) throws CommandSyntaxException {
|
||||
return displayTimings(source, getMetricsInstance(source).getTimings(), sortField, fields);
|
||||
}
|
||||
|
||||
private static int displayTimings(CommandSourceStack source, List<ComputerMetrics> timings, AggregatedMetric sortField, List<AggregatedMetric> fields) throws CommandSyntaxException {
|
||||
if (timings.isEmpty()) throw NO_TIMINGS_EXCEPTION.create();
|
||||
|
||||
timings.sort(Comparator.<ComputerMetrics, Long>comparing(x -> x.get(sortField.metric(), sortField.aggregate())).reversed());
|
||||
|
||||
var headers = new Component[1 + fields.size()];
|
||||
headers[0] = translate("commands.computercraft.track.dump.computer");
|
||||
for (var i = 0; i < fields.size(); i++) headers[i + 1] = fields.get(i).displayName();
|
||||
var table = new TableBuilder("Metrics", headers);
|
||||
|
||||
for (var entry : timings) {
|
||||
var serverComputer = entry.computer();
|
||||
|
||||
var computerComponent = linkComputer(source, serverComputer, entry.computerId());
|
||||
|
||||
var row = new Component[1 + fields.size()];
|
||||
row[0] = computerComponent;
|
||||
for (var i = 0; i < fields.size(); i++) {
|
||||
var metric = fields.get(i);
|
||||
row[i + 1] = text(entry.getFormatted(metric.metric(), metric.aggregate()));
|
||||
}
|
||||
table.row(row);
|
||||
}
|
||||
|
||||
table.display(source);
|
||||
return timings.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class CommandUtils {
|
||||
private CommandUtils() {
|
||||
}
|
||||
|
||||
public static boolean isPlayer(CommandSourceStack output) {
|
||||
var sender = output.getEntity();
|
||||
return sender instanceof ServerPlayer player && !PlatformHelper.get().isFakePlayer(player);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static CompletableFuture<Suggestions> suggestOnServer(CommandContext<?> context, Function<CommandContext<CommandSourceStack>, CompletableFuture<Suggestions>> supplier) {
|
||||
var source = context.getSource();
|
||||
if (!(source instanceof SharedSuggestionProvider)) {
|
||||
return Suggestions.empty();
|
||||
} else if (source instanceof CommandSourceStack) {
|
||||
return supplier.apply((CommandContext<CommandSourceStack>) context);
|
||||
} else {
|
||||
return ((SharedSuggestionProvider) source).customSuggestion(context);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> CompletableFuture<Suggestions> suggest(SuggestionsBuilder builder, Iterable<T> candidates, Function<T, String> toString) {
|
||||
var remaining = builder.getRemaining().toLowerCase(Locale.ROOT);
|
||||
for (var choice : candidates) {
|
||||
var name = toString.apply(choice);
|
||||
if (!name.toLowerCase(Locale.ROOT).startsWith(remaining)) continue;
|
||||
builder.suggest(name);
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
public static <T> CompletableFuture<Suggestions> suggest(SuggestionsBuilder builder, T[] candidates, Function<T, String> toString) {
|
||||
return suggest(builder, Arrays.asList(candidates), toString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command;
|
||||
|
||||
import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class Exceptions {
|
||||
public static final DynamicCommandExceptionType COMPUTER_ARG_NONE = translated1("argument.computercraft.computer.no_matching");
|
||||
public static final Dynamic2CommandExceptionType COMPUTER_ARG_MANY = translated2("argument.computercraft.computer.many_matching");
|
||||
|
||||
public static final DynamicCommandExceptionType TRACKING_FIELD_ARG_NONE = translated1("argument.computercraft.tracking_field.no_field");
|
||||
|
||||
static final SimpleCommandExceptionType NOT_TRACKING_EXCEPTION = translated("commands.computercraft.track.stop.not_enabled");
|
||||
static final SimpleCommandExceptionType NO_TIMINGS_EXCEPTION = translated("commands.computercraft.track.dump.no_timings");
|
||||
|
||||
static final SimpleCommandExceptionType TP_NOT_THERE = translated("commands.computercraft.tp.not_there");
|
||||
static final SimpleCommandExceptionType TP_NOT_PLAYER = translated("commands.computercraft.tp.not_player");
|
||||
|
||||
public static final SimpleCommandExceptionType ARGUMENT_EXPECTED = translated("argument.computercraft.argument_expected");
|
||||
|
||||
private static SimpleCommandExceptionType translated(String key) {
|
||||
return new SimpleCommandExceptionType(Component.translatable(key));
|
||||
}
|
||||
|
||||
private static DynamicCommandExceptionType translated1(String key) {
|
||||
return new DynamicCommandExceptionType(x -> Component.translatable(key, x));
|
||||
}
|
||||
|
||||
private static Dynamic2CommandExceptionType translated2(String key) {
|
||||
return new Dynamic2CommandExceptionType((x, y) -> Component.translatable(key, x, y));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* The level a user must be at in order to execute a command.
|
||||
*/
|
||||
public enum UserLevel implements Predicate<CommandSourceStack> {
|
||||
/**
|
||||
* Only can be used by the owner of the server: namely the server console or the player in SSP.
|
||||
*/
|
||||
OWNER,
|
||||
|
||||
/**
|
||||
* Can only be used by ops.
|
||||
*/
|
||||
OP,
|
||||
|
||||
/**
|
||||
* Can be used by any op, or the player in SSP.
|
||||
*/
|
||||
OWNER_OP,
|
||||
|
||||
/**
|
||||
* Can be used by anyone.
|
||||
*/
|
||||
ANYONE;
|
||||
|
||||
public int toLevel() {
|
||||
return switch (this) {
|
||||
case OWNER -> 4;
|
||||
case OP, OWNER_OP -> 2;
|
||||
case ANYONE -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(CommandSourceStack source) {
|
||||
if (this == ANYONE) return true;
|
||||
if (this == OWNER) return isOwner(source);
|
||||
if (this == OWNER_OP && isOwner(source)) return true;
|
||||
return source.hasPermission(toLevel());
|
||||
}
|
||||
|
||||
private static boolean isOwner(CommandSourceStack source) {
|
||||
var server = source.getServer();
|
||||
var sender = source.getEntity();
|
||||
return server.isDedicatedServer()
|
||||
? source.getEntity() == null && source.hasPermission(4) && source.getTextName().equals("Server")
|
||||
: sender instanceof Player player && player.getGameProfile().getName().equalsIgnoreCase(server.getServerModName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import dan200.computercraft.shared.platform.Registries;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Utilities for working with arguments.
|
||||
*
|
||||
* @see net.minecraft.commands.synchronization.ArgumentUtils
|
||||
*/
|
||||
public class ArgumentUtils {
|
||||
public static <A extends ArgumentType<?>> JsonObject serializeToJson(ArgumentTypeInfo.Template<A> template) {
|
||||
var object = new JsonObject();
|
||||
object.addProperty("type", "argument");
|
||||
object.addProperty("parser", Registries.COMMAND_ARGUMENT_TYPES.getKey(template.type()).toString());
|
||||
|
||||
var properties = new JsonObject();
|
||||
serializeToJson(properties, template.type(), template);
|
||||
if (properties.size() > 0) object.add("properties", properties);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void serializeToJson(JsonObject jsonObject, ArgumentTypeInfo<A, T> argumentTypeInfo, ArgumentTypeInfo.Template<A> template) {
|
||||
argumentTypeInfo.serializeToJson((T) template, jsonObject);
|
||||
}
|
||||
|
||||
public static <A extends ArgumentType<?>> void serializeToNetwork(FriendlyByteBuf buffer, ArgumentTypeInfo.Template<A> template) {
|
||||
serializeToNetwork(buffer, template.type(), template);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void serializeToNetwork(FriendlyByteBuf buffer, ArgumentTypeInfo<A, T> type, ArgumentTypeInfo.Template<A> template) {
|
||||
Registries.writeId(buffer, Registries.COMMAND_ARGUMENT_TYPES, type);
|
||||
type.serializeToNetwork((T) template, buffer);
|
||||
}
|
||||
|
||||
public static ArgumentTypeInfo.Template<?> deserialize(FriendlyByteBuf buffer) {
|
||||
var type = Registries.readId(buffer, Registries.COMMAND_ARGUMENT_TYPES);
|
||||
Objects.requireNonNull(type, "Unknown argument type");
|
||||
return type.deserializeFromNetwork(buffer);
|
||||
}
|
||||
|
||||
public static Component getMessage(Message message) {
|
||||
return message instanceof Component component ? component : Component.literal(message.getString());
|
||||
}
|
||||
|
||||
public static Component getMessage(SimpleCommandExceptionType exception) {
|
||||
return getMessage(exception.create().getRawMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.mojang.brigadier.Message;
|
||||
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.DynamicCommandExceptionType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class ChoiceArgumentType<T> implements ArgumentType<T> {
|
||||
private final Iterable<T> choices;
|
||||
private final Function<T, String> name;
|
||||
private final Function<T, Message> tooltip;
|
||||
private final DynamicCommandExceptionType exception;
|
||||
|
||||
protected ChoiceArgumentType(Iterable<T> choices, Function<T, String> name, Function<T, Message> tooltip, DynamicCommandExceptionType exception) {
|
||||
this.choices = choices;
|
||||
this.name = name;
|
||||
this.tooltip = tooltip;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var name = reader.readUnquotedString();
|
||||
|
||||
for (var choice : choices) {
|
||||
var choiceName = this.name.apply(choice);
|
||||
if (name.equals(choiceName)) return choice;
|
||||
}
|
||||
|
||||
reader.setCursor(start);
|
||||
throw exception.createWithContext(reader, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
var remaining = builder.getRemaining().toLowerCase(Locale.ROOT);
|
||||
for (var choice : choices) {
|
||||
var name = this.name.apply(choice);
|
||||
if (!name.toLowerCase(Locale.ROOT).startsWith(remaining)) continue;
|
||||
builder.suggest(name, tooltip.apply(choice));
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
List<String> items = choices instanceof Collection<?> ? new ArrayList<>(((Collection<T>) choices).size()) : new ArrayList<>();
|
||||
for (var choice : choices) items.add(name.apply(choice));
|
||||
items.sort(Comparator.naturalOrder());
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
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.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_MANY;
|
||||
|
||||
public final class ComputerArgumentType implements ArgumentType<ComputerArgumentType.ComputerSupplier> {
|
||||
private static final ComputerArgumentType INSTANCE = new ComputerArgumentType();
|
||||
|
||||
public static ComputerArgumentType oneComputer() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static ServerComputer getComputerArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputerSupplier.class).unwrap(context.getSource());
|
||||
}
|
||||
|
||||
private ComputerArgumentType() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputerSupplier parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var supplier = ComputersArgumentType.someComputers().parse(reader);
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
|
||||
return s -> {
|
||||
var computers = supplier.unwrap(s);
|
||||
|
||||
if (computers.size() == 1) return computers.iterator().next();
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var first = true;
|
||||
for (var computer : computers) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(computer.getInstanceID());
|
||||
}
|
||||
|
||||
|
||||
// We have an incorrect number of computers: reset and throw an error
|
||||
reader.setCursor(start);
|
||||
throw COMPUTER_ARG_MANY.createWithContext(reader, selector, builder.toString());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
return ComputersArgumentType.someComputers().listSuggestions(context, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return ComputersArgumentType.someComputers().getExamples();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ComputerSupplier {
|
||||
ServerComputer unwrap(CommandSourceStack source) throws CommandSyntaxException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
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.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggest;
|
||||
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
|
||||
import static dan200.computercraft.shared.command.Exceptions.COMPUTER_ARG_NONE;
|
||||
|
||||
public final class ComputersArgumentType implements ArgumentType<ComputersArgumentType.ComputersSupplier> {
|
||||
private static final ComputersArgumentType MANY = new ComputersArgumentType(false);
|
||||
private static final ComputersArgumentType SOME = new ComputersArgumentType(true);
|
||||
|
||||
private static final List<String> EXAMPLES = Arrays.asList(
|
||||
"0", "#0", "@Label", "~Advanced"
|
||||
);
|
||||
|
||||
public static ComputersArgumentType manyComputers() {
|
||||
return MANY;
|
||||
}
|
||||
|
||||
public static ComputersArgumentType someComputers() {
|
||||
return SOME;
|
||||
}
|
||||
|
||||
public static Collection<ServerComputer> getComputersArgument(CommandContext<CommandSourceStack> context, String name) throws CommandSyntaxException {
|
||||
return context.getArgument(name, ComputersSupplier.class).unwrap(context.getSource());
|
||||
}
|
||||
|
||||
private final boolean requireSome;
|
||||
|
||||
private ComputersArgumentType(boolean requireSome) {
|
||||
this.requireSome = requireSome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersSupplier parse(StringReader reader) throws CommandSyntaxException {
|
||||
var start = reader.getCursor();
|
||||
var kind = reader.peek();
|
||||
ComputersSupplier computers;
|
||||
if (kind == '@') {
|
||||
reader.skip();
|
||||
var label = reader.readUnquotedString();
|
||||
computers = getComputers(x -> Objects.equals(label, x.getLabel()));
|
||||
} else if (kind == '~') {
|
||||
reader.skip();
|
||||
var family = reader.readUnquotedString();
|
||||
computers = getComputers(x -> x.getFamily().name().equalsIgnoreCase(family));
|
||||
} else if (kind == '#') {
|
||||
reader.skip();
|
||||
var id = reader.readInt();
|
||||
computers = getComputers(x -> x.getID() == id);
|
||||
} else {
|
||||
var instance = reader.readInt();
|
||||
computers = s -> {
|
||||
var computer = ServerContext.get(s.getServer()).registry().get(instance);
|
||||
return computer == null ? Collections.emptyList() : Collections.singletonList(computer);
|
||||
};
|
||||
}
|
||||
|
||||
if (requireSome) {
|
||||
var selector = reader.getString().substring(start, reader.getCursor());
|
||||
return source -> {
|
||||
var matched = computers.unwrap(source);
|
||||
if (matched.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
|
||||
return matched;
|
||||
};
|
||||
} else {
|
||||
return computers;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
var remaining = builder.getRemaining();
|
||||
|
||||
// We can run this one on the client, for obvious reasons.
|
||||
if (remaining.startsWith("~")) {
|
||||
return suggest(builder, ComputerFamily.values(), x -> "~" + x.name());
|
||||
}
|
||||
|
||||
// Verify we've a command source and we're running on the server
|
||||
return suggestOnServer(context, s -> {
|
||||
if (remaining.startsWith("@")) {
|
||||
suggestComputers(s.getSource(), builder, remaining, x -> {
|
||||
var label = x.getLabel();
|
||||
return label == null ? null : "@" + label;
|
||||
});
|
||||
} else if (remaining.startsWith("#")) {
|
||||
suggestComputers(s.getSource(), builder, remaining, c -> "#" + c.getID());
|
||||
} else {
|
||||
suggestComputers(s.getSource(), builder, remaining, c -> Integer.toString(c.getInstanceID()));
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return EXAMPLES;
|
||||
}
|
||||
|
||||
private static void suggestComputers(CommandSourceStack source, SuggestionsBuilder builder, String remaining, Function<ServerComputer, String> renderer) {
|
||||
remaining = remaining.toLowerCase(Locale.ROOT);
|
||||
for (var computer : ServerContext.get(source.getServer()).registry().getComputers()) {
|
||||
var converted = renderer.apply(computer);
|
||||
if (converted != null && converted.toLowerCase(Locale.ROOT).startsWith(remaining)) {
|
||||
builder.suggest(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ComputersSupplier getComputers(Predicate<ServerComputer> predicate) {
|
||||
return s -> ServerContext.get(s.getServer()).registry()
|
||||
.getComputers()
|
||||
.stream()
|
||||
.filter(predicate)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static class Info implements ArgumentTypeInfo<ComputersArgumentType, Template> {
|
||||
@Override
|
||||
public void serializeToNetwork(ComputersArgumentType.Template arg, FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(arg.requireSome());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
|
||||
var requiresSome = buf.readBoolean();
|
||||
return new ComputersArgumentType.Template(this, requiresSome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeToJson(ComputersArgumentType.Template arg, JsonObject json) {
|
||||
json.addProperty("requireSome", arg.requireSome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputersArgumentType.Template unpack(@NotNull ComputersArgumentType argumentType) {
|
||||
return new ComputersArgumentType.Template(this, argumentType.requireSome);
|
||||
}
|
||||
}
|
||||
|
||||
public record Template(Info info, boolean requireSome) implements ArgumentTypeInfo.Template<ComputersArgumentType> {
|
||||
@Override
|
||||
public ComputersArgumentType instantiate(@NotNull CommandBuildContext context) {
|
||||
return requireSome ? SOME : MANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Info type() {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ComputersSupplier {
|
||||
Collection<ServerComputer> unwrap(CommandSourceStack source) throws CommandSyntaxException;
|
||||
}
|
||||
|
||||
public static Set<ServerComputer> unwrap(CommandSourceStack source, Collection<ComputersSupplier> suppliers) throws CommandSyntaxException {
|
||||
Set<ServerComputer> computers = new HashSet<>();
|
||||
for (var supplier : suppliers) computers.addAll(supplier.unwrap(source));
|
||||
return computers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
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 net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfos;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Reads one argument multiple times.
|
||||
* <p>
|
||||
* Note that this must be the last element in an argument chain: in order to improve the quality of error messages,
|
||||
* we will always try to consume another argument while there is input remaining.
|
||||
* <p>
|
||||
* One problem with how parsers function, is that they must consume some input: and thus we
|
||||
*
|
||||
* @param <T> The type of each value returned
|
||||
* @param <U> The type of the inner parser. This will normally be a {@link List} or {@code T}.
|
||||
*/
|
||||
public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
|
||||
private final ArgumentType<U> child;
|
||||
private final BiConsumer<List<T>, U> appender;
|
||||
private final boolean flatten;
|
||||
private final SimpleCommandExceptionType some;
|
||||
|
||||
private RepeatArgumentType(ArgumentType<U> child, BiConsumer<List<T>, U> appender, boolean flatten, SimpleCommandExceptionType some) {
|
||||
this.child = child;
|
||||
this.appender = appender;
|
||||
this.flatten = flatten;
|
||||
this.some = some;
|
||||
}
|
||||
|
||||
public static <T> RepeatArgumentType<T, T> some(ArgumentType<T> appender, SimpleCommandExceptionType missing) {
|
||||
return new RepeatArgumentType<>(appender, List::add, false, missing);
|
||||
}
|
||||
|
||||
public static <T> RepeatArgumentType<T, List<T>> someFlat(ArgumentType<List<T>> appender, SimpleCommandExceptionType missing) {
|
||||
return new RepeatArgumentType<>(appender, List::addAll, true, missing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> parse(StringReader reader) throws CommandSyntaxException {
|
||||
var hadSome = false;
|
||||
List<T> out = new ArrayList<>();
|
||||
while (true) {
|
||||
reader.skipWhitespace();
|
||||
if (!reader.canRead()) break;
|
||||
|
||||
var startParse = reader.getCursor();
|
||||
appender.accept(out, child.parse(reader));
|
||||
hadSome = true;
|
||||
|
||||
if (reader.getCursor() == startParse) {
|
||||
throw new IllegalStateException(child + " did not consume any input on " + reader.getRemaining());
|
||||
}
|
||||
}
|
||||
|
||||
// Note that each child may return an empty list, we just require that some actual input
|
||||
// was consumed.
|
||||
// We should probably review that this is sensible in the future.
|
||||
if (!hadSome) throw some.createWithContext(reader);
|
||||
|
||||
return Collections.unmodifiableList(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
var reader = new StringReader(builder.getInput());
|
||||
reader.setCursor(builder.getStart());
|
||||
var previous = reader.getCursor();
|
||||
while (reader.canRead()) {
|
||||
try {
|
||||
child.parse(reader);
|
||||
} catch (CommandSyntaxException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
var cursor = reader.getCursor();
|
||||
reader.skipWhitespace();
|
||||
if (cursor == reader.getCursor()) break;
|
||||
previous = reader.getCursor();
|
||||
}
|
||||
|
||||
reader.setCursor(previous);
|
||||
return child.listSuggestions(context, builder.createOffset(previous));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getExamples() {
|
||||
return child.getExamples();
|
||||
}
|
||||
|
||||
public static class Info implements ArgumentTypeInfo<RepeatArgumentType<?, ?>, Template> {
|
||||
@Override
|
||||
public void serializeToNetwork(RepeatArgumentType.Template arg, FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(arg.flatten);
|
||||
ArgumentUtils.serializeToNetwork(buf, arg.child);
|
||||
buf.writeComponent(ArgumentUtils.getMessage(arg.some));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepeatArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
|
||||
var isList = buf.readBoolean();
|
||||
var child = ArgumentUtils.deserialize(buf);
|
||||
var message = buf.readComponent();
|
||||
return new RepeatArgumentType.Template(this, child, isList, new SimpleCommandExceptionType(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepeatArgumentType.Template unpack(RepeatArgumentType<?, ?> argumentType) {
|
||||
return new RepeatArgumentType.Template(this, ArgumentTypeInfos.unpack(argumentType.child), argumentType.flatten, argumentType.some);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeToJson(RepeatArgumentType.Template arg, JsonObject json) {
|
||||
json.addProperty("flatten", arg.flatten);
|
||||
json.add("child", ArgumentUtils.serializeToJson(arg.child));
|
||||
json.addProperty("error", Component.Serializer.toJson(ArgumentUtils.getMessage(arg.some)));
|
||||
}
|
||||
}
|
||||
|
||||
public record Template(
|
||||
Info info, ArgumentTypeInfo.Template<?> child, boolean flatten, SimpleCommandExceptionType some
|
||||
) implements ArgumentTypeInfo.Template<RepeatArgumentType<?, ?>> {
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public RepeatArgumentType<?, ?> instantiate(@NotNull CommandBuildContext commandBuildContext) {
|
||||
var child = child().instantiate(commandBuildContext);
|
||||
return flatten ? RepeatArgumentType.someFlat((ArgumentType) child, some()) : RepeatArgumentType.some(child, some());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArgumentTypeInfo<RepeatArgumentType<?, ?>, ?> type() {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.arguments;
|
||||
|
||||
import dan200.computercraft.shared.command.Exceptions;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
|
||||
public final class TrackingFieldArgumentType extends ChoiceArgumentType<AggregatedMetric> {
|
||||
private static final TrackingFieldArgumentType INSTANCE = new TrackingFieldArgumentType();
|
||||
|
||||
private TrackingFieldArgumentType() {
|
||||
super(
|
||||
AggregatedMetric.aggregatedMetrics().toList(),
|
||||
AggregatedMetric::name, AggregatedMetric::displayName, Exceptions.TRACKING_FIELD_ARG_NONE
|
||||
);
|
||||
}
|
||||
|
||||
public static TrackingFieldArgumentType metric() {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.builder;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
|
||||
/**
|
||||
* A {@link Command} which accepts an argument.
|
||||
*
|
||||
* @param <S> The command source we consume.
|
||||
* @param <T> The argument given to this command when executed.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ArgCommand<S, T> {
|
||||
int run(CommandContext<S> ctx, T arg) throws CommandSyntaxException;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.builder;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static dan200.computercraft.shared.command.Exceptions.ARGUMENT_EXPECTED;
|
||||
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.literal;
|
||||
|
||||
/**
|
||||
* An alternative way of building command nodes, so one does not have to nest.
|
||||
* {@link ArgumentBuilder#then(CommandNode)}s.
|
||||
*
|
||||
* @param <S> The command source we consume.
|
||||
*/
|
||||
public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
private final List<ArgumentBuilder<S, ?>> args = new ArrayList<>();
|
||||
private @Nullable Predicate<S> requires;
|
||||
|
||||
public static CommandBuilder<CommandSourceStack> args() {
|
||||
return new CommandBuilder<>();
|
||||
}
|
||||
|
||||
public static CommandBuilder<CommandSourceStack> command(String literal) {
|
||||
var builder = new CommandBuilder<CommandSourceStack>();
|
||||
builder.args.add(literal(literal));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public CommandBuilder<S> requires(Predicate<S> predicate) {
|
||||
requires = requires == null ? predicate : requires.and(predicate);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandBuilder<S> arg(String name, ArgumentType<?> type) {
|
||||
args.add(RequiredArgumentBuilder.argument(name, type));
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, List<T> empty) {
|
||||
return argMany(name, type, () -> empty);
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, T defaultValue) {
|
||||
return argManyValue(name, type, Collections.singletonList(defaultValue));
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany(String name, ArgumentType<T> type, Supplier<List<T>> empty) {
|
||||
return argMany(name, RepeatArgumentType.some(type, ARGUMENT_EXPECTED), empty);
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyFlatten(String name, ArgumentType<List<T>> type, Supplier<List<T>> empty) {
|
||||
return argMany(name, RepeatArgumentType.someFlat(type, ARGUMENT_EXPECTED), empty);
|
||||
}
|
||||
|
||||
private <T, U> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany(String name, RepeatArgumentType<T, ?> type, Supplier<List<T>> empty) {
|
||||
if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder");
|
||||
|
||||
return command -> {
|
||||
// The node for no arguments
|
||||
var tail = tail(ctx -> command.run(ctx, empty.get()));
|
||||
|
||||
// The node for one or more arguments
|
||||
ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder
|
||||
.<S, List<T>>argument(name, type)
|
||||
.executes(ctx -> command.run(ctx, getList(ctx, name)));
|
||||
|
||||
// Chain all of them together!
|
||||
tail.then(moreArg);
|
||||
return link(tail);
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> List<T> getList(CommandContext<?> context, String name) {
|
||||
return (List<T>) context.getArgument(name, List.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandNode<S> executes(Command<S> command) {
|
||||
if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder");
|
||||
|
||||
return link(tail(command));
|
||||
}
|
||||
|
||||
private ArgumentBuilder<S, ?> tail(Command<S> command) {
|
||||
var defaultTail = args.get(args.size() - 1);
|
||||
defaultTail.executes(command);
|
||||
if (requires != null) defaultTail.requires(requires);
|
||||
return defaultTail;
|
||||
}
|
||||
|
||||
private CommandNode<S> link(ArgumentBuilder<S, ?> tail) {
|
||||
for (var i = args.size() - 2; i >= 0; i--) tail = args.get(i).then(tail);
|
||||
return tail.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.builder;
|
||||
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
|
||||
/**
|
||||
* A builder which generates a {@link CommandNode} from the provided action.
|
||||
*
|
||||
* @param <S> The command source we consume.
|
||||
* @param <T> The type of action to execute when this command is run.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandNodeBuilder<S, T> {
|
||||
/**
|
||||
* Generate a command node which executes this command.
|
||||
*
|
||||
* @param command The command to run
|
||||
* @return The constructed node.
|
||||
*/
|
||||
CommandNode<S> executes(T command);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.builder;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.ClickEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.translate;
|
||||
|
||||
/**
|
||||
* An alternative to {@link LiteralArgumentBuilder} which also provides a {@code /... help} command, and defaults
|
||||
* to that command when no arguments are given.
|
||||
*/
|
||||
public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<CommandSourceStack> {
|
||||
private final Collection<HelpingArgumentBuilder> children = new ArrayList<>();
|
||||
|
||||
private HelpingArgumentBuilder(String literal) {
|
||||
super(literal);
|
||||
}
|
||||
|
||||
public static HelpingArgumentBuilder choice(String literal) {
|
||||
return new HelpingArgumentBuilder(literal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiteralArgumentBuilder<CommandSourceStack> executes(final Command<CommandSourceStack> command) {
|
||||
throw new IllegalStateException("Cannot use executes on a HelpingArgumentBuilder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiteralArgumentBuilder<CommandSourceStack> then(final ArgumentBuilder<CommandSourceStack, ?> argument) {
|
||||
if (getRedirect() != null) throw new IllegalStateException("Cannot add children to a redirected node");
|
||||
|
||||
if (argument instanceof HelpingArgumentBuilder) {
|
||||
children.add((HelpingArgumentBuilder) argument);
|
||||
} else if (argument instanceof LiteralArgumentBuilder) {
|
||||
super.then(argument);
|
||||
} else {
|
||||
throw new IllegalStateException("HelpingArgumentBuilder can only accept literal children");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiteralArgumentBuilder<CommandSourceStack> then(CommandNode<CommandSourceStack> argument) {
|
||||
if (!(argument instanceof LiteralCommandNode)) {
|
||||
throw new IllegalStateException("HelpingArgumentBuilder can only accept literal children");
|
||||
}
|
||||
return super.then(argument);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiteralCommandNode<CommandSourceStack> build() {
|
||||
return buildImpl(getLiteral().replace('-', '_'), getLiteral());
|
||||
}
|
||||
|
||||
private LiteralCommandNode<CommandSourceStack> build(String id, String command) {
|
||||
return buildImpl(id + "." + getLiteral().replace('-', '_'), command + " " + getLiteral());
|
||||
}
|
||||
|
||||
private LiteralCommandNode<CommandSourceStack> buildImpl(String id, String command) {
|
||||
var helpCommand = new HelpCommand(id, command);
|
||||
var node = new LiteralCommandNode<CommandSourceStack>(getLiteral(), helpCommand, getRequirement(), getRedirect(), getRedirectModifier(), isFork());
|
||||
helpCommand.node = node;
|
||||
|
||||
// Set up a /... help command
|
||||
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help")
|
||||
.requires(x -> getArguments().stream().anyMatch(y -> y.getRequirement().test(x)))
|
||||
.executes(helpCommand);
|
||||
|
||||
// Add all normal command children to this and the help node
|
||||
for (var child : getArguments()) {
|
||||
node.addChild(child);
|
||||
|
||||
helpNode.then(LiteralArgumentBuilder.<CommandSourceStack>literal(child.getName())
|
||||
.requires(child.getRequirement())
|
||||
.executes(helpForChild(child, id, command))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
// And add alternative versions of which forward instead
|
||||
for (var childBuilder : children) {
|
||||
var child = childBuilder.build(id, command);
|
||||
node.addChild(child);
|
||||
helpNode.then(LiteralArgumentBuilder.<CommandSourceStack>literal(child.getName())
|
||||
.requires(child.getRequirement())
|
||||
.executes(helpForChild(child, id, command))
|
||||
.redirect(child.getChild("help"))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
node.addChild(helpNode.build());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static final ChatFormatting HEADER = ChatFormatting.LIGHT_PURPLE;
|
||||
private static final ChatFormatting SYNOPSIS = ChatFormatting.AQUA;
|
||||
private static final ChatFormatting NAME = ChatFormatting.GREEN;
|
||||
|
||||
private static final class HelpCommand implements Command<CommandSourceStack> {
|
||||
private final String id;
|
||||
private final String command;
|
||||
@Nullable
|
||||
LiteralCommandNode<CommandSourceStack> node;
|
||||
|
||||
private HelpCommand(String id, String command) {
|
||||
this.id = id;
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int run(CommandContext<CommandSourceStack> context) {
|
||||
context.getSource().sendSuccess(getHelp(context, assertNonNull(node), id, command), false);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Command<CommandSourceStack> helpForChild(CommandNode<CommandSourceStack> node, String id, String command) {
|
||||
return context -> {
|
||||
context.getSource().sendSuccess(getHelp(context, node, id + "." + node.getName().replace('-', '_'), command + " " + node.getName()), false);
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
private static Component getHelp(CommandContext<CommandSourceStack> context, CommandNode<CommandSourceStack> node, String id, String command) {
|
||||
// An ugly hack to extract usage information from the dispatcher. We generate a temporary node, generate
|
||||
// the shorthand usage, and emit that.
|
||||
var dispatcher = context.getSource().getServer().getCommands().getDispatcher();
|
||||
CommandNode<CommandSourceStack> temp = new LiteralCommandNode<>("_", null, x -> true, null, null, false);
|
||||
temp.addChild(node);
|
||||
var usage = assertNonNull(dispatcher.getSmartUsage(temp, context.getSource()).get(node)).substring(node.getName().length());
|
||||
|
||||
var output = Component.literal("")
|
||||
.append(coloured("/" + command + usage, HEADER))
|
||||
.append(" ")
|
||||
.append(coloured(translate("commands." + id + ".synopsis"), SYNOPSIS))
|
||||
.append("\n")
|
||||
.append(translate("commands." + id + ".desc"));
|
||||
|
||||
for (var child : node.getChildren()) {
|
||||
if (!child.getRequirement().test(context.getSource()) || !(child instanceof LiteralCommandNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
output.append("\n");
|
||||
|
||||
var component = coloured(child.getName(), NAME);
|
||||
component.getStyle().withClickEvent(new ClickEvent(
|
||||
ClickEvent.Action.SUGGEST_COMMAND,
|
||||
"/" + command + " " + child.getName()
|
||||
));
|
||||
output.append(component);
|
||||
|
||||
output.append(" - ").append(translate("commands." + id + "." + child.getName() + ".synopsis"));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.text;
|
||||
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.ClickEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.HoverEvent;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Various helpers for building chat messages.
|
||||
*/
|
||||
public final class ChatHelpers {
|
||||
private static final ChatFormatting HEADER = ChatFormatting.LIGHT_PURPLE;
|
||||
|
||||
private ChatHelpers() {
|
||||
}
|
||||
|
||||
public static MutableComponent coloured(@Nullable String text, ChatFormatting colour) {
|
||||
return Component.literal(text == null ? "" : text).withStyle(colour);
|
||||
}
|
||||
|
||||
public static <T extends MutableComponent> T coloured(T component, ChatFormatting colour) {
|
||||
component.withStyle(colour);
|
||||
return component;
|
||||
}
|
||||
|
||||
public static MutableComponent text(@Nullable String text) {
|
||||
return Component.literal(text == null ? "" : text);
|
||||
}
|
||||
|
||||
public static MutableComponent translate(@Nullable String text) {
|
||||
return Component.translatable(text == null ? "" : text);
|
||||
}
|
||||
|
||||
public static MutableComponent translate(@Nullable String text, Object... args) {
|
||||
return Component.translatable(text == null ? "" : text, args);
|
||||
}
|
||||
|
||||
public static MutableComponent list(Component... children) {
|
||||
var component = Component.literal("");
|
||||
for (var child : children) {
|
||||
component.append(child);
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
public static MutableComponent position(@Nullable BlockPos pos) {
|
||||
if (pos == null) return translate("commands.computercraft.generic.no_position");
|
||||
return translate("commands.computercraft.generic.position", pos.getX(), pos.getY(), pos.getZ());
|
||||
}
|
||||
|
||||
public static MutableComponent bool(boolean value) {
|
||||
return value
|
||||
? coloured(translate("commands.computercraft.generic.yes"), ChatFormatting.GREEN)
|
||||
: coloured(translate("commands.computercraft.generic.no"), ChatFormatting.RED);
|
||||
}
|
||||
|
||||
public static Component link(MutableComponent component, String command, Component toolTip) {
|
||||
return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip);
|
||||
}
|
||||
|
||||
public static Component link(Component component, ClickEvent click, Component toolTip) {
|
||||
var style = component.getStyle();
|
||||
|
||||
if (style.getColor() == null) style = style.withColor(ChatFormatting.YELLOW);
|
||||
style = style.withClickEvent(click);
|
||||
style = style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, toolTip));
|
||||
|
||||
return component.copy().withStyle(style);
|
||||
}
|
||||
|
||||
public static MutableComponent header(String text) {
|
||||
return coloured(text, HEADER);
|
||||
}
|
||||
|
||||
public static MutableComponent copy(String text) {
|
||||
var name = Component.literal(text);
|
||||
var style = name.getStyle()
|
||||
.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, text))
|
||||
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")));
|
||||
return name.withStyle(style);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.text;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ServerTableFormatter implements TableFormatter {
|
||||
private final CommandSourceStack source;
|
||||
|
||||
public ServerTableFormatter(CommandSourceStack source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Component getPadding(Component component, int width) {
|
||||
var extraWidth = width - getWidth(component);
|
||||
if (extraWidth <= 0) return null;
|
||||
return Component.literal(StringUtils.repeat(' ', extraWidth));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPadding() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth(Component component) {
|
||||
return component.getString().length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLine(String label, Component component) {
|
||||
source.sendSuccess(component, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.text;
|
||||
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.command.CommandUtils;
|
||||
import dan200.computercraft.shared.network.client.ChatTableClientMessage;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TableBuilder {
|
||||
private final String id;
|
||||
private int columns = -1;
|
||||
private final @Nullable Component[] headers;
|
||||
private final ArrayList<Component[]> rows = new ArrayList<>();
|
||||
private int additional;
|
||||
|
||||
public TableBuilder(String id, Component... headers) {
|
||||
this.id = id;
|
||||
this.headers = headers;
|
||||
columns = headers.length;
|
||||
}
|
||||
|
||||
public TableBuilder(String id) {
|
||||
this.id = id;
|
||||
headers = null;
|
||||
}
|
||||
|
||||
public TableBuilder(String id, String... headers) {
|
||||
this.id = id;
|
||||
this.headers = new Component[headers.length];
|
||||
columns = headers.length;
|
||||
|
||||
for (var i = 0; i < headers.length; i++) this.headers[i] = ChatHelpers.header(headers[i]);
|
||||
}
|
||||
|
||||
public void row(Component... row) {
|
||||
if (columns == -1) columns = row.length;
|
||||
if (row.length != columns) throw new IllegalArgumentException("Row is the incorrect length");
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique identifier for this table type.
|
||||
* <p>
|
||||
* When showing a table within Minecraft, previous instances of this table with
|
||||
* the same ID will be removed from chat.
|
||||
*
|
||||
* @return This table's type.
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of columns for this table.
|
||||
* <p>
|
||||
* This will be the same as {@link #getHeaders()}'s length if it is is non-{@code null},
|
||||
* otherwise the length of the first column.
|
||||
*
|
||||
* @return The number of columns.
|
||||
*/
|
||||
public int getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Component[] getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public List<Component[]> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public int getAdditional() {
|
||||
return additional;
|
||||
}
|
||||
|
||||
public void setAdditional(int additional) {
|
||||
this.additional = additional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim this table to a given height.
|
||||
*
|
||||
* @param height The desired height.
|
||||
*/
|
||||
public void trim(int height) {
|
||||
if (rows.size() > height) {
|
||||
additional += rows.size() - height - 1;
|
||||
rows.subList(height - 1, rows.size()).clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void display(CommandSourceStack source) {
|
||||
if (CommandUtils.isPlayer(source)) {
|
||||
trim(18);
|
||||
var player = (ServerPlayer) Nullability.assertNonNull(source.getEntity());
|
||||
PlatformHelper.get().sendToPlayer(new ChatTableClientMessage(this), player);
|
||||
} else {
|
||||
trim(100);
|
||||
new ServerTableFormatter(source).display(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.command.text;
|
||||
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.translate;
|
||||
|
||||
public interface TableFormatter {
|
||||
Component SEPARATOR = coloured("| ", ChatFormatting.GRAY);
|
||||
Component HEADER = coloured("=", ChatFormatting.GRAY);
|
||||
|
||||
/**
|
||||
* Get additional padding for the component.
|
||||
*
|
||||
* @param component The component to pad
|
||||
* @param width The desired width for the component
|
||||
* @return The padding for this component, or {@code null} if none is needed.
|
||||
*/
|
||||
@Nullable
|
||||
Component getPadding(Component component, int width);
|
||||
|
||||
/**
|
||||
* Get the minimum padding between each column.
|
||||
*
|
||||
* @return The minimum padding.
|
||||
*/
|
||||
int getColumnPadding();
|
||||
|
||||
int getWidth(Component component);
|
||||
|
||||
void writeLine(String label, Component component);
|
||||
|
||||
default void display(TableBuilder table) {
|
||||
if (table.getColumns() <= 0) return;
|
||||
|
||||
var id = table.getId();
|
||||
var columns = table.getColumns();
|
||||
var maxWidths = new int[columns];
|
||||
|
||||
var headers = table.getHeaders();
|
||||
if (headers != null) {
|
||||
for (var i = 0; i < columns; i++) maxWidths[i] = getWidth(headers[i]);
|
||||
}
|
||||
|
||||
for (var row : table.getRows()) {
|
||||
for (var i = 0; i < row.length; i++) {
|
||||
var width = getWidth(row[i]);
|
||||
if (width > maxWidths[i]) maxWidths[i] = width;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a small amount of padding after each column
|
||||
{
|
||||
var padding = getColumnPadding();
|
||||
for (var i = 0; i < maxWidths.length - 1; i++) maxWidths[i] += padding;
|
||||
}
|
||||
|
||||
// And compute the total width
|
||||
var totalWidth = (columns - 1) * getWidth(SEPARATOR);
|
||||
for (var x : maxWidths) totalWidth += x;
|
||||
|
||||
if (headers != null) {
|
||||
var line = Component.literal("");
|
||||
for (var i = 0; i < columns - 1; i++) {
|
||||
line.append(headers[i]);
|
||||
var padding = getPadding(headers[i], maxWidths[i]);
|
||||
if (padding != null) line.append(padding);
|
||||
line.append(SEPARATOR);
|
||||
}
|
||||
line.append(headers[columns - 1]);
|
||||
|
||||
writeLine(id, line);
|
||||
|
||||
// Write a separator line. We round the width up rather than down to make
|
||||
// it a tad prettier.
|
||||
var rowCharWidth = getWidth(HEADER);
|
||||
var rowWidth = totalWidth / rowCharWidth + (totalWidth % rowCharWidth == 0 ? 0 : 1);
|
||||
writeLine(id, coloured(StringUtils.repeat(HEADER.getString(), rowWidth), ChatFormatting.GRAY));
|
||||
}
|
||||
|
||||
for (var row : table.getRows()) {
|
||||
var line = Component.literal("");
|
||||
for (var i = 0; i < columns - 1; i++) {
|
||||
line.append(row[i]);
|
||||
var padding = getPadding(row[i], maxWidths[i]);
|
||||
if (padding != null) line.append(padding);
|
||||
line.append(SEPARATOR);
|
||||
}
|
||||
line.append(row[columns - 1]);
|
||||
writeLine(id, line);
|
||||
}
|
||||
|
||||
if (table.getAdditional() > 0) {
|
||||
writeLine(id, coloured(translate("commands.computercraft.generic.additional_rows", table.getAdditional()), ChatFormatting.AQUA));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.common;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.util.ColourTracker;
|
||||
import dan200.computercraft.shared.util.ColourUtils;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CustomRecipe;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
public final class ColourableRecipe extends CustomRecipe {
|
||||
public ColourableRecipe(ResourceLocation id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingContainer inv, Level world) {
|
||||
var hasColourable = false;
|
||||
var hasDye = false;
|
||||
for (var i = 0; i < inv.getContainerSize(); i++) {
|
||||
var stack = inv.getItem(i);
|
||||
if (stack.isEmpty()) continue;
|
||||
|
||||
if (stack.getItem() instanceof IColouredItem) {
|
||||
if (hasColourable) return false;
|
||||
hasColourable = true;
|
||||
} else if (ColourUtils.getStackColour(stack) != null) {
|
||||
hasDye = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasColourable && hasDye;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingContainer inv) {
|
||||
var colourable = ItemStack.EMPTY;
|
||||
|
||||
var tracker = new ColourTracker();
|
||||
|
||||
for (var i = 0; i < inv.getContainerSize(); i++) {
|
||||
var stack = inv.getItem(i);
|
||||
|
||||
if (stack.isEmpty()) continue;
|
||||
|
||||
if (stack.getItem() instanceof IColouredItem) {
|
||||
colourable = stack;
|
||||
} else {
|
||||
var dye = ColourUtils.getStackColour(stack);
|
||||
if (dye != null) tracker.addColour(dye);
|
||||
}
|
||||
}
|
||||
|
||||
if (colourable.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
var stack = ((IColouredItem) colourable.getItem()).withColour(colourable, tracker.getColour());
|
||||
stack.setCount(1);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCraftInDimensions(int x, int y) {
|
||||
return x >= 2 && y >= 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipeSerializer<?> getSerializer() {
|
||||
return ModRegistry.RecipeSerializers.DYEABLE_ITEM.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.common;
|
||||
|
||||
import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
|
||||
public class DefaultBundledRedstoneProvider implements IBundledRedstoneProvider {
|
||||
@Override
|
||||
public int getBundledRedstoneOutput(Level world, BlockPos pos, Direction side) {
|
||||
return getDefaultBundledRedstoneOutput(world, pos, side);
|
||||
}
|
||||
|
||||
public static int getDefaultBundledRedstoneOutput(Level world, BlockPos pos, Direction side) {
|
||||
var block = world.getBlockState(pos).getBlock();
|
||||
if (block instanceof IBundledRedstoneBlock generic) {
|
||||
if (generic.getBundledRedstoneConnectivity(world, pos, side)) {
|
||||
return generic.getBundledRedstoneOutput(world, pos, side);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.common;
|
||||
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.shared.platform.RegistryEntry;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public abstract class GenericBlock extends BaseEntityBlock {
|
||||
private final RegistryEntry<? extends BlockEntityType<? extends GenericTile>> type;
|
||||
|
||||
public GenericBlock(Properties settings, RegistryEntry<? extends BlockEntityType<? extends GenericTile>> type) {
|
||||
super(settings);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public final void onRemove(BlockState block, Level world, BlockPos pos, BlockState replace, boolean bool) {
|
||||
if (block.getBlock() == replace.getBlock()) return;
|
||||
|
||||
var tile = world.getBlockEntity(pos);
|
||||
super.onRemove(block, world, pos, replace, bool);
|
||||
world.removeBlockEntity(pos);
|
||||
if (tile instanceof GenericTile generic) generic.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
|
||||
var tile = world.getBlockEntity(pos);
|
||||
return tile instanceof GenericTile generic ? generic.onActivate(player, hand, hit) : InteractionResult.PASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public final void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
|
||||
var tile = world.getBlockEntity(pos);
|
||||
if (tile instanceof GenericTile generic) generic.onNeighbourChange(neighbourPos);
|
||||
}
|
||||
|
||||
@ForgeOverride
|
||||
public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) {
|
||||
var tile = world.getBlockEntity(pos);
|
||||
if (tile instanceof GenericTile generic) generic.onNeighbourTileEntityChange(neighbour);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource rand) {
|
||||
var te = world.getBlockEntity(pos);
|
||||
if (te instanceof GenericTile generic) generic.blockTick();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return type.get().create(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.common;
|
||||
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public abstract class GenericTile extends BlockEntity {
|
||||
public GenericTile(BlockEntityType<? extends GenericTile> type, BlockPos pos, BlockState state) {
|
||||
super(type, pos, state);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
public final void updateBlock() {
|
||||
setChanged();
|
||||
var pos = getBlockPos();
|
||||
var state = getBlockState();
|
||||
getLevel().sendBlockUpdated(pos, state, state, Block.UPDATE_ALL);
|
||||
}
|
||||
|
||||
public InteractionResult onActivate(Player player, InteractionHand hand, BlockHitResult hit) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
public void onNeighbourChange(BlockPos neighbour) {
|
||||
}
|
||||
|
||||
public void onNeighbourTileEntityChange(BlockPos neighbour) {
|
||||
}
|
||||
|
||||
protected void blockTick() {
|
||||
}
|
||||
|
||||
protected double getInteractRange(Player player) {
|
||||
return 8.0;
|
||||
}
|
||||
|
||||
public boolean isUsable(Player player) {
|
||||
if (player == null || !player.isAlive() || getLevel().getBlockEntity(getBlockPos()) != this) return false;
|
||||
|
||||
var range = getInteractRange(player);
|
||||
var pos = getBlockPos();
|
||||
return player.getCommandSenderWorld() == getLevel() &&
|
||||
player.distanceToSqr(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5) <= range * range;
|
||||
}
|
||||
|
||||
@ForgeOverride // FIXME: Implement this: I'd forgotten about this
|
||||
public final void onDataPacket(Connection net, ClientboundBlockEntityDataPacket packet) {
|
||||
var tag = packet.getTag();
|
||||
if (tag != null) handleUpdateTag(tag);
|
||||
}
|
||||
|
||||
@ForgeOverride
|
||||
public void handleUpdateTag(CompoundTag tag) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.shared.common;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.network.container.HeldItemContainerData;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class HeldItemMenu extends AbstractContainerMenu {
|
||||
private final ItemStack stack;
|
||||
private final InteractionHand hand;
|
||||
|
||||
public HeldItemMenu(MenuType<? extends HeldItemMenu> type, int id, Player player, InteractionHand hand) {
|
||||
super(type, id);
|
||||
|
||||
this.hand = hand;
|
||||
stack = player.getItemInHand(hand).copy();
|
||||
}
|
||||
|
||||
public static HeldItemMenu createPrintout(int id, Inventory inventory, HeldItemContainerData data) {
|
||||
return new HeldItemMenu(ModRegistry.Menus.PRINTOUT.get(), id, inventory.player, data.getHand());
|
||||
}
|
||||
|
||||
public ItemStack getStack() {
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack quickMoveStack(Player player, int slot) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stillValid(Player player) {
|
||||
if (!player.isAlive()) return false;
|
||||
|
||||
var stack = player.getItemInHand(hand);
|
||||
return stack == this.stack || !stack.isEmpty() && !this.stack.isEmpty() && stack.getItem() == this.stack.getItem();
|
||||
}
|
||||
|
||||
public static class Factory implements MenuProvider {
|
||||
private final MenuType<HeldItemMenu> type;
|
||||
private final Component name;
|
||||
private final InteractionHand hand;
|
||||
|
||||
public Factory(MenuType<HeldItemMenu> type, ItemStack stack, InteractionHand hand) {
|
||||
this.type = type;
|
||||
name = stack.getHoverName();
|
||||
this.hand = hand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {
|
||||
return new HeldItemMenu(type, id, player, hand);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user