Skip to content

Commit

Permalink
Rewrite books in Java
Browse files Browse the repository at this point in the history
  • Loading branch information
Juuxel committed Jul 31, 2024
1 parent e326d0b commit 71ea3ac
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 256 deletions.
18 changes: 18 additions & 0 deletions common/src/main/java/juuxel/adorn/client/book/Book.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package juuxel.adorn.client.book;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import juuxel.adorn.util.MoreCodecs;
import net.minecraft.text.Text;

import java.util.List;

public record Book(Text title, Text subtitle, Text author, List<Page> pages, float titleScale) {
public static final Codec<Book> CODEC = RecordCodecBuilder.create(instance -> instance.group(
MoreCodecs.TEXT.fieldOf("title").forGetter(Book::title),
MoreCodecs.TEXT.fieldOf("subtitle").forGetter(Book::subtitle),
MoreCodecs.TEXT.fieldOf("author").forGetter(Book::author),
Page.CODEC.listOf().fieldOf("pages").forGetter(Book::pages),
Codec.FLOAT.fieldOf("titleScale").forGetter(Book::titleScale)
).apply(instance, Book::new));
}
59 changes: 59 additions & 0 deletions common/src/main/java/juuxel/adorn/client/book/BookManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package juuxel.adorn.client.book;

import com.google.common.base.Predicates;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.JsonOps;
import juuxel.adorn.util.Logging;
import net.minecraft.resource.JsonDataLoader;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import net.minecraft.util.profiler.Profiler;
import org.slf4j.Logger;

import java.util.Map;
import java.util.stream.Collectors;

public class BookManager extends JsonDataLoader {
private static final Logger LOGGER = Logging.logger();
private static final String DATA_TYPE = "adorn/books";
private static final Gson GSON = new Gson();

private Map<Identifier, Book> books = Map.of();

public BookManager() {
super(GSON, DATA_TYPE);
}

@Override
protected void apply(Map<Identifier, JsonElement> prepared, ResourceManager manager, Profiler profiler) {
books = prepared.entrySet()
.stream()
.map(entry -> {
var id = entry.getKey();
var book = Book.CODEC.decode(JsonOps.INSTANCE, entry.getValue()).get();
return book.map(
pair -> Pair.of(id, pair.getFirst()),
partial -> {
LOGGER.error("Could not load book {}: {}", id, partial.message());
return null;
}
);
})
.filter(Predicates.notNull())
.collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
}

public boolean contains(Identifier id) {
return books.containsKey(id);
}

public Book get(Identifier id) {
var book = books.get(id);
if (book == null) {
throw new IllegalArgumentException("Tried to get unknown book '%s' from BookManager".formatted(id));
}
return book;
}
}
51 changes: 51 additions & 0 deletions common/src/main/java/juuxel/adorn/client/book/Image.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package juuxel.adorn.client.book;

import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import juuxel.adorn.util.MoreCodecs;
import juuxel.adorn.util.Vec2i;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public record Image(Identifier location, Vec2i size, Placement placement, List<HoverArea> hoverAreas) {
public static final Codec<Image> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("location").forGetter(Image::location),
Vec2i.CODEC.fieldOf("size").forGetter(Image::size),
Placement.CODEC.optionalFieldOf("placement", Placement.AFTER_TEXT).forGetter(Image::placement),
HoverArea.CODEC.listOf().optionalFieldOf("hoverAreas", List.of()).forGetter(Image::hoverAreas)
).apply(instance, Image::new));

public enum Placement {
BEFORE_TEXT("beforeText"),
AFTER_TEXT("afterText");

private static final Map<String, Placement> BY_ID = Arrays.stream(values())
.map(placement -> Pair.of(placement.id, placement))
.collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
public static final Codec<Placement> CODEC = Codec.STRING.xmap(BY_ID::get, placement -> placement.id);

private final String id;

Placement(String id) {
this.id = id;
}
}

public record HoverArea(Vec2i position, Vec2i size, Text tooltip) {
public static final Codec<HoverArea> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Vec2i.CODEC.fieldOf("position").forGetter(HoverArea::position),
Vec2i.CODEC.fieldOf("size").forGetter(HoverArea::size),
MoreCodecs.TEXT.fieldOf("tooltip").forGetter(HoverArea::tooltip)
).apply(instance, HoverArea::new));

public boolean contains(int x, int y) {
return position.x() <= x && x <= position.x() + size.x() && position.y() <= y && y <= position.y() + size.y();
}
}
}
83 changes: 83 additions & 0 deletions common/src/main/java/juuxel/adorn/client/book/Page.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package juuxel.adorn.client.book;

import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import juuxel.adorn.util.MoreCodecs;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public record Page(List<Icon> icons, Text title, Text text, @Nullable Image image) {
public static final Codec<Page> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Icon.CODEC.listOf().fieldOf("icons").forGetter(Page::icons),
MoreCodecs.TEXT.fieldOf("title").forGetter(Page::title),
MoreCodecs.TEXT.optionalFieldOf("text", Text.empty()).forGetter(Page::text),
Image.CODEC.optionalFieldOf("image").forGetter(page -> Optional.ofNullable(page.image))
).apply(instance, Page::new));

// For DFU
private Page(List<Icon> icons, Text title, Text text, Optional<Image> image) {
this(icons, title, text, image.orElse(null));
}

public sealed interface Icon {
Codec<Icon> CODEC = new Codec<>() {
@Override
public <T> DataResult<T> encode(Icon input, DynamicOps<T> ops, T prefix) {
var id = switch (input) {
case ItemIcon(var item) -> Registries.ITEM.getId(item).toString();
case TagIcon(var tag) -> "#" + tag.id();
};
return ops.mergeToPrimitive(prefix, ops.createString(id));
}

@Override
public <T> DataResult<Pair<Icon, T>> decode(DynamicOps<T> ops, T input) {
return ops.getStringValue(input)
.map(id -> {
Icon icon;
if (id.startsWith("#")) {
icon = new TagIcon(TagKey.of(RegistryKeys.ITEM, new Identifier(id.substring(1))));
} else {
icon = new ItemIcon(Registries.ITEM.get(new Identifier(id)));
}

return Pair.of(icon, ops.empty());
});
}
};

List<ItemStack> createStacks();

record ItemIcon(Item item) implements Icon {
@Override
public List<ItemStack> createStacks() {
return List.of(item.getDefaultStack());
}
}

record TagIcon(TagKey<Item> tag) implements Icon {
@Override
public List<ItemStack> createStacks() {
var entries = Registries.ITEM.getOrCreateEntryList(tag);
List<ItemStack> result = new ArrayList<>(entries.size());
for (var entry : entries) {
result.add(entry.value().getDefaultStack());
}
return result;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected void init() {
// its mouse hover tooltip renders on top of all widgets.
flipBook = addDrawableChild(new FlipBook(this::updatePageTurnButtons));
flipBook.add(new TitlePage(pageX, pageY));
for (var page : book.getPages()) {
for (var page : book.pages()) {
var panel = new Panel();
panel.add(new BookPageTitle(pageX, pageY, page));
var body = new BookPageBody(pageX, pageY + PAGE_TEXT_Y, page);
Expand Down Expand Up @@ -161,7 +161,7 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
private final class TitlePage implements Element, Drawable {
private final int x;
private final int y;
private final Text byAuthor = Text.translatable("book.byAuthor", book.getAuthor());
private final Text byAuthor = Text.translatable("book.byAuthor", book.author());
private boolean focused = false;

private TitlePage(int x, int y) {
Expand All @@ -175,11 +175,11 @@ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
var matrices = context.getMatrices();
matrices.push();
matrices.translate(cx, y + 7 + 25, 0.0);
matrices.scale(book.getTitleScale(), book.getTitleScale(), 1.0f);
context.drawText(textRenderer, book.getTitle(), -textRenderer.getWidth(book.getTitle()) / 2, 0, Colors.SCREEN_TEXT, false);
matrices.scale(book.titleScale(), book.titleScale(), 1.0f);
context.drawText(textRenderer, book.title(), -textRenderer.getWidth(book.title()) / 2, 0, Colors.SCREEN_TEXT, false);
matrices.pop();

context.drawText(textRenderer, book.getSubtitle(), cx - textRenderer.getWidth(book.getSubtitle()) / 2, y + 45, Colors.SCREEN_TEXT, false);
context.drawText(textRenderer, book.subtitle(), cx - textRenderer.getWidth(book.subtitle()) / 2, y + 45, Colors.SCREEN_TEXT, false);
context.drawText(textRenderer, byAuthor, cx - textRenderer.getWidth(byAuthor) / 2, y + 60, Colors.SCREEN_TEXT, false);
}

Expand All @@ -206,8 +206,8 @@ private final class BookPageTitle implements Element, Drawable, TickingElement {
private BookPageTitle(int x, int y, Page page) {
this.x = x;
this.y = y;
this.wrappedTitleLines = textRenderer.wrapLines(page.getTitle().copy().styled(style -> style.withBold(true)), PAGE_TITLE_WIDTH);
this.icons = CollectionsKt.interleave(page.getIcons().stream().map(Page.Icon::createStacks).toList());
this.wrappedTitleLines = textRenderer.wrapLines(page.title().copy().styled(style -> style.withBold(true)), PAGE_TITLE_WIDTH);
this.icons = CollectionsKt.interleave(page.icons().stream().map(Page.Icon::createStacks).toList());
}

@Override
Expand Down Expand Up @@ -254,9 +254,9 @@ private BookPageBody(int x, int y, Page page) {
this.x = x;
this.y = y;
this.page = page;
this.wrappedBodyLines = textRenderer.wrapLines(page.getText(), PAGE_WIDTH - PAGE_TEXT_X);
this.wrappedBodyLines = textRenderer.wrapLines(page.text(), PAGE_WIDTH - PAGE_TEXT_X);
this.textHeight = wrappedBodyLines.size() * textRenderer.fontHeight;
this.imageHeight = page.getImage() != null ? page.getImage().getSize().getY() + PAGE_IMAGE_GAP : 0;
this.imageHeight = page.image() != null ? page.image().size().y() + PAGE_IMAGE_GAP : 0;
this.height = Math.max(PAGE_BODY_HEIGHT, textHeight + imageHeight);
}

Expand Down Expand Up @@ -293,39 +293,39 @@ public boolean isMouseOver(double mouseX, double mouseY) {

@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
int textYOffset = page.getImage() != null && page.getImage().getPlacement() == Image.Placement.BEFORE_TEXT ? imageHeight : 0;
int textYOffset = page.image() != null && page.image().placement() == Image.Placement.BEFORE_TEXT ? imageHeight : 0;

for (int i = 0; i < wrappedBodyLines.size(); i++) {
var line = wrappedBodyLines.get(i);
context.drawText(textRenderer, line, x + PAGE_TEXT_X, textYOffset + y + i * textRenderer.fontHeight, Colors.SCREEN_TEXT, false);
}

if (page.getImage() != null) {
renderImage(context, page.getImage(), mouseX, mouseY);
if (page.image() != null) {
renderImage(context, page.image(), mouseX, mouseY);
}

var hoveredStyle = getTextStyleAt(mouseX, mouseY);
Scissors.suspendScissors(() -> context.drawHoverEvent(textRenderer, hoveredStyle, mouseX, mouseY));
}

private void renderImage(DrawContext context, Image image, int mouseX, int mouseY) {
var imageX = x + (PAGE_WIDTH - image.getSize().getX()) / 2;
var imageY = switch (image.getPlacement()) {
var imageX = x + (PAGE_WIDTH - image.size().x()) / 2;
var imageY = switch (image.placement()) {
case BEFORE_TEXT -> y;
case AFTER_TEXT -> y + textHeight + PAGE_IMAGE_GAP;
};

RenderSystem.enableBlend();
context.drawTexture(image.getLocation(), imageX, imageY, 0f, 0f, image.getSize().getX(), image.getSize().getY(), image.getSize().getX(), image.getSize().getY());
context.drawTexture(image.location(), imageX, imageY, 0f, 0f, image.size().x(), image.size().y(), image.size().x(), image.size().y());
RenderSystem.disableBlend();

for (var hoverArea : image.getHoverAreas()) {
for (var hoverArea : image.hoverAreas()) {
if (hoverArea.contains(mouseX - imageX, mouseY - imageY)) {
var hX = imageX + hoverArea.getPosition().getX();
var hY = imageY + hoverArea.getPosition().getY();
context.fill(hX, hY, hX + hoverArea.getSize().getX(), hY + hoverArea.getSize().getY(), HOVER_AREA_HIGHLIGHT_COLOR);
var hX = imageX + hoverArea.position().x();
var hY = imageY + hoverArea.position().y();
context.fill(hX, hY, hX + hoverArea.size().x(), hY + hoverArea.size().y(), HOVER_AREA_HIGHLIGHT_COLOR);

var wrappedTooltip = textRenderer.wrapLines(hoverArea.getTooltip(), PAGE_WIDTH);
var wrappedTooltip = textRenderer.wrapLines(hoverArea.tooltip(), PAGE_WIDTH);
Scissors.suspendScissors(() -> context.drawOrderedTooltip(textRenderer, wrappedTooltip, mouseX, mouseY));
break;
}
Expand Down
41 changes: 41 additions & 0 deletions common/src/main/java/juuxel/adorn/util/Vec2i.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package juuxel.adorn.util;

import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;

import java.util.stream.IntStream;

public record Vec2i(int x, int y) {
public static final Codec<Vec2i> CODEC = new Codec<>() {
@Override
public <T> DataResult<T> encode(Vec2i input, DynamicOps<T> ops, T prefix) {
return ops.mergeToPrimitive(prefix, ops.createIntList(IntStream.of(input.x, input.y)));
}

@Override
public <T> DataResult<Pair<Vec2i, T>> decode(DynamicOps<T> ops, T input) {
return ops.getIntStream(input).flatMap(stream -> {
var iter = stream.iterator();

if (!iter.hasNext()) return mismatchedComponentCountResult();
int x = iter.nextInt();
if (!iter.hasNext()) return mismatchedComponentCountResult();
int y = iter.nextInt();
if (iter.hasNext()) return mismatchedComponentCountResult();

return DataResult.success(Pair.of(new Vec2i(x, y), ops.empty()));
});
}

@Override
public String toString() {
return "Vec2i";
}
};

private static <T> DataResult<T> mismatchedComponentCountResult() {
return DataResult.error(() -> "Vec2i must have exactly two int components");
}
}
26 changes: 0 additions & 26 deletions common/src/main/kotlin/juuxel/adorn/client/book/Book.kt

This file was deleted.

Loading

0 comments on commit 71ea3ac

Please sign in to comment.