1
0
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:
Jonathan Coates
2022-11-09 23:58:56 +00:00
parent bdf590fa30
commit f04acdc199
992 changed files with 2294 additions and 2125 deletions

View File

@@ -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()));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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++;
}
}

View File

@@ -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)
);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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.
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View File

@@ -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)
);
}
}

View File

@@ -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)));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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.
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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)
)
);
}
}

View File

@@ -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";
}
}

View File

@@ -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);
}
}

View File

@@ -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";
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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