Handbook rewrite part 1: Formatting and JSON-ification!

- Everything is now defined in a JSON file
- Formatting is now done dynamically and content wraps properly no matter what font is used
- Added inline hyperlinks
- Added a movable bookmark for quick access to a specific page
- Added next section and previous section buttons to flip through entire sections at once
- Added a contents button visible on every page, which navigates directly to the main contents
This commit is contained in:
Electroblob77
2019-01-05 22:38:06 +00:00
parent 78189ed862
commit 13569058ae
16 changed files with 2819 additions and 785 deletions
@@ -1,42 +0,0 @@
package electroblob.wizardry.client.gui;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.resources.I18n;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
@SideOnly(Side.CLIENT)
class GuiButtonApply extends GuiButton {
public GuiButtonApply(int id, int x, int y){
super(id, x, y, 32, 16, I18n.format("container." + Wizardry.MODID + ":arcane_workbench.apply"));
}
@Override
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
// Whether the button is highlighted
this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
int k = 36;
int l = 220;
int colour = 14737632;
if(this.enabled){
if(this.hovered){
k += this.width * 2;
colour = 16777120;
}
}else{
k += this.width;
colour = 10526880;
}
WizardryUtilities.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, 256, 256);
this.drawCenteredString(minecraft.fontRenderer, this.displayString, this.x + this.width / 2,
this.y + (this.height - 8) / 2, colour);
}
}
@@ -6,7 +6,7 @@ import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
@SideOnly(Side.CLIENT)
class GuiButtonInvisible extends GuiButton {
public class GuiButtonInvisible extends GuiButton {
public GuiButtonInvisible(int id, int x, int y, int width, int height){
super(id, x, y, width, height, "");
@@ -1,47 +0,0 @@
package electroblob.wizardry.client.gui;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
@SideOnly(Side.CLIENT)
class GuiButtonTurnPage extends GuiButton {
/** True for pointing right (next page), false for pointing left (previous page). */
private final boolean nextPage;
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
public GuiButtonTurnPage(int id, int x, int y, boolean isNextPage){
super(id, x, y, 23, 13, "");
this.nextPage = isNextPage;
}
@Override
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
if(this.visible){
boolean flag = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
minecraft.getTextureManager().bindTexture(texture);
int k = 0;
int l = 192;
if(flag){
k += 23;
}
if(!this.nextPage){
l += 13;
}
WizardryUtilities.drawTexturedRect(this.x, this.y, k, l, 23, 13, 288, 256);
}
}
}
@@ -1,695 +0,0 @@
package electroblob.wizardry.client.gui;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.GL11;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.client.ClientProxy;
import electroblob.wizardry.constants.Constants;
import electroblob.wizardry.constants.Element;
import electroblob.wizardry.constants.Tier;
import electroblob.wizardry.registry.WizardryBlocks;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.RenderHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.ItemStack;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
public class GuiWizardHandbook extends GuiScreen {
private int xSize, ySize;
private int pageNumber = 0;
private static final int PAGE_WIDTH = 120;
/**
* The integer colour for black passed into the font renderer methods. This used to be 0 but that's now white for
* some reason, so I've made a it a constant in case it changes again.
*/
// I think this is actually ever-so-slightly lighter than pure black, but the difference is unnoticeable.
private static final int BLACK = 1;
public static final ResourceLocation regularHandbook = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
public static final ResourceLocation ore = new ResourceLocation(Wizardry.MODID, "textures/gui/ore_picture.png");
public static final ResourceLocation crystal = new ResourceLocation(Wizardry.MODID, "textures/items/magic_crystal.png");
public static final ResourceLocation workbenchGui = new ResourceLocation(Wizardry.MODID, "textures/gui/arcane_workbench.png");
public static final ResourceLocation craftingGrids = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook_recipes.png");
private List<ArrayList<String>> text;
private List<Section> sections;
private static final List<Pair<ItemStack, NonNullList<NonNullList<ItemStack>>>> RECIPES = new ArrayList<>();
private int guiPage, imagePage;
public GuiWizardHandbook(){
super();
xSize = 288;
ySize = 180;
}
@Override
public void drawScreen(int mouseX, int mouseY, float par3){
int xPos = this.width / 2 - xSize / 2;
int yPos = this.height / 2 - this.ySize / 2;
// Tests for crafting recipes section
if(pageNumber >= (sections.get(sections.size() - 1).pageNumber - 1) / 2
&& pageNumber < (sections.get(sections.size() - 1).pageNumber - 1) / 2 + 4){
Minecraft.getMinecraft().renderEngine.bindTexture(craftingGrids);
}else{
Minecraft.getMinecraft().renderEngine.bindTexture(regularHandbook);
}
WizardryUtilities.drawTexturedRect(xPos, yPos, 0, 0, xSize, ySize, xSize, 256);
// Arcane workbench gui picture
if(pageNumber == (this.guiPage - 1) / 2){
Minecraft.getMinecraft().renderEngine.bindTexture(workbenchGui);
this.drawTexturedModalRect(this.guiPage % 2 == 1 ? xPos + 17 : this.width / 2 + 7, yPos + 14, 28, 12, 120,
118);
}
// Magic crystal and crystal ore images
if(pageNumber == (this.imagePage - 1) / 2){
Minecraft.getMinecraft().renderEngine.bindTexture(ore);
WizardryUtilities.drawTexturedRect(this.imagePage % 2 == 1 ? xPos + 17 : this.width / 2 + 7, yPos + 80, 0,
0, 64, 64, 64, 64);
Minecraft.getMinecraft().renderEngine.bindTexture(crystal);
drawTexturedStretchedRect(this.imagePage % 2 == 1 ? xPos + 17 + 64 : this.width / 2 + 7 + 62, yPos + 80, 0,
0, 64, 64, 1, 1);
}
this.fontRenderer.drawString("" + (pageNumber * 2 + 1), xPos + xSize / 4 - 3, yPos + ySize - 20, 0);
this.fontRenderer.drawString("" + (pageNumber * 2 + 2), xPos + 3 * xSize / 4 - 5, yPos + ySize - 20, 0);
super.drawScreen(mouseX, mouseY, par3);
int lineNumber = 0;
if(pageNumber == 1){
for(Section s : sections){
s.drawContents();
}
}else{
for(Section s : sections){
s.hideButton();
}
}
for(String paragraph : text.get(pageNumber * 2)){
this.fontRenderer.drawSplitString(paragraph, xPos + 17,
yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
List<String> list = new ArrayList<String>(
this.fontRenderer.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
lineNumber += list.size();
}
lineNumber = 0;
// Prevents crash when the last page is blank (and hence is not in the list of pages)
if(text.size() > pageNumber * 2 + 1){
for(String paragraph : text.get(pageNumber * 2 + 1)){
// First page is centred
if(pageNumber == 0){
int startX = this.width / 2 + 7 + PAGE_WIDTH / 2
- this.fontRenderer.getStringWidth(paragraph) / 2;
this.fontRenderer.drawSplitString(paragraph, startX,
yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
}else{
this.fontRenderer.drawSplitString(paragraph, this.width / 2 + 7,
yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
}
List<String> list = new ArrayList<String>(
this.fontRenderer.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
lineNumber += list.size();
}
}
// Which page of the recipes this is
int recipePage = pageNumber - (sections.get(sections.size() - 1).pageNumber - 1) / 2;
if(recipePage >= 0 && recipePage < 4){
// 4 recipes per page, hence the recipePage*4
this.renderCraftingRecipe(xPos + 23, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4).getRight(), RECIPES.get(recipePage*4).getLeft());
this.renderCraftingRecipe(xPos + 23, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+1).getRight(), RECIPES.get(recipePage*4+1).getLeft());
this.renderCraftingRecipe(xPos + 156, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4+2).getRight(), RECIPES.get(recipePage*4+2).getLeft());
this.renderCraftingRecipe(xPos + 156, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+3).getRight(), RECIPES.get(recipePage*4+3).getLeft());
// Tooltips are rendered after recipes to prevent tooltips on the left appearing behind items on the right.
this.renderCraftingTooltips(xPos + 23, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4).getRight(), RECIPES.get(recipePage*4).getLeft());
this.renderCraftingTooltips(xPos + 23, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+1).getRight(), RECIPES.get(recipePage*4+1).getLeft());
this.renderCraftingTooltips(xPos + 156, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4+2).getRight(), RECIPES.get(recipePage*4+2).getLeft());
this.renderCraftingTooltips(xPos + 156, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+3).getRight(), RECIPES.get(recipePage*4+3).getLeft());
}
}
// TODO: In 1.12, this all needs redoing nicely. With the crafting system halfway through changing in 1.11.2, this
// isn't worth doing until then.
private void renderCraftingRecipe(int xPos, int yPos, int mouseX, int mouseY, NonNullList<NonNullList<ItemStack>> craftingGrid,
ItemStack craftingResult){
GlStateManager.pushMatrix();
RenderHelper.enableGUIStandardItemLighting();
GlStateManager.disableLighting();
GlStateManager.enableRescaleNormal();
GlStateManager.enableColorMaterial();
GlStateManager.enableLighting();
itemRender.zLevel = 100.0F;
for(int i = 0; i < craftingGrid.size(); i++){
for(int j = 0; j < craftingGrid.get(i).size(); j++){
ItemStack stack = craftingGrid.get(i).get(j);
if(!stack.isEmpty()){
itemRender.renderItemAndEffectIntoGUI(stack, xPos + 18 * i, yPos + 18 * j);
itemRender.renderItemOverlays(this.fontRenderer, stack, xPos + 18 * i,
yPos + 18 * j);
}
}
}
if(!craftingResult.isEmpty()){
itemRender.renderItemAndEffectIntoGUI(craftingResult, xPos + 86, yPos + 18);
itemRender.renderItemOverlays(this.fontRenderer, craftingResult, xPos + 86, yPos + 18);
}
GlStateManager.popMatrix();
GlStateManager.enableLighting();
GlStateManager.enableDepth();
RenderHelper.enableStandardItemLighting();
}
private void renderCraftingTooltips(int xPos, int yPos, int mouseX, int mouseY, NonNullList<NonNullList<ItemStack>> craftingGrid,
ItemStack craftingResult){
int guiLeft = this.width / 2 - xSize / 2;
int guiTop = this.height / 2 - this.ySize / 2;
GlStateManager.pushMatrix();
RenderHelper.enableGUIStandardItemLighting();
GlStateManager.disableLighting();
GlStateManager.enableRescaleNormal();
GlStateManager.enableColorMaterial();
itemRender.zLevel = 0.0F;
GlStateManager.disableLighting();
for(int i = 0; i < craftingGrid.size(); i++){
for(int j = 0; j < craftingGrid.get(i).size(); j++){
ItemStack stack = craftingGrid.get(i).get(j);
if(!stack.isEmpty()
&& isPointInRegion(xPos + 18 * i, yPos + 18 * j, 16, 16, mouseX + guiLeft, mouseY + guiTop)){
this.renderToolTip(stack, mouseX, mouseY);
}
}
}
if(!craftingResult.isEmpty() && isPointInRegion(xPos + 86, yPos + 18, 16, 16, mouseX + guiLeft, mouseY + guiTop)){
this.renderToolTip(craftingResult, mouseX, mouseY);
}
GlStateManager.popMatrix();
GlStateManager.enableLighting();
GlStateManager.enableDepth();
RenderHelper.enableStandardItemLighting();
}
@Override
public void initGui(){
super.initGui();
Keyboard.enableRepeatEvents(true);
int nextButtonId = 0;
this.buttonList.clear();
this.buttonList.add(new GuiButtonTurnPage(nextButtonId++, this.width / 2 + this.xSize / 2 - 22 - 23,
this.height / 2 + this.ySize / 2 - 10 - 13, true));
this.buttonList.add(new GuiButtonTurnPage(nextButtonId++, this.width / 2 - this.xSize / 2 + 21,
this.height / 2 + this.ySize / 2 - 10 - 13, false));
text = new ArrayList<ArrayList<String>>(1);
sections = new ArrayList<Section>(1);
BufferedReader bufferedreader = null;
String textFilepath = Wizardry.MODID + ":texts/handbook_"
+ Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage().getLanguageCode() + ".txt";
try{
bufferedreader = new BufferedReader(new InputStreamReader(
this.mc.getResourceManager().getResource(new ResourceLocation(textFilepath)).getInputStream(),
StandardCharsets.UTF_8));
}catch (IOException e){
Wizardry.logger.info(
"Wizard handbook text file missing for the current language. Using default (English - US) instead.");
textFilepath = Wizardry.MODID + ":texts/handbook_en_us.txt";
try {
bufferedreader = new BufferedReader(new InputStreamReader(
this.mc.getResourceManager().getResource(new ResourceLocation(textFilepath)).getInputStream(),
StandardCharsets.UTF_8));
} catch (IOException x){
Wizardry.logger.error("Couldn't find file: " + Wizardry.MODID + "/assets/texts/handbook_en_us.txt. The file may be"
+ "missing; please try re-downloading and reinstalling Wizardry.", x);
}
}
if(bufferedreader != null){
try{
String paragraph = bufferedreader.readLine();
ArrayList<String> page = new ArrayList<String>(1);
int linesPerPage = 16;
int lineNumber = 0;
while(paragraph != null){
// System.out.println(paragraph);
if(paragraph.contains("PAGEBREAK") || lineNumber >= linesPerPage){
text.add(page);
page = new ArrayList<String>(1);
lineNumber = 0;
if(paragraph.contains("PAGEBREAK")) paragraph = bufferedreader.readLine();
}else if(paragraph.contains("LINEBREAK")){
lineNumber++;
page.add("");
paragraph = bufferedreader.readLine();
}else if(paragraph.contains("SECTION")){
sections.add(
new Section(paragraph.replace("SECTION ", ""), text.size() + 1, this.width / 2 + 7,
this.height / 2 - this.ySize / 2 + 14
+ (sections.size() + 2) * this.fontRenderer.FONT_HEIGHT,
nextButtonId++));
paragraph = bufferedreader.readLine();
}else if(paragraph.contains("IMAGE")){
if(paragraph.contains("WORKBENCH")){
this.guiPage = text.size() + 1;
}else if(paragraph.contains("CRYSTAL")){
this.imagePage = text.size() + 1;
}
paragraph = bufferedreader.readLine();
}else{
paragraph = paragraph.replaceAll("NEXT_SPELL_KEY", ClientProxy.NEXT_SPELL.getDisplayName());
paragraph = paragraph.replaceAll("PREVIOUS_SPELL_KEY", ClientProxy.PREVIOUS_SPELL.getDisplayName());
paragraph = paragraph.replaceAll("MANA_PER_CRYSTAL_MINUS_30", "" + (Constants.MANA_PER_CRYSTAL - 30));
paragraph = paragraph.replaceAll("MANA_PER_CRYSTAL", "" + Constants.MANA_PER_CRYSTAL);
paragraph = paragraph.replaceAll("BASIC_MAX_CHARGE", "" + Tier.BASIC.maxCharge);
paragraph = paragraph.replaceAll("APPRENTICE_MAX_CHARGE", "" + Tier.APPRENTICE.maxCharge);
paragraph = paragraph.replaceAll("ADVANCED_MAX_CHARGE", "" + Tier.ADVANCED.maxCharge);
paragraph = paragraph.replaceAll("MASTER_MAX_CHARGE", "" + Tier.MASTER.maxCharge);
paragraph = paragraph.replaceAll("BASIC_COLOUR", "\u00A77");
paragraph = paragraph.replaceAll("APPRENTICE_COLOUR", Tier.APPRENTICE.getFormattingCode());
paragraph = paragraph.replaceAll("ADVANCED_COLOUR", Tier.ADVANCED.getFormattingCode());
paragraph = paragraph.replaceAll("MASTER_COLOUR", Tier.MASTER.getFormattingCode());
paragraph = paragraph.replaceAll("FIRE_COLOUR", Element.FIRE.getFormattingCode());
paragraph = paragraph.replaceAll("ICE_COLOUR", Element.ICE.getFormattingCode());
paragraph = paragraph.replaceAll("LIGHTNING_COLOUR", Element.LIGHTNING.getFormattingCode());
paragraph = paragraph.replaceAll("NECROMANCY_COLOUR", Element.NECROMANCY.getFormattingCode());
paragraph = paragraph.replaceAll("EARTH_COLOUR", Element.EARTH.getFormattingCode());
paragraph = paragraph.replaceAll("SORCERY_COLOUR", Element.SORCERY.getFormattingCode());
paragraph = paragraph.replaceAll("HEALING_COLOUR", Element.HEALING.getFormattingCode());
paragraph = paragraph.replaceAll("RESET_COLOUR", "\u00A70");
paragraph = paragraph.replaceAll("MCVERSION", "1.12.2");
paragraph = paragraph.replaceAll("VERSION", Wizardry.VERSION);
int linesInParagraph = this.fontRenderer
.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH).size();
// Ignores empty lines at the top of a page.
if(paragraph.isEmpty() && lineNumber == 0){
paragraph = bufferedreader.readLine();
// Normal paragraph, all on one page
}else if(lineNumber + linesInParagraph <= linesPerPage){
page.add(paragraph);
lineNumber += linesInParagraph;
paragraph = bufferedreader.readLine();
// Paragraphs split across two pages (or more?)
}else{
int linesInFirstPart = linesPerPage - lineNumber;
String paragraphFirstPart = "";
String paragraphLastPart = "";
int i = 0;
List<String> strings = this.fontRenderer.listFormattedStringToWidth(paragraph,
GuiWizardHandbook.PAGE_WIDTH);
for(Object s : strings){
if(i < linesInFirstPart){
paragraphFirstPart = paragraphFirstPart.concat((String)s + " ");
}else{
paragraphLastPart = paragraphLastPart.concat((String)s + " ");
}
i++;
}
// System.out.println("Paragraph crosses page boundary; string split into: \"" +
// paragraphFirstPart + "\" and \"" + paragraphLastPart + "\"");
page.add(paragraphFirstPart);
lineNumber += linesInFirstPart;
paragraph = paragraphLastPart;
}
}
}
text.add(page);
}catch (IOException e){
Wizardry.logger.error("Something went wrong reading file: " + textFilepath
+ ". The file may be damaged; please try re-downloading and reinstalling wizardry.", e);
}
}
}
private class Section {
/** The integer text colour used for the section when it is moused over. Currently orange. */
private static final int HIGHLIGHT_COLOUR = 0xdd4c1d;
String name;
int pageNumber;
int x, y;
int buttonId;
Section(String name, int pageNumber, int x, int y, int id){
this.name = name;
this.pageNumber = pageNumber;
this.x = x;
this.y = y;
this.buttonId = id;
GuiWizardHandbook.this.buttonList.add(new GuiButtonInvisible(id, x, y, GuiWizardHandbook.PAGE_WIDTH,
GuiWizardHandbook.this.fontRenderer.FONT_HEIGHT));
}
void hideButton(){
GuiWizardHandbook.this.buttonList.get(buttonId).visible = false;
}
void drawContents(){
GuiWizardHandbook.this.buttonList.get(buttonId).visible = true;
GuiWizardHandbook.this.fontRenderer.drawString(name, x, y,
GuiWizardHandbook.this.buttonList.get(buttonId).isMouseOver() ? HIGHLIGHT_COLOUR : BLACK);
int nameWidth = GuiWizardHandbook.this.fontRenderer.getStringWidth(name);
String dotsAndNumber = " " + this.pageNumber;
while(GuiWizardHandbook.this.fontRenderer.getStringWidth(dotsAndNumber) < GuiWizardHandbook.PAGE_WIDTH
- nameWidth - 2){
dotsAndNumber = "." + dotsAndNumber;
}
GuiWizardHandbook.this.fontRenderer.drawString(dotsAndNumber, x + GuiWizardHandbook.PAGE_WIDTH
- GuiWizardHandbook.this.fontRenderer.getStringWidth(dotsAndNumber), y, BLACK);
}
}
@Override
public void onGuiClosed(){
super.onGuiClosed();
Keyboard.enableRepeatEvents(false);
}
/**
* Fired when a control is clicked. This is the equivalent of ActionListener.actionPerformed(ActionEvent e).
*/
@Override
protected void actionPerformed(GuiButton par1GuiButton){
if(par1GuiButton.enabled){
if(par1GuiButton.id == 0){
if(pageNumber < (text.size() - 1) / 2) pageNumber++;
}else if(par1GuiButton.id == 1){
if(pageNumber > 0) pageNumber--;
}else{
if(pageNumber == 1) pageNumber = (sections.get(par1GuiButton.id - 2).pageNumber - 1) / 2;
}
}
}
/**
* Args: left, top, width, height, pointX, pointY. Note: left, top are local to Gui, pointX, pointY are local to
* screen
*/
protected boolean isPointInRegion(int par1, int par2, int par3, int par4, int par5, int par6){
int k1 = this.width / 2 - xSize / 2;
int l1 = this.height / 2 - this.ySize / 2;
par5 -= k1;
par6 -= l1;
return par5 >= par1 - 1 && par5 < par1 + par3 + 1 && par6 >= par2 - 1 && par6 < par2 + par4 + 1;
}
/**
* Draws a textured rectangle, stretching the section of the image to fit the size given.
*
* @param x The x position of the rectangle
* @param y The y position of the rectangle
* @param u The x position of the top left corner of the section of the image wanted, expressed as a fraction of the
* image width
* @param v The y position of the top left corner of the section of the image wanted, expressed as a fraction of the
* image width
* @param finalWidth The width as rendered
* @param finalHeight The height as rendered
* @param width The width of the section, expressed as a fraction of the image width
* @param height The height of the section, expressed as a fraction of the image width
*/
public static void drawTexturedStretchedRect(int x, int y, int u, int v, int finalWidth, int finalHeight, int width,
int height){
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
buffer.pos((x), y + finalHeight, 0).tex(u, v + height).endVertex();
buffer.pos(x + finalWidth, y + finalHeight, 0).tex(u + width, v + height).endVertex();
buffer.pos(x + finalWidth, (y), 0).tex(u + width, v).endVertex();
buffer.pos((x), (y), 0).tex(u, v).endVertex();
tessellator.draw();
}
private static NonNullList<NonNullList<ItemStack>> createGrid(){
NonNullList<NonNullList<ItemStack>> grid = NonNullList.withSize(3, NonNullList.create());
for(int i=0; i<3; i++){
grid.set(i, NonNullList.withSize(3, ItemStack.EMPTY));
}
return grid;
}
/** Called from init() in the main mod class to initialise the recipes for display in the handbook. */
public static void initDisplayRecipes(){
NonNullList<NonNullList<ItemStack>> craftingGrid;
ItemStack craftingResult;
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(Items.GOLD_NUGGET));
craftingGrid.get(1).set(0, new ItemStack(Blocks.CARPET, 1, 10));
craftingGrid.get(2).set(0, new ItemStack(Items.GOLD_NUGGET));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(1, new ItemStack(Blocks.LAPIS_BLOCK));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(0).set(2, new ItemStack(Blocks.STONE));
craftingGrid.get(1).set(2, new ItemStack(Blocks.STONE));
craftingGrid.get(2).set(2, new ItemStack(Blocks.STONE));
craftingResult = new ItemStack(WizardryBlocks.arcane_workbench);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(1, new ItemStack(Items.STICK));
craftingGrid.get(0).set(2, new ItemStack(Items.GOLD_NUGGET));
craftingResult = new ItemStack(WizardryItems.magic_wand);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(1, new ItemStack(Items.BOOK));
craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingResult = new ItemStack(WizardryItems.spell_book, 1, 1);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(Items.BOOK));
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
craftingResult = new ItemStack(WizardryItems.wizard_handbook);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(WizardryBlocks.crystal_flower));
craftingResult = new ItemStack(WizardryItems.magic_crystal, 2);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(1, new ItemStack(Items.GLASS_BOTTLE));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_crystal));
craftingResult = new ItemStack(WizardryItems.mana_flask);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(1).set(0, new ItemStack(Blocks.STONE));
craftingGrid.get(0).set(1, new ItemStack(Blocks.STONE));
craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(2, new ItemStack(Blocks.STONE));
craftingGrid.get(2).set(1, new ItemStack(Blocks.STONE));
craftingResult = new ItemStack(WizardryBlocks.transportation_stone, 2);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(1).set(0, new ItemStack(Items.STRING));
craftingGrid.get(0).set(1, new ItemStack(Items.STRING));
craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_crystal));
craftingGrid.get(1).set(2, new ItemStack(Items.STRING));
craftingGrid.get(2).set(1, new ItemStack(Items.STRING));
craftingResult = new ItemStack(WizardryItems.magic_silk, 2);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
craftingResult = new ItemStack(WizardryItems.wizard_hat);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_silk));
craftingResult = new ItemStack(WizardryItems.wizard_robe);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_silk));
craftingResult = new ItemStack(WizardryItems.wizard_leggings);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
craftingResult = new ItemStack(WizardryItems.wizard_boots);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(Items.PAPER));
craftingGrid.get(1).set(0, new ItemStack(Items.STRING));
craftingResult = new ItemStack(WizardryItems.blank_scroll);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(Items.BLAZE_POWDER));
craftingGrid.get(1).set(0, new ItemStack(Items.BLAZE_POWDER));
craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
craftingResult = new ItemStack(WizardryItems.firebomb, 3);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(Items.SPIDER_EYE));
craftingGrid.get(1).set(0, new ItemStack(Items.SPIDER_EYE));
craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
craftingResult = new ItemStack(WizardryItems.poison_bomb, 3);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
craftingGrid = createGrid();
craftingGrid.get(0).set(0, new ItemStack(Items.COAL));
craftingGrid.get(1).set(0, new ItemStack(Items.COAL));
craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
craftingResult = new ItemStack(WizardryItems.smoke_bomb, 3);
RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
}
}
@@ -0,0 +1,181 @@
package electroblob.wizardry.client.gui.handbook;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import electroblob.wizardry.client.DrawingUtils;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.util.JsonUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Instances of this class represent tables of contents in the wizard's handbook. Each {@link Section} can have a
* single table of contents, which can reference any other sections in the handbook (though it is normal to list
* top-level sections in a main contents and have subsections listed in their respective parent sections' contents).
*
* This class handles JSON parsing, formatting and drawing of the contents itself, working on a line-by-line basis
* (as opposed to sections, which work on a page-by-page basis). It also stores its own list of buttons.
*
* @author Electroblob
* @since Wizardry 4.2
*/
class Contents {
// Final fields are mandatory, the rest are optional
final String id;
private boolean hyperlinks = true;
private boolean pageNumbers = true;
private String separator = ".";
// Derived fields, not specifically defined in JSON
private int startPage;
private int startLine;
private final List<List<GuiButton>> buttons;
private final List<Section> entries;
private Contents(String id){
this.id = id;
this.entries = new ArrayList<>();
this.buttons = new ArrayList<>();
}
/** Returns an unmodifiable, flattened collection of all the buttons in this contents. */
Collection<GuiButton> getButtons(){
return WizardryUtilities.flatten(buttons);
}
void addEntry(Section section){
entries.add(section);
}
/**
* Draws this contents for the given double-page spread and shows/hides buttons accordingly. Will draw nothing
* if the given page is outside of this contents.
*
* @param font The font renderer object.
* @param doublePage The index of the <b>double-page</b> to be drawn.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
*/
void draw(FontRenderer font, int doublePage, int left, int top){
// Show/hide buttons
int i = 0;
for(List<GuiButton> list : buttons){
final int i1 = i++;
list.forEach(b -> b.visible = GuiWizardHandbook.singleToDoublePage(startPage + i1) == doublePage);
}
if(!pageNumbers) return; // No page numbers means only the buttons are drawn
// FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
int leftIndex = GuiWizardHandbook.doubleToSinglePage(doublePage, false);
// Relative indices of the pages to be rendered - often these will be outside the section entirely
int[] visiblePages = {leftIndex - startPage, leftIndex - startPage + 1};
for(int page : visiblePages){
if(page >= 0 && page < entries.size() / maxLineNumber + 1){
int x = left + (GuiWizardHandbook.isRightPage(startPage + page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X);
int y = top + GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT;
for(Section entry : this.entries){
int nameWidth = font.getStringWidth(entry.title);
String dotsAndNumber = " " + entry.startPage;
while(font.getStringWidth(dotsAndNumber) < GuiWizardHandbook.PAGE_WIDTH - nameWidth - 2){
dotsAndNumber = separator + dotsAndNumber;
}
font.drawString(dotsAndNumber, x + GuiWizardHandbook.PAGE_WIDTH - font.getStringWidth(dotsAndNumber), y, DrawingUtils.BLACK, false);
if(!hyperlinks) font.drawString(entry.title, x, y, DrawingUtils.BLACK, false);
y += font.FONT_HEIGHT;
}
}
}
}
/**
* Called on GUI load to format the section and all subsections, contents tables and other elements. Does not
* perform any actual drawing.
*
* @param font The font renderer object, for measurement purposes.
* @param startPage The index of the first page (single side, not double-page) of this section.
* @param startLine The index of the first line of this contents.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
* @return The number of lines this contents takes up.
* @throws JsonSyntaxException if at any point the formatting is found to be invalid.
*/
int format(FontRenderer font, int startPage, int startLine, int left, int top){
this.buttons.clear();
if(hyperlinks){
// FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
this.startPage = startPage;
this.startLine = startLine;
List<GuiButton> list = new ArrayList<>(maxLineNumber);
for(Section entry : this.entries){
int x = GuiWizardHandbook.isRightPage(startPage) ? left + GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : left + GuiWizardHandbook.TEXT_INSET_X;
int y = top + GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT;
list.add(new GuiButtonHyperlink.Internal(0, x, y, font, entry.title, entry, 0, ""));
startLine++;
if(startLine == maxLineNumber){
startLine = 0;
startPage++;
buttons.add(list);
list = new ArrayList<>(maxLineNumber); // If there are no more entries this will be discarded anyway
}
}
buttons.add(list);
}
// Returning this is kind of trivial at the moment but if we ever wanted to add a header or something,
// it would be more useful.
return entries.size();
}
/**
* Parses the given JSON object and constructs a new {@code Contents} from it, setting all the relevant fields
* and references.
*
* @param json A JSON object representing the contents to be constructed. This must contain at least an "id"
* string.
* @return The resulting {@code Contents} object.
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
*/
static Contents fromJson(JsonObject json){
Contents contents = new Contents(JsonUtils.getString(json, "id"));
contents.hyperlinks = JsonUtils.getBoolean(json, "hyperlinks", true);
contents.pageNumbers = JsonUtils.getBoolean(json, "page_numbers", true);
contents.separator = JsonUtils.getString(json, "separator", ".");
return contents;
}
}
@@ -0,0 +1,225 @@
package electroblob.wizardry.client.gui.handbook;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import electroblob.wizardry.client.DrawingUtils;
import electroblob.wizardry.spell.Mine;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.recipebook.GuiRecipeBook;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.RenderHelper;
import net.minecraft.client.renderer.RenderItem;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.CraftingManager;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.stats.RecipeBook;
import net.minecraft.util.JsonUtils;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class CraftingRecipe {
static final int WIDTH = 111, HEIGHT = 56;
// Final fields are mandatory, the rest are optional
private final ResourceLocation location;
// Derived fields, not specifically defined in JSON
private IRecipe recipe;
private final Set<int[]> instances = new HashSet<>();
private CraftingRecipe(ResourceLocation location){
this.location = location;
}
/**
* Adds an instance of this recipe to the list.
*
* @param page The index of the <b>single</b> page this image is on.
* @param x The x-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
* @param y The y-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
*/
void addInstance(int page, int x, int y){
instances.add(new int[]{page, x, y});
}
/** Removes all instances of this recipe from the list. */
void clearInstances(){
instances.clear();
}
/** Called on GUI open to load the actual recipe object from the registry. This cannot be done on JSON load since
* the recipes aren't necessarily loaded at that point. */
void load(){
this.recipe = CraftingManager.getRecipe(location);
if(recipe == null) throw new JsonSyntaxException("No such recipe: " + location);
}
/**
* Draws all instances of this recipe that are located on the given double-page spread.
*
* @param font The font renderer object.
* @param itemRenderer The item renderer object.
* @param doublePage The double-page index of the page to be drawn.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
*/
void draw(FontRenderer font, RenderItem itemRenderer, int doublePage, int left, int top){
for(int[] instance : instances){
if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
renderCraftingRecipe(font, itemRenderer, left + instance[1], top + instance[2], this.recipe);
}
}
}
/**
* Draws the tooltips for all instances of this recipe that are located on the given double-page spread. This has to
* be done separately so that the tooltips are on top of everything else.
*
* @param itemRenderer The item renderer object.
* @param doublePage The double-page index of the page to be drawn.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
*/
void drawTooltips(GuiWizardHandbook gui, FontRenderer font, RenderItem itemRenderer, int doublePage, int left, int top, int mouseX, int mouseY){
for(int[] instance : instances){
if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
renderCraftingTooltips(gui, itemRenderer, left + instance[1], top + instance[2], mouseX, mouseY, this.recipe);
}
}
}
/**
* Parses the given JSON object and constructs a new {@code Image} from it, setting all the relevant fields
* and references.
*
* @param json A JSON object representing the image to be constructed. This must contain at least a "location"
* string.
* @return The resulting {@code Image} object.
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
*/
static CraftingRecipe fromJson(JsonObject json){
ResourceLocation location = new ResourceLocation(JsonUtils.getString(json, "location"));
return new CraftingRecipe(location);
}
static void populate(Map<String, CraftingRecipe> map, JsonObject json){
JsonObject sectionsObject = JsonUtils.getJsonObject(json, "recipes");
// Need to iterate over these since we don't know what they're called or how many there are
for(Map.Entry<String, JsonElement> entry : sectionsObject.entrySet()){
String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
CraftingRecipe recipe = fromJson(entry.getValue().getAsJsonObject());
map.put(key, recipe);
}
}
private static void renderCraftingRecipe(FontRenderer font, RenderItem itemRenderer, int x, int y, IRecipe recipe){
ItemStack result = recipe.getRecipeOutput();
GlStateManager.color(1, 1, 1, 1);
Minecraft.getMinecraft().renderEngine.bindTexture(GuiWizardHandbook.texture);
DrawingUtils.drawTexturedRect(x - 2, y - 2, 60, 190, WIDTH, HEIGHT, 512, 256);
GlStateManager.pushMatrix();
RenderHelper.enableGUIStandardItemLighting();
GlStateManager.disableLighting();
GlStateManager.enableRescaleNormal();
GlStateManager.enableColorMaterial();
GlStateManager.enableLighting();
itemRenderer.zLevel = 100.0F;
// TODO: There may be a better way of doing this
int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
int i = 0;
for(Ingredient ingredient : recipe.getIngredients()){
if(ingredient != Ingredient.EMPTY){
ItemStack stack = ingredient.getMatchingStacks()[index % ingredient.getMatchingStacks().length];
if(!stack.isEmpty()){
itemRenderer.renderItemAndEffectIntoGUI(stack, x + 18 * (i%3), y + 18 * (i/3));
itemRenderer.renderItemOverlays(font, stack, x + 18 * (i%3), y + 18 * (i/3));
}
}
i++;
}
if(!result.isEmpty()){
itemRenderer.renderItemAndEffectIntoGUI(result, x + 86, y + 18);
itemRenderer.renderItemOverlays(font, result, x + 86, y + 18);
}
GlStateManager.popMatrix();
GlStateManager.enableLighting();
GlStateManager.enableDepth();
GlStateManager.disableColorMaterial();
itemRenderer.zLevel = 0.0F;
//RenderHelper.enableStandardItemLighting();
}
private static void renderCraftingTooltips(GuiWizardHandbook gui, RenderItem itemRenderer, int x, int y, int mouseX, int mouseY, IRecipe recipe){
ItemStack result = recipe.getRecipeOutput();
GlStateManager.pushMatrix();
RenderHelper.enableGUIStandardItemLighting();
GlStateManager.disableLighting();
GlStateManager.enableRescaleNormal();
GlStateManager.enableColorMaterial();
itemRenderer.zLevel = 0.0F;
GlStateManager.disableLighting();
// TODO: There may be a better way of doing this
int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
int i = 0;
for(Ingredient ingredient : recipe.getIngredients()){
if(ingredient != Ingredient.EMPTY){
ItemStack stack = ingredient.getMatchingStacks()[index % ingredient.getMatchingStacks().length];
if(!stack.isEmpty() && isPointInRegion(x + 18 * (i%3), y + 18 * (i/3), 16, 16, mouseX, mouseY)){
gui.renderToolTip(stack, mouseX, mouseY);
}
}
i++;
}
if(!result.isEmpty() && isPointInRegion(x + 86, y + 18, 16, 16, mouseX, mouseY)){
gui.renderToolTip(result, mouseX, mouseY);
}
GlStateManager.popMatrix();
GlStateManager.enableLighting();
GlStateManager.enableDepth();
GlStateManager.disableColorMaterial();
RenderHelper.enableStandardItemLighting();
}
private static boolean isPointInRegion(int left, int top, int width, int height, int mouseX, int mouseY){
return mouseX >= left - 1 && mouseX < left + width + 1 && mouseY >= top - 1 && mouseY < top + height + 1;
}
}
@@ -0,0 +1,179 @@
package electroblob.wizardry.client.gui.handbook;
import com.google.gson.JsonSyntaxException;
import electroblob.wizardry.registry.WizardrySounds;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.client.audio.SoundHandler;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.event.ClickEvent;
import java.util.ArrayList;
import java.util.List;
public abstract class GuiButtonHyperlink extends GuiButton {
public static final String URL_REGEX = "^((https?|ftp)://|(www|ftp)\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+([/?].*)?$";
final int indent;
final List<String> lines;
GuiButtonHyperlink(int id, int x, int y, FontRenderer font, String text, int indent, String suffix){
super(id, x, y, font.getStringWidth(text), font.FONT_HEIGHT, text);
// Sometimes a link has punctuation or something after it that causes it to wrap onto a new line
String linkWithSuffix = text + suffix;
// If the string won't fit any words at the end of the current line, treat it as if we started a new line
if(font.getStringWidth(linkWithSuffix.split("\\s")[0]) > GuiWizardHandbook.PAGE_WIDTH - indent){
indent = 0;
this.y += font.FONT_HEIGHT;
}
this.indent = indent; // Assigned here in case it was corrected above
String line1 = font.listFormattedStringToWidth(linkWithSuffix, GuiWizardHandbook.PAGE_WIDTH - indent).get(0);
// Without trim(), there will be at least 1 leading space due to the custom wrapping
String remainder = linkWithSuffix.substring(line1.length()).trim();
// ... then wrap the rest to the normal width.
lines = new ArrayList<>();
lines.add(line1);
// Some links are only one line, if this wasn't checked they would cause a StackOverflowError
if(!remainder.isEmpty()) lines.addAll(font.listFormattedStringToWidth(remainder, GuiWizardHandbook.PAGE_WIDTH));
// Removes the suffix if it exists (ugly as heck, but it works)
if(!suffix.isEmpty()){
for(int i=lines.size()-1; i>=0; i--){
String line = lines.get(i);
if(suffix.endsWith(line)){
lines.remove(i);
}else if(line.endsWith(suffix)){
lines.set(i, line.substring(0, line.length() - suffix.length()));
break;
}
}
}
}
public boolean isHovered(net.minecraft.client.gui.FontRenderer font, int mouseX, int mouseY){
int i = 0;
for(String line : lines){
int l = x;
if(i == 0) l += indent;
int t = y + font.FONT_HEIGHT * i;
if(mouseX >= l && mouseY >= t && mouseX < l + font.getStringWidth(line) && mouseY < t + font.FONT_HEIGHT){
return true;
}
i++;
}
return false;
}
@Override
public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY){
return this.enabled && this.visible && isHovered(minecraft.fontRenderer, mouseX, mouseY);
}
@Override
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
if(this.visible){
this.hovered = isHovered(minecraft.fontRenderer, mouseX, mouseY);
int colour = hovered ? GuiWizardHandbook.colours.get("highlight") : GuiWizardHandbook.colours.get("hyperlink");
int i = 0;
for(String line : lines){
int l = x;
if(i == 0) l += indent;
int t = y + minecraft.fontRenderer.FONT_HEIGHT * i;
minecraft.fontRenderer.drawString(line, l, t, colour);
i++;
}
}
}
/**
* Creates a new hyperlink button from the given arguments, automatically differentiating between URLs and sections.
* @param x The x position of the button
* @param y The y position of the button
* @param font A reference to the FontRenderer object
* @param upToLink The paragraph (as a list of lines) up to the link, used to determine positioning and word wrap
* @param arguments The link arguments - that is, everything between the two @ signs, split by spaces
* @param suffix The text directly after the link, up to the first whitespace; used for word wrap. Usually this is
* either empty or contains a single punctuation mark.
* @return The resulting button
* @throws IllegalArgumentException if the given argument array is empty or contains more than 2 arguments
* @throws JsonSyntaxException if the specified link target is not a URL or a valid section ID
*/
public static GuiButtonHyperlink create(int x, int y, FontRenderer font, List<String> upToLink, String[] arguments, String suffix){
if(arguments.length == 0 || arguments.length > 2) throw new IllegalArgumentException("Incorrect array length!");
GuiButtonHyperlink button;
if(arguments[0].matches(URL_REGEX)){
button = new GuiButtonHyperlink.External(0, x, y, font, arguments[arguments.length - 1], arguments[0],
font.getStringWidth(upToLink.get(upToLink.size() - 1)), suffix);
}else{
Section target = GuiWizardHandbook.sections.get(arguments[0]);
if(target == null) throw new JsonSyntaxException("Hyperlink points to nonexistent section id " + arguments[0]);
button = new GuiButtonHyperlink.Internal(0, x, y, font, arguments[arguments.length - 1],
target, font.getStringWidth(upToLink.get(upToLink.size() - 1)), suffix);
}
return button;
}
static class Internal extends GuiButtonHyperlink {
final Section target;
Internal(int id, int x, int y, FontRenderer font, String text, Section target, int indent, String suffix){
super(id, x, y, font, text, indent, suffix);
this.target = target;
}
@Override
public void playPressSound(SoundHandler soundHandler){
soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
}
}
static class External extends GuiButtonHyperlink {
final ITextComponent link;
External(int id, int x, int y, FontRenderer font, String text, String url, int indent, String suffix){
super(id, x, y, font, text, indent, suffix);
this.link = new TextComponentString(text);
link.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url));
}
}
}
@@ -0,0 +1,63 @@
package electroblob.wizardry.client.gui.handbook;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.client.DrawingUtils;
import electroblob.wizardry.registry.WizardrySounds;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.client.audio.SoundHandler;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
@SideOnly(Side.CLIENT)
class GuiButtonTurnPage extends GuiButton {
static final int WIDTH = 20;
static final int HEIGHT = 12;
enum Type {
NEXT_PAGE(0, 196),
PREVIOUS_PAGE(0, 208),
NEXT_SECTION(0, 220),
PREVIOUS_SECTION(0, 232),
CONTENTS(0, 244);
private final int u, v;
Type(int u, int v){
this.u = u;
this.v = v;
}
}
public final Type type;
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
public GuiButtonTurnPage(int id, int x, int y, Type type){
super(id, x, y, WIDTH, HEIGHT, "");
this.type = type;
}
@Override
public void playPressSound(SoundHandler soundHandler){
soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
}
@Override
public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
if(this.visible){
boolean flag = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
minecraft.getTextureManager().bindTexture(texture);
DrawingUtils.drawTexturedRect(this.x, this.y, flag ? type.u + width : type.u, type.v, width, height, 512, 256);
}
}
}
@@ -0,0 +1,566 @@
package electroblob.wizardry.client.gui.handbook;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.client.ClientProxy;
import electroblob.wizardry.client.DrawingUtils;
import electroblob.wizardry.client.gui.GuiButtonInvisible;
import electroblob.wizardry.client.gui.handbook.GuiButtonTurnPage.Type;
import electroblob.wizardry.constants.Constants;
import electroblob.wizardry.constants.Element;
import electroblob.wizardry.constants.Tier;
import electroblob.wizardry.registry.WizardrySounds;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.client.audio.SoundHandler;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.multiplayer.ClientAdvancementManager;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.resources.IResource;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.item.ItemStack;
import net.minecraft.util.JsonUtils;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.event.entity.player.AdvancementEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.ReflectionHelper;
import org.apache.commons.lang3.tuple.Pair;
import org.lwjgl.input.Keyboard;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.*;
import java.util.List;
/**
* GUI class for the wizard's handbook. Like any GUI class, this is instantiated each time the book is opened. As of
* Wizardry 4.2, the handbook text is defined as a JSON file rather than a plain text file, and is loaded only on
* resource pack reload, rather than every time the book is opened. This means all the data structures (sections, images,
* etc.) are built before the GUI instance exists at all. However, since some things depend on positioning, these have to
* be initialised on GUI creation. (Previously, everything was done on GUI load)
*
* @author Electroblob
* @since Wizardry 1.0
* @see Section
* @see Contents
* @see Image
* @see CraftingRecipe
*/
@Mod.EventBusSubscriber
public class GuiWizardHandbook extends GuiScreen {
private static final ResourceLocation DEFAULT = new ResourceLocation(Wizardry.MODID, "texts/handbook_en_us.json");
static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
/** Global Gson instance for the handbook. */
private static final Gson gson = new Gson();
// Formatting markup
static final char FORMAT_MARKER = '#';
static final char HYPERLINK_MARKER = '@';
static final String IMAGE_TAG = "image";
static final String RECIPE_TAG = "recipe";
static final Map<String, String> FORMAT_TAGS = new HashMap<>();
// Dimension constants
// Private constants are not relevant to book elements, package-protected ones are
/** The dimensions of the rendered GUI area. */
static final int GUI_WIDTH = 288, GUI_HEIGHT = 180;
/** The dimensions of the area of a single page in which text can be drawn. */
static final int PAGE_WIDTH = 120, PAGE_HEIGHT = 140;
/** The distance of the text from the top outside corner of each page. */
static final int TEXT_INSET_X = 17, TEXT_INSET_Y = 16;
/** The distance of the buttons from the bottom outside corners of the GUI. */
private static final int BUTTON_INSET_X = 22, BUTTON_INSET_Y = 13;
/** The distance between adjacent buttons. */
private static final int BUTTON_SPACING = 20;
/** The distance of the page numbers from the bottom of the GUI. */
private static final int PAGE_NUMBER_INSET = 22;
// IDEA: Constant dimensions could be converted to JSON like the spell HUD ones
// Global variables
/**
* The <b>double-page</b> currently being viewed. Each double-page spread counts as a single page, with the inside
* of the front cover being page 0.
*/
private int currentPage = 0;
/**
* The number of <b>single</b> pages currently in the book. This is calculated on GUI load based on visible sections.
*/
private int pageCount = 1; // Starts at 1 because the first single-page is the inside of the cover
/**
* The <b>double-page</b> number where the bookmark is currently set, <b>relative to the section stored in
* {@link GuiWizardHandbook#bookmarkSection}</b>. Static because it persists when the book is closed.
*/
private static int bookmarkPage = 1;
/**
* A reference to the section in which the bookmark is currently set. Static because it persists when the book is
* closed. Storing a section means the bookmark doesn't change location when new sections are unlocked.
*/
private static Section bookmarkSection;
// Buttons
private GuiButton bookmark, next, previous, nextSection, previousSection, menu;
// Handbook content
// As a general rule, I prefer to make static final fields lowercase if they're collections that change, because even
// though the collection itself is constant, the stuff in it is not, so being lowercase highlights this difference.
/**
* A map which stores all loaded section objects, including subsections. This gets wiped on resource pack reload and
* repopulated with mappings as specified by the handbook JSON file for the current language. The keys in the map
* correspond to the keys in the sections object in that file, and are sorted in that order.
*/
static final Map<String, Section> sections = new LinkedHashMap<>();
/**
* A list which stores all loaded section objects, including subsections. This is an unmodifiable list view of the
* values in {@link GuiWizardHandbook#sections}, sorted in the same (page number) order. This exists only to allow
* sections to be accessed by ordinal index for the various navigation buttons, hence why it is private.
*/
private static List<Section> sectionList;
/**
* A map which stores all loaded contents objects. This gets wiped on resource pack reload and repopulated with
* mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
* id strings for the contents objects in that file. This map is not sorted.
*/
static final Map<String, Contents> contentsList = new HashMap<>();
/**
* A map which stores all loaded hex colour values. This gets wiped on resource pack reload and repopulated with
* mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
* keys in the colours object in that file. This map is not sorted.
*/
static final Map<String, Integer> colours = new HashMap<>();
/**
* A map which stores all loaded image objects. This gets wiped on resource pack reload and repopulated with
* mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
* keys in the images object in that file. This map is not sorted.
*/
static final Map<String, Image> images = new HashMap<>();
/**
* A map which stores all loaded crafting recipe objects. This gets wiped on resource pack reload and repopulated
* with mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to
* the keys in the recipes object in that file. This map is not sorted.
*/
static final Map<String, CraftingRecipe> recipes = new HashMap<>();
static String rulerText = "--------------------"; // Assigned here as a fallback
private static final List<Pair<ItemStack, NonNullList<NonNullList<ItemStack>>>> RECIPES = new ArrayList<>();
// Reflection
static final Field ADVANCEMENT_TO_PROGRESS;
static {
// TODO: Obf field name
ADVANCEMENT_TO_PROGRESS = ReflectionHelper.findField(ClientAdvancementManager.class, "advancementToProgress");
}
/**
* Adds a format tag to the handbook. All occurrences of the given tag string preceded by a # will be replaced with
* the result of the given value string on GUI load. The value string, therefore, can be anything that should be
* input dynamically, as long as it does not change while the GUI is open. Examples include wizardry's version,
* the various element colours and the keys assigned to wizardry's controls.
* @param tag The tag string, as defined in the handbook JSON file, excluding the # character. Cannot include spaces.
* @param value The string to replace occurrences of the given format tag with. Can include spaces but not the # character.
*/
public static void addFormatTag(String tag, String value){
FORMAT_TAGS.put(tag, value);
}
private static void initFormatTags(){
addFormatTag("next_spell_key", ClientProxy.NEXT_SPELL.getDisplayName());
addFormatTag("previous_spell_key", ClientProxy.PREVIOUS_SPELL.getDisplayName());
addFormatTag("mana_per_crystal_minus_30", "" + (Constants.MANA_PER_CRYSTAL - 30));
addFormatTag("mana_per_crystal", "" + Constants.MANA_PER_CRYSTAL);
addFormatTag("novice_max_charge", "" + Tier.BASIC.maxCharge);
addFormatTag("apprentice_max_charge", "" + Tier.APPRENTICE.maxCharge);
addFormatTag("advanced_max_charge", "" + Tier.ADVANCED.maxCharge);
addFormatTag("master_max_charge", "" + Tier.MASTER.maxCharge);
addFormatTag("version", Wizardry.VERSION);
addFormatTag("mcversion", Minecraft.getMinecraft().getVersion());
addFormatTag("colour_novice", "\u00A77");
addFormatTag("colour_apprentice", Tier.APPRENTICE.getFormattingCode());
addFormatTag("colour_advanced", Tier.ADVANCED.getFormattingCode());
addFormatTag("colour_master", Tier.MASTER.getFormattingCode());
addFormatTag("colour_fire", Element.FIRE.getFormattingCode());
addFormatTag("colour_ice", Element.ICE.getFormattingCode());
addFormatTag("colour_lightning", Element.LIGHTNING.getFormattingCode());
addFormatTag("colour_necromancy", Element.NECROMANCY.getFormattingCode());
addFormatTag("colour_earth", Element.EARTH.getFormattingCode());
addFormatTag("colour_sorcery", Element.SORCERY.getFormattingCode());
addFormatTag("colour_healing", Element.HEALING.getFormattingCode());
addFormatTag("colour_reset", "\u00A70");
}
// Helper methods
/**
* Converts the given single page index to a double-page index. Inverse of
* {@link GuiWizardHandbook#doubleToSinglePage(int, boolean)}.
*
* @param singlePageIndex The single-page index, which is the same as the page numbers actually displayed.
* @return The corresponding double-page index.
*/
static int singleToDoublePage(int singlePageIndex){
// Yes, this is trivial, but if I ever change the numbering it'll be useful. It's also more descriptive.
return singlePageIndex / 2;
}
/**
* Converts the given double-page index to a single-page index. Inverse of
* {@link GuiWizardHandbook#singleToDoublePage(int)}.
*
* @param doublePageIndex The double-page index, as stored in {@link GuiWizardHandbook#currentPage}.
* @param rightHandPage True to return the page on the right (1 greater), false for the left-hand page.
* @return The corresponding single-page index.
*/
static int doubleToSinglePage(int doublePageIndex, boolean rightHandPage){
return rightHandPage ? doublePageIndex * 2 + 1 : doublePageIndex * 2;
}
/**
* Returns whether the given page index refers to a right-hand page or a left-hand page.
*
* @param page The single-page index, which is the same as the page number actually displayed.
* @return True if the given page index refers to a right-hand page, false if it is a left-hand page.
*/
static boolean isRightPage(int page){
return page % 2 == 1;
}
// Drawing
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks){
int left = this.width / 2 - GUI_WIDTH / 2;
int top = this.height / 2 - GUI_HEIGHT / 2;
Minecraft.getMinecraft().renderEngine.bindTexture(texture);
// Main background
DrawingUtils.drawTexturedRect(left, top, 0, 0, GUI_WIDTH, GUI_HEIGHT, 512, 256);
// First page background
if(currentPage == 0){
DrawingUtils.drawTexturedRect(left, top, 368, 0, GUI_WIDTH / 2, GUI_HEIGHT, 512, 256);
previous.visible = false;
previousSection.visible = false; // Not worth testing if we're in the first section every frame
menu.visible = false;
}else{
previous.visible = true;
previousSection.visible = true;
menu.visible = true;
}
// Last page background
if(currentPage == singleToDoublePage(pageCount)){
DrawingUtils.drawTexturedFlippedRect(left + GUI_WIDTH / 2, top, 368, 0, GUI_WIDTH / 2, GUI_HEIGHT, 512, 256, true, false);
next.visible = false;
nextSection.visible = false;
}else{
next.visible = true;
nextSection.visible = true;
}
// Page numbers
if(currentPage > 0){
String pageNumber = "" + doubleToSinglePage(currentPage, false);
this.fontRenderer.drawString(pageNumber, left + TEXT_INSET_X + PAGE_WIDTH / 2
- fontRenderer.getStringWidth(pageNumber)/2, top + GUI_HEIGHT - PAGE_NUMBER_INSET, DrawingUtils.BLACK);
}
if(currentPage < singleToDoublePage(pageCount)){
String pageNumber = "" + doubleToSinglePage(currentPage, true);
this.fontRenderer.drawString(pageNumber, left + GUI_WIDTH - TEXT_INSET_X - PAGE_WIDTH / 2
- fontRenderer.getStringWidth(pageNumber)/2, top + GUI_HEIGHT - PAGE_NUMBER_INSET, DrawingUtils.BLACK);
}
// Main content
images.values().forEach(i -> i.draw(fontRenderer, currentPage, left, top));
contentsList.values().forEach(c -> c.draw(fontRenderer, currentPage, left, top));
sections.values().forEach(s -> s.draw(fontRenderer, currentPage, left, top));
recipes.values().forEach(r -> r.draw(fontRenderer, itemRender, currentPage, left, top));
// Buttons
super.drawScreen(mouseX, mouseY, partialTicks);
// Bookmark
GlStateManager.color(1, 1, 1, 1);
Minecraft.getMinecraft().renderEngine.bindTexture(texture);
if(currentPage == singleToDoublePage(bookmarkSection.startPage) + bookmarkPage){
bookmark.visible = false;
DrawingUtils.drawTexturedRect(left + 138, top, 299, 0, 11, 191, 512, 256);
}else{
bookmark.visible = true;
bookmark.x = left + (currentPage > singleToDoublePage(bookmarkSection.startPage) + bookmarkPage ? 130 : 147);
DrawingUtils.drawTexturedRect(bookmark.x, top,
bookmark.isMouseOver() ? 310 : 288, 0, 11, 191, 512, 256);
}
// Recipe tooltips
recipes.values().forEach(r -> r.drawTooltips(this, fontRenderer, itemRender, currentPage, left, top, mouseX, mouseY));
}
// GUI Initialisation / Close
@Override
public void onResize(Minecraft minecraft, int width, int height){
initGui();
}
@Override
public void onGuiClosed(){
super.onGuiClosed();
Keyboard.enableRepeatEvents(false);
}
@Override
public void initGui(){
super.initGui();
Keyboard.enableRepeatEvents(true);
initFormatTags();
final int left = this.width / 2 - GUI_WIDTH / 2;
final int top = this.height / 2 - GUI_HEIGHT / 2;
recipes.values().forEach(CraftingRecipe::load);
int nextButtonId = 0;
this.buttonList.clear();
this.buttonList.add(next = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH - BUTTON_INSET_X - GuiButtonTurnPage.WIDTH,
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.NEXT_PAGE));
this.buttonList.add(previous = new GuiButtonTurnPage(nextButtonId++, left + BUTTON_INSET_X,
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.PREVIOUS_PAGE));
this.buttonList.add(nextSection = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH - BUTTON_INSET_X - GuiButtonTurnPage.WIDTH - BUTTON_SPACING,
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.NEXT_SECTION));
this.buttonList.add(previousSection = new GuiButtonTurnPage(nextButtonId++, left + BUTTON_INSET_X + BUTTON_SPACING,
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.PREVIOUS_SECTION));
this.buttonList.add(menu = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH/2 - 28,
top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.CONTENTS));
this.buttonList.add(bookmark = new GuiButtonInvisible(nextButtonId++, left + 130, top + 172, 11, 19) {
@Override
public void playPressSound(SoundHandler soundHandler){
soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
}
});
pageCount = 1;
// Clears instances of all images and recipes
images.values().forEach(Image::clearInstances);
recipes.values().forEach(CraftingRecipe::clearInstances);
// Formats all the unlocked sections in order
for(Section section : sections.values()){
if(section.isUnlocked()){
pageCount = section.format(this.fontRenderer, pageCount, left, top);
buttonList.addAll(section.getButtons());
}
}
contentsList.values().forEach(c -> buttonList.addAll(c.getButtons()));
this.mc.getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_BOOK_OPEN, 1));
}
// JSON Parsing / Data Construction
/**
* Called from preInit in the main mod class (via the proxies) to initialise the handbook (parses the JSON file
* and constructs the relevant data structures), and again on each resource reload (changing the language triggers
* a resource reload).
*/
public static void loadHandbookFile(IResourceManager manager){
IResource handbookFile = getHandbookResource(manager);
if(handbookFile != null){
// Wipes all the maps before repopulating them
images.clear();
sections.clear();
contentsList.clear();
colours.clear();
bookmarkSection = null; // Also need to wipe the reference to the old bookmarked section
BufferedReader reader = new BufferedReader(new InputStreamReader(handbookFile.getInputStream()));
JsonElement je = gson.fromJson(reader, JsonElement.class);
JsonObject json = je.getAsJsonObject();
rulerText = JsonUtils.getString(json, "ruler_text");
JsonUtils.getJsonObject(json, "colours").entrySet().forEach(e -> colours.put(e.getKey(),
Color.decode(e.getValue().getAsString()).getRGB()));
// Repopulates the remaining maps
Image.populate(images, json);
CraftingRecipe.populate(recipes, json);
Section.populate(sections, json);
sectionList = Collections.unmodifiableList(new ArrayList<>(sections.values()));
if(sections.isEmpty()){
Wizardry.logger.warn("Handbook has no sections! Aborting loading...");
return;
}
// Attempts to find a suitable place to put the bookmark to start with
bookmarkSection = sections.get("introduction");
if(bookmarkSection == null) bookmarkSection = sectionList.get(0);
}
}
/**
* Retrieves the handbook JSON file for the current language and returns its IResource object. If a handbook file
* cannot be found for the current language, a message is printed to the console and the method attempts to retrieve
* the default file instead (English-US). If this file cannot be found, the resulting error is printed to the
* console and the method returns null.
*
* @param manager The resource manager instance to use.
* @return The handbook JSON file, as an IResource, or null if it was not found.
*/
private static IResource getHandbookResource(IResourceManager manager){
IResource handbookFile = null;
try{
handbookFile = manager.getResource(new ResourceLocation(Wizardry.MODID, "texts/handbook_"
+ Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage().getLanguageCode() + ".json"));
}catch(IOException e){
Wizardry.logger.info("Wizard handbook JSON file missing for the current language (" + Minecraft.getMinecraft()
.getLanguageManager().getCurrentLanguage() + "). Using default (English-US) instead.");
try{
handbookFile = manager.getResource(DEFAULT);
}catch(IOException x){
Wizardry.logger.error("Couldn't find file: " + DEFAULT + ". The file may be missing; please try re-downloading and reinstalling Wizardry.", x);
}
}
return handbookFile;
}
// Controls
@Override
protected void actionPerformed(GuiButton button){
if(button.enabled){
if(button == next){
if(currentPage < singleToDoublePage(pageCount)) currentPage++;
}else if(button == previous){
if(currentPage > 0) currentPage--;
}else if(button == nextSection || button == previousSection){
Section currentSection = null;
for(Section section : sections.values()){
// We always want this button to do something, and taking the right-hand page means it always does
if(section.containsPage(doubleToSinglePage(currentPage, true))){
currentSection = section;
break;
}
}
if(currentSection != null){
int index = sectionList.indexOf(currentSection);
if(button == nextSection && index + 1 < sections.size()){
currentPage = singleToDoublePage(sectionList.get(index + 1).startPage);
}else if(index > 0){
currentPage = singleToDoublePage(sectionList.get(index - 1).startPage);
}
}
}else if(button == menu){
currentPage = singleToDoublePage(sections.get("main_contents").startPage);
}else if(button == bookmark && bookmarkSection != null){
currentPage = singleToDoublePage(bookmarkSection.startPage) + bookmarkPage;
}else{
if(button instanceof GuiButtonHyperlink.Internal){
currentPage = singleToDoublePage(((GuiButtonHyperlink.Internal)button).target.startPage);
}else if(button instanceof GuiButtonHyperlink.External){
this.handleComponentClick(((GuiButtonHyperlink.External)button).link);
}
}
}
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException{
if(mouseButton == 1){
// Right-clicking of bookmark
if(bookmark.mousePressed(this.mc, mouseX, mouseY)){
this.selectedButton = bookmark;
for(Section section : sections.values()){
// The bookmark is assumed to bookmark the left-hand page
if(section.containsPage(doubleToSinglePage(bookmarkPage, false))) bookmarkSection = section;
}
bookmarkPage = currentPage - singleToDoublePage(bookmarkSection.startPage);
}
}else{
super.mouseClicked(mouseX, mouseY, mouseButton);
}
}
// Overridden to make it public
@Override
public void renderToolTip(ItemStack stack, int x, int y){
super.renderToolTip(stack, x, y);
}
@SubscribeEvent
public static void onAdvancementEvent(AdvancementEvent event){
sections.values().forEach(s -> s.onAdvancement(event.getEntityPlayer(), event.getAdvancement()));
}
}
@@ -0,0 +1,119 @@
package electroblob.wizardry.client.gui.handbook;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import electroblob.wizardry.client.DrawingUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.JsonUtils;
import net.minecraft.util.ResourceLocation;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class Image {
// Final fields are mandatory, the rest are optional
private final ResourceLocation location;
private final int width, height;
private int textureWidth, textureHeight;
private int u = 0, v = 0;
private String caption = "";
// Derived fields, not specifically defined in JSON
private final Set<int[]> instances = new HashSet<>();
private static final int CAPTION_OFFSET = 4;
private Image(ResourceLocation location, int width, int height){
this.location = location;
this.width = width;
this.height = height;
}
/** Returns the width of the image. */
int getWidth(){
return width;
}
/** Returns the total height of the image, including caption if it has one. */
int getHeight(FontRenderer font){
return caption.isEmpty() ? height : height + CAPTION_OFFSET + font.FONT_HEIGHT;
}
/**
* Adds an instance of this image to the list.
*
* @param page The index of the <b>single</b> page this image is on.
* @param x The x-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
* @param y The y-coordinate of the top-left corner of the image, <i>relative</i> to the top-left corner of the GUI.
*/
void addInstance(int page, int x, int y){
instances.add(new int[]{page, x, y});
}
/** Removes all instances of this image from the list. */
void clearInstances(){
instances.clear();
}
/**
* Draws all instances of this image that are located on the given double-page spread.
*
* @param font The font renderer object.
* @param doublePage The double-page index of the page to be drawn.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
*/
void draw(FontRenderer font, int doublePage, int left, int top){
for(int[] instance : instances){
if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
Minecraft.getMinecraft().renderEngine.bindTexture(location);
GlStateManager.color(1, 1, 1, 1);
DrawingUtils.drawTexturedRect(left + instance[1], top + instance[2], u, v, width, height, textureWidth, textureHeight);
font.drawString("\u00A7o" + caption, left + instance[1] + width/ 2 - font.getStringWidth(caption)/2,
top + instance[2] + height + CAPTION_OFFSET, GuiWizardHandbook.colours.get("caption"));
}
}
}
/**
* Parses the given JSON object and constructs a new {@code Image} from it, setting all the relevant fields
* and references.
*
* @param json A JSON object representing the image to be constructed. This must contain at least a "location"
* string.
* @return The resulting {@code Image} object.
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
*/
static Image fromJson(JsonObject json){
Image image = new Image(new ResourceLocation(JsonUtils.getString(json, "location")),
JsonUtils.getInt(json, "width"), JsonUtils.getInt(json, "height"));
image.u = JsonUtils.getInt(json, "u", 0);
image.v = JsonUtils.getInt(json, "v", 0);
image.textureWidth = JsonUtils.getInt(json, "texture_width", image.width);
image.textureHeight = JsonUtils.getInt(json, "texture_height", image.height);
image.caption = JsonUtils.getString(json, "caption", "");
return image;
}
static void populate(Map<String, Image> map, JsonObject json){
JsonObject sectionsObject = JsonUtils.getJsonObject(json, "images");
// Need to iterate over these since we don't know what they're called or how many there are
for(Map.Entry<String, JsonElement> entry : sectionsObject.entrySet()){
String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
Image image = fromJson(entry.getValue().getAsJsonObject());
map.put(key, image);
}
}
}
@@ -0,0 +1,429 @@
package electroblob.wizardry.client.gui.handbook;
import com.google.common.collect.Streams;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.client.DrawingUtils;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.AdvancementProgress;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* Instances of this class represent sections in the wizard's handbook. As of wizardry 4.2, this class handles
* everything within the section itself, including JSON parsing, unlock triggers and drawing the actual rawText.
* Sections may now also be nested and have other elements within them, such as images and a table of contents, a
* behaviour which is also handled within this class.
* <p>
* The formatting of the book is now done 'dynamically' - that is, the exact positions and page numbers of
* sections, images and so on are determined on GUI load and depend on which of the previous sections have been
* unlocked, amongst other factors. This means that all of the unlocked sections must be formatted in order on GUI
* load, so that each section knows the previous section's length and therefore where to start.
*
* @author Electroblob
* @since Wizardry 4.2
*/
// Because these are now generated on resource pack reload (not on handbook open, as before), this class must now
// be static
class Section {
// Final fields are mandatory (none here though), the rest are optional
String title;
private String[] rawText;
private Contents contents;
private Advancement[] triggers;
private Map<String, Section> subsections;
private boolean centreX, centreY;
// Derived fields, not explicitly defined in JSON
/** The <b>single-page</b> index of the first page of this section. */
int startPage;
private final List<List<GuiButton>> buttons;
/**
* A list of <b>single</b> pages, which are themselves lists of paragraphs (each paragraph is a single
* string which may include line breaks and other escape characters).
*/
private final List<List<String>> pages;
private Section(){
this.buttons = new ArrayList<>();
this.pages = new ArrayList<>();
this.subsections = new LinkedHashMap<>();
}
Collection<GuiButton> getButtons(){
return WizardryUtilities.flatten(buttons);
}
/**
* Returns true if the given page is within this section, false if not.
*/
boolean containsPage(int page){
return startPage <= page && startPage + pages.size() > page;
}
/**
* Returns true if this section is unlocked for the client player, false if not. Always returns true if
* handbook progression is disabled in the config.
*/
boolean isUnlocked(){
if(!Wizardry.settings.handbookProgression) return true; // Always unlocked if handbook progression is off
// A section is automatically unlocked if one of its subsections is unlocked
for(Section subsection : subsections.values()){
if(subsection.isUnlocked()) return true;
}
if(triggers == null) return true; // If no triggers were defined, the section is unlocked from the start
for(Advancement trigger : triggers){
try{
// TESTME: Is this properly synced or do we need to do that as well?
Map<Advancement, AdvancementProgress> advancements = (Map)GuiWizardHandbook.ADVANCEMENT_TO_PROGRESS.get(Minecraft.getMinecraft().player.connection.getAdvancementManager());
if(advancements.get(trigger).isDone()){
return true;
}
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
return false;
}
/**
* Actually draws the contents of the given section for the given double-page spread. Will do nothing if the
* given page is outside of this section.
*
* @param font The font renderer object.
* @param doublePage The index of the <b>double-page</b> to be drawn.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
*/
// This method is supposed to be 'idiot-proof' in the sense that the code calling it need not check whether the
// section actually needs drawing, so it can just dumbly call draw(...) for all the sections in order.
void draw(FontRenderer font, int doublePage, int left, int top){
// Show/hide buttons
int i = 0;
for(List<GuiButton> list : buttons){
final int i1 = i++;
list.forEach(b -> b.visible = GuiWizardHandbook.singleToDoublePage(startPage + i1) == doublePage);
}
int leftIndex = GuiWizardHandbook.doubleToSinglePage(doublePage, false);
// Relative indices of the pages to be rendered - often these will be outside the section entirely
int[] visiblePages = {leftIndex - startPage, leftIndex - startPage + 1};
for(int page : visiblePages){
if(page >= 0 && page < pages.size()){
List<String> lines = pages.get(page);
int x = left + (GuiWizardHandbook.isRightPage(startPage + page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X);
int y = top + GuiWizardHandbook.TEXT_INSET_Y;
if(centreY) y += GuiWizardHandbook.PAGE_HEIGHT / 2 - lines.size() / 2 * font.FONT_HEIGHT;
for(String line : lines){
int lx = centreX ? x + GuiWizardHandbook.PAGE_WIDTH / 2 - font.getStringWidth(line) / 2 : x;
font.drawString(line, lx, y, DrawingUtils.BLACK, false);
y += font.FONT_HEIGHT;
}
}
}
}
/**
* Called on GUI load to format the section, contents tables and other elements, <b>excluding</b> subsections.
* Does not perform any actual drawing.
*
* @param font The font renderer object, for measurement purposes.
* @param startPage The index of the first page (single side, not double-page) of this section.
* @param left The x coordinate of the left side of the GUI.
* @param top The y coordinate of the top of the GUI.
* @return The <b>single-page</b> index of the next blank page after the end of this section.
* @throws JsonSyntaxException if at any point the formatting is found to be invalid.
*/
int format(FontRenderer font, int startPage, int left, int top){
this.buttons.clear();
this.pages.clear();
// FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
this.startPage = startPage;
// First everything is added to a single list of lines, then it is split into pages.
List<String> lines = new ArrayList<>();
// Adds the header if present
if(!this.title.isEmpty()){
lines.add(this.title);
lines.add(GuiWizardHandbook.rulerText);
}
// Adds space for the contents if it exists
if(this.contents != null){
lines.addAll(Collections.nCopies(this.contents.format(font, startPage, lines.size(), left, top), ""));
// Line break between contents and first paragraph
if((lines.size() % maxLineNumber) != 0) lines.add("");
}
if(this.rawText != null){
// Paragraphs are defined as a JSON list because it makes it easier to arrange them properly across pages
// - using multiple line breaks would mean having to find and remove them when at the top of a page.
for(String paragraph : this.rawText){
// (lines.size() % maxLineNumber) gives the number of lines on the current page
// (lines.size() / maxLineNumber) gives the index of the current page minus the value of startPage
// Formats the paragraph
String raw = paragraph; // For error messages
// Images (images must be separate paragraphs)
if(paragraph.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.IMAGE_TAG)){
String[] arguments = paragraph.split("\\s", 2);
if(arguments.length < 2) throw new JsonSyntaxException("Missing image name in string "
+ StringUtils.abbreviate(raw, 50));
Image image = GuiWizardHandbook.images.get(arguments[1]);
if(image == null) throw new JsonSyntaxException("Image with id " + arguments[1] + "is undefined");
// Starts a new page if the image will not fit on the current one
if((lines.size() % maxLineNumber) * font.FONT_HEIGHT + image.getHeight(font) > GuiWizardHandbook.PAGE_HEIGHT){
// Remaining number of lines on the page
lines.addAll(Collections.nCopies(maxLineNumber - (lines.size() % maxLineNumber), ""));
}
if(image.getWidth() > GuiWizardHandbook.PAGE_WIDTH) Wizardry.logger.warn("Image with id " + arguments[1]
+ "has a width (" + image.getWidth() + ") greater than the maximum page width (" + GuiWizardHandbook.PAGE_WIDTH
+ "), it will extend beyond the page area.");
if(image.getHeight(font) > GuiWizardHandbook.PAGE_HEIGHT) Wizardry.logger.warn("Image with id " + arguments[1]
+ "has a height (" + image.getHeight(font) + ") greater than the maximum page height (" + GuiWizardHandbook.PAGE_HEIGHT
+ "), it will extend beyond the page area.");
int page = startPage + (lines.size() / maxLineNumber);
image.addInstance(page, GuiWizardHandbook.PAGE_WIDTH / 2 - image.getWidth() / 2
+ (GuiWizardHandbook.isRightPage(page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X),
GuiWizardHandbook.TEXT_INSET_Y + (lines.size() % maxLineNumber) * font.FONT_HEIGHT);
// Height of the image in lines, rounded up
// Uses a single space instead of an empty string so that the page trimming doesn't remove them
lines.addAll(Collections.nCopies(image.getHeight(font) / font.FONT_HEIGHT, " "));
lines.add(""); // The last one is removable though, since it's actually extra space
// Recipes (recipes must be separate paragraphs)
}else if(paragraph.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.RECIPE_TAG)){
String[] arguments = paragraph.split("\\s", 2);
if(arguments.length < 2) throw new JsonSyntaxException("Missing recipe name in string "
+ StringUtils.abbreviate(raw, 50));
CraftingRecipe recipe = GuiWizardHandbook.recipes.get(arguments[1]);
if(recipe == null) throw new JsonSyntaxException("Recipe with id " + arguments[1] + "is undefined");
// Starts a new page if the recipe will not fit on the current one
if((lines.size() % maxLineNumber) * font.FONT_HEIGHT + CraftingRecipe.HEIGHT > GuiWizardHandbook.PAGE_HEIGHT){
// Remaining number of lines on the page
lines.addAll(Collections.nCopies(maxLineNumber - (lines.size() % maxLineNumber), ""));
}
int page = startPage + (lines.size() / maxLineNumber);
recipe.addInstance(page, GuiWizardHandbook.PAGE_WIDTH / 2 - CraftingRecipe.WIDTH / 2
+ (GuiWizardHandbook.isRightPage(page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X),
GuiWizardHandbook.TEXT_INSET_Y + (lines.size() % maxLineNumber) * font.FONT_HEIGHT);
// Height of the recipe in lines, rounded up
// Uses a single space instead of an empty string so that the page trimming doesn't remove them
lines.addAll(Collections.nCopies(CraftingRecipe.HEIGHT / font.FONT_HEIGHT, " "));
// This time we're not adding an extra space because it's not really needed
}else{ // All other paragraphs
// Hyperlinks
int linkStart;
while((linkStart = paragraph.indexOf(GuiWizardHandbook.HYPERLINK_MARKER)) > -1){ // Ooh an assignment and a comparison in one...
int linkEnd = paragraph.indexOf(GuiWizardHandbook.HYPERLINK_MARKER, linkStart + 1);
if(linkEnd < 0) throw new JsonSyntaxException("Un-closed hyperlink marker in string "
+ StringUtils.abbreviate(raw, 50));
List<String> upToLink = font.listFormattedStringToWidth(paragraph.substring(0, linkStart), GuiWizardHandbook.PAGE_WIDTH);
String linkRaw = paragraph.substring(linkStart, linkEnd + 1);
String[] arguments = paragraph.substring(linkStart + 1, linkEnd).split("\\s", 2);
String suffix = paragraph.substring(linkEnd).split("\\s", 2)[0];
// The index of the single page currently being formatted, relative to the section
int pageRelative = (lines.size() + upToLink.size() - 1) / maxLineNumber;
// The overall index of the single page currently being formatted
int page = startPage + pageRelative;
int x = GuiWizardHandbook.isRightPage(page) ? left + GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : left + GuiWizardHandbook.TEXT_INSET_X;
int y = top + GuiWizardHandbook.TEXT_INSET_Y + (((lines.size() + upToLink.size() - 1) % maxLineNumber)) * font.FONT_HEIGHT;
// Adds any missing sub-lists
while(this.buttons.size() <= pageRelative){
this.buttons.add(new ArrayList<>());
}
// The button id only does what you use it for, so we're just not using it at all.
this.buttons.get(pageRelative).add(GuiButtonHyperlink.create(x, y, font, upToLink, arguments, suffix));
// The link button should exactly overlay the display rawText in the main string
// If the link has no display rawText specified, it displays the unformatted target string
paragraph = paragraph.replace(linkRaw, arguments[arguments.length - 1]);
}
// Formatting
for(Map.Entry<String, String> entry : GuiWizardHandbook.FORMAT_TAGS.entrySet()){
paragraph = paragraph.replace(GuiWizardHandbook.FORMAT_MARKER + entry.getKey(), entry.getValue());
}
lines.addAll(font.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
}
// Line break between paragraphs (the last one will just be deleted later)
if((lines.size() % maxLineNumber) != 0) lines.add("");
}
}
// Splits lines into pages
List<String> page = new ArrayList<>();
pages.add(page);
while(!lines.isEmpty()){
if(page.size() == maxLineNumber){
// Removes blank lines at the end of the page
while(page.get(page.size() - 1).isEmpty()) page.remove(page.size() - 1);
// Adds a new page
pages.add(page = new ArrayList<>());
}
String line = lines.remove(0);
// Prevents blank lines at the start of the page
if(!page.isEmpty() || !line.isEmpty()) page.add(line);
}
return startPage + pages.size();
}
/**
* Parses the given JSON object and constructs a new {@code Section} from it, setting all the relevant fields
* and references. This method converts the JSON object to a {@code Section} object and retrieves any resources;
* the section is not formatted in any way until GUI load, in {@link Section#format(FontRenderer, int, int, int)}.
*
* @param json A JSON object representing the section to be constructed. This must contain at least a "title"
* string.
* @return The resulting {@code Section} object.
* @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
*/
static Section fromJson(JsonObject json){
Section section = new Section();
section.title = JsonUtils.getString(json, "title", "");
if(JsonUtils.hasField(json, "include_in_contents")){
String id = JsonUtils.getString(json, "include_in_contents");
Contents belongsTo = GuiWizardHandbook.contentsList.get(id);
if(belongsTo == null){
throw new JsonSyntaxException("Expected include_in_contents to be the id of a previously defined contents, but no contents with the id " + id + " exists yet.");
}else{
belongsTo.addEntry(section);
}
}
if(JsonUtils.hasField(json, "contents")){
section.contents = Contents.fromJson(JsonUtils.getJsonObject(json, "contents"));
GuiWizardHandbook.contentsList.put(section.contents.id, section.contents);
}
if(JsonUtils.hasField(json, "text")){
// TESTME: Does this always preserve order?
section.rawText = Streams.stream(JsonUtils.getJsonArray(json, "text"))
.map(e -> JsonUtils.getString(e, "element of array rawText"))
.toArray(String[]::new);
}
// TODO: Triggers
if(JsonUtils.hasField(json, "centre")){
JsonObject centre = JsonUtils.getJsonObject(json,"centre");
section.centreX = JsonUtils.getBoolean(centre, "x", false);
section.centreY = JsonUtils.getBoolean(centre, "y", false);
}
// The only benefit of having subsections (other than logical grouping) is that the parent section can
// automatically be unlocked if one of the subsections is.
if(JsonUtils.hasField(json, "sections")){
populate(section.subsections, json);
}
return section;
}
static void populate(Map<String, Section> map, JsonObject json){
JsonObject sectionsObject = JsonUtils.getJsonObject(json, "sections");
// Need to iterate over these since we don't know what they're called or how many there are
for(Map.Entry<String, JsonElement> entry : sectionsObject.entrySet()){
String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
Section section = fromJson(entry.getValue().getAsJsonObject());
map.put(key, section);
map.putAll(section.subsections);
}
}
public void onAdvancement(EntityPlayer player, Advancement advancement){
Minecraft minecraft = Minecraft.getMinecraft();
//System.out.println(title);
if(triggers != null && Arrays.asList(triggers).contains(advancement) && player == minecraft.player){
//minecraft.getToastGui().drawToast(IToast);
// Debug
//System.out.println(title);
}
}
}