forked from shadowfacts/shadowfacts.net
Initial commit
This commit is contained in:
commit
2e60126ded
|
@ -0,0 +1,4 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
built/
|
||||||
|
out/
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Generate",
|
||||||
|
"program": "${workspaceFolder}/lib/index.ts",
|
||||||
|
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/built/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules": true,
|
||||||
|
"out": true,
|
||||||
|
"built": true,
|
||||||
|
},
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.insertSpaces": false
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Page, PostMetadata } from "../metadata";
|
||||||
|
import generatePaginated from "./paginated";
|
||||||
|
|
||||||
|
export default async function homepage(posts: Page[]): Promise<Map<string, Page[]>> {
|
||||||
|
const categories = new Map<string, Page[]>();
|
||||||
|
|
||||||
|
for (const post of posts) {
|
||||||
|
const category = (<PostMetadata>post.metadata).category;
|
||||||
|
if (!categories.has(category)) {
|
||||||
|
categories.set(category, []);
|
||||||
|
}
|
||||||
|
categories.get(category)!.push(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.forEach(async (categoryPosts, category) => {
|
||||||
|
await generatePaginated(categoryPosts, `/${category}/`, "site/category.html.ejs", {
|
||||||
|
category
|
||||||
|
}, {
|
||||||
|
title: `${category} posts`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import * as util from "../util";
|
||||||
|
import sass, { Result as SassResult } from "node-sass";
|
||||||
|
import ejs from "ejs";
|
||||||
|
|
||||||
|
function renderSass(data: string, minify = false): Promise<SassResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
sass.render({
|
||||||
|
data: data,
|
||||||
|
outputStyle: minify ? "compressed" : "expanded"
|
||||||
|
}, (error, result) => {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate(theme: string) {
|
||||||
|
const filename = `site/css/${theme}.scss`;
|
||||||
|
|
||||||
|
let sass = (await fs.readFile(filename)).toString();
|
||||||
|
sass = ejs.render(sass, {}, {
|
||||||
|
filename: filename
|
||||||
|
});
|
||||||
|
const result = await renderSass(sass);
|
||||||
|
|
||||||
|
util.write(`css/${theme}.css`, result.css);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function css() {
|
||||||
|
await generate("light");
|
||||||
|
await generate("dark");
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Page } from "../metadata";
|
||||||
|
import generatePaginated from "./paginated";
|
||||||
|
|
||||||
|
export default async function homepage(posts: Page[]) {
|
||||||
|
await generatePaginated(posts, "/", "site/index.html.ejs");
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import categories from "./categories";
|
||||||
|
import css from "./css";
|
||||||
|
import homepage from "./homepage";
|
||||||
|
import missing from "./missing";
|
||||||
|
import posts from "./posts";
|
||||||
|
import redirects from "./redirects";
|
||||||
|
import rss from "./rss";
|
||||||
|
import tutorials from "./tutorials";
|
||||||
|
|
||||||
|
export = {
|
||||||
|
categories,
|
||||||
|
css,
|
||||||
|
homepage,
|
||||||
|
missing,
|
||||||
|
posts,
|
||||||
|
redirects,
|
||||||
|
rss,
|
||||||
|
tutorials
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import ejs from "ejs";
|
||||||
|
import * as metadata from "../metadata";
|
||||||
|
import layout from "../layout";
|
||||||
|
import * as util from "../util";
|
||||||
|
|
||||||
|
export default async function missing() {
|
||||||
|
const page = await metadata.get("site/404.html.ejs");
|
||||||
|
|
||||||
|
page.text = ejs.render(page.text, {
|
||||||
|
metadata: page.metadata
|
||||||
|
}, {
|
||||||
|
filename: "site/404.html.ejs"
|
||||||
|
});
|
||||||
|
|
||||||
|
page.text = await layout(page.text, page.metadata, page.metadata.layout!);
|
||||||
|
|
||||||
|
util.write("404.html", page.text);
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import path from "path";
|
||||||
|
import ejs from "ejs";
|
||||||
|
import formatDate from "date-fns/format";
|
||||||
|
import * as util from "../util";
|
||||||
|
import * as metadata from "../metadata";
|
||||||
|
import { Page, PostMetadata } from "../metadata";
|
||||||
|
import layout from "../layout";
|
||||||
|
|
||||||
|
export default async function generatePaginted(posts: Page[], basePath: string, templatePath: string, extraData?: object, extraMetadata?: object) {
|
||||||
|
const page = await metadata.get(templatePath);
|
||||||
|
|
||||||
|
if (extraMetadata) page.metadata = {...page.metadata, ...extraMetadata};
|
||||||
|
|
||||||
|
posts = posts.sort((a, b) => {
|
||||||
|
const aDate = <Date>(<PostMetadata>a.metadata).date;
|
||||||
|
const bDate = <Date>(<PostMetadata>b.metadata).date;
|
||||||
|
return bDate.getTime() - aDate.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunks = util.chunk(posts, 5);
|
||||||
|
for (const {chunk, index} of chunks) {
|
||||||
|
const pageNum = index + 1;
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
metadata: page.metadata,
|
||||||
|
posts: chunk,
|
||||||
|
pagination: {
|
||||||
|
current: pageNum,
|
||||||
|
total: chunks.length,
|
||||||
|
prevLink: pageNum == 1 ? "" : pageNum == 2 ? basePath : path.join(basePath, (pageNum - 1).toString()),
|
||||||
|
nextLink: pageNum == chunks.length ? "" : path.join(basePath, (pageNum + 1).toString())
|
||||||
|
},
|
||||||
|
formatDate
|
||||||
|
};
|
||||||
|
if (extraData) data = {...data, ...extraData};
|
||||||
|
|
||||||
|
let renderedTemplate = ejs.render(page.text, data, {
|
||||||
|
filename: templatePath
|
||||||
|
});
|
||||||
|
|
||||||
|
renderedTemplate = await layout(renderedTemplate, page.metadata, page.metadata.layout!);
|
||||||
|
|
||||||
|
util.write(path.join(basePath, pageNum.toString(), "index.html"), renderedTemplate);
|
||||||
|
|
||||||
|
if (pageNum == 1) {
|
||||||
|
util.write(path.join(basePath, "index.html"), renderedTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import dateFns from "date-fns";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import * as util from "../util";
|
||||||
|
import * as metadata from "../metadata";
|
||||||
|
import { Page, PostMetadata } from "../metadata";
|
||||||
|
import * as markdown from "../markdown";
|
||||||
|
import layout from "../layout";
|
||||||
|
|
||||||
|
export default async function posts(): Promise<Page[]> {
|
||||||
|
const posts: Page[] = [];
|
||||||
|
|
||||||
|
const files = await fs.readdir("site/posts");
|
||||||
|
for (const f of files) {
|
||||||
|
let page = await metadata.get(path.join("site/posts", f));
|
||||||
|
|
||||||
|
if (!(<PostMetadata>page.metadata).permalink) {
|
||||||
|
let postMeta = <PostMetadata>page.metadata;
|
||||||
|
postMeta.date = dateFns.parse(postMeta.date);
|
||||||
|
postMeta.slug = postMeta.slug || slugify(postMeta.title);
|
||||||
|
postMeta.permalink = `/${postMeta.category}/${postMeta.date.getFullYear()}/${postMeta.slug}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.metadata.source && page.metadata.source!.endsWith(".md")) {
|
||||||
|
page.text = markdown.render(page.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(<PostMetadata>page.metadata).excerpt) {
|
||||||
|
const parts = page.text.split("<!-- excerpt-end -->");
|
||||||
|
(<PostMetadata>page.metadata).excerpt = parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedText = await layout(page.text, page.metadata, page.metadata.layout || "article.html.ejs");
|
||||||
|
|
||||||
|
let dest = page.metadata.permalink;
|
||||||
|
if (dest.endsWith("/")) {
|
||||||
|
dest += "index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
util.write(dest, renderedText);
|
||||||
|
|
||||||
|
posts.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import ejs from "ejs";
|
||||||
|
import * as metadata from "../metadata";
|
||||||
|
import { Page } from "../metadata";
|
||||||
|
import layout from "../layout";
|
||||||
|
import * as util from "../util";
|
||||||
|
|
||||||
|
async function generateRedirect(oldPermalink: string, newPermalink: string) {
|
||||||
|
const page = await metadata.get("site/redirect.html.ejs");
|
||||||
|
|
||||||
|
page.text = ejs.render(page.text, {
|
||||||
|
metadata: page.metadata,
|
||||||
|
newPermalink
|
||||||
|
}, {
|
||||||
|
filename: "site/redirect.html.ejs"
|
||||||
|
});
|
||||||
|
|
||||||
|
page.text = await layout(page.text, page.metadata, page.metadata.layout!);
|
||||||
|
|
||||||
|
if (oldPermalink.endsWith("/")) {
|
||||||
|
oldPermalink += "index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
util.write(oldPermalink, page.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function redirects(posts: Page[]) {
|
||||||
|
for (const post of posts) {
|
||||||
|
if (post.metadata.oldPermalink) {
|
||||||
|
await generateRedirect(post.metadata.oldPermalink, post.metadata.permalink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import ejs from "ejs";
|
||||||
|
import path from "path";
|
||||||
|
import { Page, PostMetadata } from "../metadata";
|
||||||
|
import * as util from "../util";
|
||||||
|
import { TutorialSeries } from "./tutorials";
|
||||||
|
|
||||||
|
async function generateFeed(posts: Page[], permalink: string, category?: string) {
|
||||||
|
posts = posts.sort((a, b) => {
|
||||||
|
const aDate = <Date>(<PostMetadata>a.metadata).date;
|
||||||
|
const bDate = <Date>(<PostMetadata>b.metadata).date;
|
||||||
|
return bDate.getTime() - aDate.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dest = path.join(permalink, "feed.xml");
|
||||||
|
|
||||||
|
let text = (await fs.readFile("site/feed.xml.ejs")).toString();
|
||||||
|
text = ejs.render(text, {
|
||||||
|
posts,
|
||||||
|
category,
|
||||||
|
permalink,
|
||||||
|
feedPath: dest
|
||||||
|
}, {
|
||||||
|
filename: "site/feed.xml.ejs"
|
||||||
|
});
|
||||||
|
|
||||||
|
util.write(dest, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function rss(posts: Page[], categories: Map<string, Page[]>, tutorials: TutorialSeries[]) {
|
||||||
|
generateFeed(posts, "/");
|
||||||
|
categories.forEach((posts, category) => {
|
||||||
|
generateFeed(posts, `/${category}/`, category);
|
||||||
|
});
|
||||||
|
tutorials.forEach(series => {
|
||||||
|
generateFeed(series.posts, `/tutorials/${series.series}/`, series.seriesName);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import dateFns from "date-fns";
|
||||||
|
import formatDate from "date-fns/format";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import { Page, PostMetadata } from "../metadata";
|
||||||
|
import * as metadata from "../metadata";
|
||||||
|
import * as markdown from "../markdown";
|
||||||
|
import layout from "../layout";
|
||||||
|
import * as util from "../util";
|
||||||
|
import ejs from "ejs";
|
||||||
|
|
||||||
|
async function generateTutorials(group: string): Promise<Page[]> {
|
||||||
|
const tutorials: Page[] = [];
|
||||||
|
|
||||||
|
const files = await fs.readdir(`site/tutorials/${group}`);
|
||||||
|
for (const f of files) {
|
||||||
|
let page = await metadata.get(path.join("site/tutorials", group, f));
|
||||||
|
|
||||||
|
if (!(<PostMetadata>page.metadata).permalink) {
|
||||||
|
let postMeta = <PostMetadata>page.metadata;
|
||||||
|
postMeta.date = dateFns.parse(postMeta.date);
|
||||||
|
postMeta.slug = postMeta.slug || slugify(postMeta.title);
|
||||||
|
postMeta.permalink = `/tutorials/${group}/${postMeta.slug}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.metadata.source && page.metadata.source!.endsWith(".md")) {
|
||||||
|
page.text = markdown.render(page.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedText = await layout(page.text, page.metadata, page.metadata.layout || "article.html.ejs");
|
||||||
|
|
||||||
|
let dest = page.metadata.permalink;
|
||||||
|
if (dest.endsWith("/")) {
|
||||||
|
dest += "index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
util.write(dest, renderedText);
|
||||||
|
|
||||||
|
tutorials.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tutorials;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TutorialSeries {
|
||||||
|
index: Page;
|
||||||
|
posts: Page[];
|
||||||
|
series: string;
|
||||||
|
seriesName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TutorialIndexMetadata extends PostMetadata {
|
||||||
|
lastUpdated?: Date;
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateTutorialsAndIndex(group: string, title: string): Promise<TutorialSeries> {
|
||||||
|
let tutorials = await generateTutorials(group);
|
||||||
|
tutorials = tutorials.sort((a, b) => {
|
||||||
|
const aDate = <Date>(<PostMetadata>a.metadata).date;
|
||||||
|
const bDate = <Date>(<PostMetadata>b.metadata).date;
|
||||||
|
return aDate.getTime() - bDate.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await metadata.get("site/tutorial-series.html.ejs");
|
||||||
|
|
||||||
|
(<PostMetadata>page.metadata).permalink = `/tutorials/${group}/`;
|
||||||
|
(<PostMetadata>page.metadata).title = title;
|
||||||
|
(<TutorialIndexMetadata>page.metadata).group = group;
|
||||||
|
(<TutorialIndexMetadata>page.metadata).lastUpdated = <Date>(<PostMetadata>tutorials[tutorials.length - 1].metadata).date
|
||||||
|
|
||||||
|
page.text = ejs.render(page.text, {
|
||||||
|
tutorials,
|
||||||
|
metadata: page.metadata,
|
||||||
|
formatDate
|
||||||
|
}, {
|
||||||
|
filename: "site/tutorial-series.html.ejs"
|
||||||
|
});
|
||||||
|
|
||||||
|
page.text = await layout(page.text, page.metadata, page.metadata.layout!);
|
||||||
|
|
||||||
|
util.write(path.join("tutorials", group, "index.html"), page.text);
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: page,
|
||||||
|
posts: tutorials,
|
||||||
|
series: group,
|
||||||
|
seriesName: title
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateIndex(allSeries: TutorialSeries[]) {
|
||||||
|
const page = await metadata.get("site/tutorials.html.ejs");
|
||||||
|
|
||||||
|
page.text = ejs.render(page.text, {
|
||||||
|
allSeries,
|
||||||
|
formatDate
|
||||||
|
}, {
|
||||||
|
filename: "site/tutorials.html.ejs"
|
||||||
|
});
|
||||||
|
|
||||||
|
page.text = await layout(page.text, page.metadata, page.metadata.layout!);
|
||||||
|
|
||||||
|
util.write("tutorials/index.html", page.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function tutorials(): Promise<TutorialSeries[]> {
|
||||||
|
const series = [
|
||||||
|
await generateTutorialsAndIndex("forge-modding-1102", "Forge Mods for 1.10.2"),
|
||||||
|
await generateTutorialsAndIndex("forge-modding-1112", "Forge Mods for 1.11.2"),
|
||||||
|
await generateTutorialsAndIndex("forge-modding-112", "Forge Mods for 1.12")
|
||||||
|
];
|
||||||
|
|
||||||
|
generateIndex(series);
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import generators = require("./generate");
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
generators.css();
|
||||||
|
generators.missing();
|
||||||
|
|
||||||
|
const tutorials = await generators.tutorials();
|
||||||
|
|
||||||
|
const posts = await generators.posts();
|
||||||
|
generators.homepage(posts);
|
||||||
|
generators.redirects(posts);
|
||||||
|
const categories = await generators.categories(posts);
|
||||||
|
generators.rss(posts, categories, tutorials);
|
||||||
|
}
|
||||||
|
|
||||||
|
generate();
|
|
@ -0,0 +1,22 @@
|
||||||
|
import path from "path";
|
||||||
|
import ejs from "ejs";
|
||||||
|
import formatDate from "date-fns/format";
|
||||||
|
import * as metadata from "./metadata";
|
||||||
|
import { Metadata } from "./metadata";
|
||||||
|
|
||||||
|
export default async function layout(text: string, pageMetadata: Metadata, layoutPath: string): Promise<string> {
|
||||||
|
const layoutFile = path.join("site/layouts", layoutPath);
|
||||||
|
let layoutPage = await metadata.get(layoutFile);
|
||||||
|
text = ejs.render(layoutPage.text, {
|
||||||
|
content: text,
|
||||||
|
metadata: pageMetadata,
|
||||||
|
formatDate
|
||||||
|
}, {
|
||||||
|
filename: layoutFile,
|
||||||
|
});
|
||||||
|
if (layoutPage.metadata.layout) {
|
||||||
|
return await layout(text, pageMetadata, layoutPage.metadata.layout);
|
||||||
|
} else {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import * as util from "./util";
|
||||||
|
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
highlight: util.highlight,
|
||||||
|
html: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export function render(text: string): string {
|
||||||
|
return md.render(text);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
export interface Page {
|
||||||
|
text: string;
|
||||||
|
metadata: Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
permalink: string;
|
||||||
|
source?: string;
|
||||||
|
layout?: string;
|
||||||
|
oldPermalink?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostMetadata extends Metadata {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
category: string;
|
||||||
|
date: string | Date;
|
||||||
|
excerpt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(path: string): Promise<Page> {
|
||||||
|
let text = (await fs.readFile(path)).toString();
|
||||||
|
let metadata = {
|
||||||
|
source: path
|
||||||
|
} as Metadata;
|
||||||
|
if (text.startsWith("```")) {
|
||||||
|
const parts = text.split("```");
|
||||||
|
text = parts.slice(2).join("```");
|
||||||
|
const configure = new Function("metadata", parts[1]);
|
||||||
|
configure(metadata);
|
||||||
|
}
|
||||||
|
return {text, metadata};
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
|
||||||
|
export async function write(filePath: string, data: any) {
|
||||||
|
const dest = path.join("out", filePath);
|
||||||
|
await fs.mkdir(path.dirname(dest), {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
await fs.writeFile(dest, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlight(source: string, language?: string): string {
|
||||||
|
const res = language ? hljs.highlight(language, source) : hljs.highlightAuto(source);
|
||||||
|
const highlighted = res.value;
|
||||||
|
return `<pre class="hljs"><code>${highlighted}</code></pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Chunk<T> {
|
||||||
|
chunk: T[];
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chunk<T>(array: T[], size: number): Chunk<T>[] {
|
||||||
|
const chunks: Chunk<T>[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < array.length; i += size) {
|
||||||
|
chunks.push({
|
||||||
|
chunk: array.slice(i, i + size),
|
||||||
|
index: i / size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "shadowfacts.net",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"generate": "tsc -p . && node built/index.js",
|
||||||
|
"serve": "npm run generate && http-server out"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sindresorhus/slugify": "^0.6.0",
|
||||||
|
"date-fns": "^1.30.1",
|
||||||
|
"ejs": "^2.6.1",
|
||||||
|
"highlight.js": "^9.13.1",
|
||||||
|
"markdown-it": "^8.4.2",
|
||||||
|
"node-sass": "^4.11.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ejs": "^2.6.1",
|
||||||
|
"@types/highlight.js": "^9.12.3",
|
||||||
|
"@types/markdown-it": "0.0.7",
|
||||||
|
"@types/node-sass": "^3.10.32",
|
||||||
|
"@types/sindresorhus__slugify": "^0.6.0",
|
||||||
|
"http-server": "^0.11.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Not Found"
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main search">
|
||||||
|
<h1 class="page-heading">Unable to find what you were looking for.</h1>
|
||||||
|
<h3>Try searching:</h3>
|
||||||
|
<form action="https://www.google.com/search" method="GET">
|
||||||
|
<input type="hidden" name="q" value="site:shadowfacts.net">
|
||||||
|
<input type="text" name="q" id="q" placeholder="Search...">
|
||||||
|
<input type="submit" value="Search">
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,48 @@
|
||||||
|
```
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<h1 class="page-heading"><%= category %> posts</h1>
|
||||||
|
<p class="rss">
|
||||||
|
Subscribe to just <%= category %> posts via <a href="/<%= category %>/feed.xml">RSS</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% for (const post of posts) { %>
|
||||||
|
<article itemscope itemtype="https://schema.org/BlogPosting">
|
||||||
|
<h2 class="article-title" itemprop="headline">
|
||||||
|
<a href="<%= post.metadata.permalink %>" class="fancy-link" itemprop="url mainEntityOfPage">
|
||||||
|
<%= post.metadata.title %>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<%- include("includes/article-meta.html.ejs", { metadata: post.metadata }) %>
|
||||||
|
<div class="article-content" itemprop="description">
|
||||||
|
<%- post.metadata.excerpt %>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<p>
|
||||||
|
<span class="pagination-link">
|
||||||
|
<% if (pagination.prevLink) { %>
|
||||||
|
<a href="<%= pagination.prevLink %>">
|
||||||
|
<span class="arrow-left"></span> Previous
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="arrow-left"></span> Previous
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
|
Page <%= pagination.current %> of <%= pagination.total %>
|
||||||
|
<span class="pagination-link">
|
||||||
|
<% if (pagination.nextLink) { %>
|
||||||
|
<a href="<%= pagination.nextLink %>">
|
||||||
|
Next <span class="arrow-right"></span>
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
Next <span class="arrow-right"></span>
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
:root {
|
||||||
|
--accent-color: #f9c72f;
|
||||||
|
--content-background-color: #111;
|
||||||
|
--shadow-color: #151515;
|
||||||
|
--ui-background-color: #111;
|
||||||
|
--ui-text-color: white;
|
||||||
|
--secondary-ui-text-color: #999;
|
||||||
|
--content-text-color: #ddd;
|
||||||
|
|
||||||
|
// Syntax highlighting
|
||||||
|
--atom-base: #282c34;
|
||||||
|
--atom-mono-1: #abb2bf;
|
||||||
|
--atom-mono-2: #818896;
|
||||||
|
--atom-mono-3: #5c6370;
|
||||||
|
--atom-hue-1: #56b6c2;
|
||||||
|
--atom-hue-2: #61aeee;
|
||||||
|
--atom-hue-3: #c678dd;
|
||||||
|
--atom-hue-4: #98c379;
|
||||||
|
--atom-hue-5: #e06c75;
|
||||||
|
--atom-hue-5-2: #be5046;
|
||||||
|
--atom-hue-6: #d19a66;
|
||||||
|
--atom-hue-6-2: #e6c07b;
|
||||||
|
}
|
||||||
|
|
||||||
|
<%- include("main.scss") %>
|
|
@ -0,0 +1,25 @@
|
||||||
|
:root {
|
||||||
|
--accent-color: #0638d0;
|
||||||
|
--content-background-color: white;
|
||||||
|
--shadow-color: #f7f7f7;
|
||||||
|
--ui-background-color: white;
|
||||||
|
--ui-text-color: black;
|
||||||
|
--secondary-ui-text-color: #666;
|
||||||
|
--content-text-color: #222;
|
||||||
|
|
||||||
|
// Syntax highlighting
|
||||||
|
--atom-base: #fafafa;
|
||||||
|
--atom-mono-1: #383a42;
|
||||||
|
--atom-mono-2: #686b77;
|
||||||
|
--atom-mono-3: #a0a1a7;
|
||||||
|
--atom-hue-1: #0184bb;
|
||||||
|
--atom-hue-2: #4078f2;
|
||||||
|
--atom-hue-3: #a626a4;
|
||||||
|
--atom-hue-4: #50a14f;
|
||||||
|
--atom-hue-5: #e45649;
|
||||||
|
--atom-hue-5-2: #c91243;
|
||||||
|
--atom-hue-6: #986801;
|
||||||
|
--atom-hue-6-2: #c18401;
|
||||||
|
}
|
||||||
|
|
||||||
|
<%- include("main.scss") %>
|
|
@ -0,0 +1,357 @@
|
||||||
|
<%- include("normalize.css") %>
|
||||||
|
<%- include("syntax-highlighting.css") %>
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
$sansSerif: Lucida Grande, Arial, sans-serif;
|
||||||
|
$serif: Georgia, serif;
|
||||||
|
$monospace: SF Mono, Monaco, Courier New, monospace;
|
||||||
|
|
||||||
|
// General
|
||||||
|
body {
|
||||||
|
background-color: var(--content-background-color);
|
||||||
|
font-family: $sansSerif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--ui-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.page-heading {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 20px auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--content-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rss {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: var(--secondary-ui-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
// max-width: 720px;
|
||||||
|
// margin: 0 auto;
|
||||||
|
margin-bottom: 75px;
|
||||||
|
color: var(--content-text-color);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
height: 1px;
|
||||||
|
background-image: linear-gradient(to right, var(--secondary-ui-text-color), var(--shadow-color));
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
color: var(--content-text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: 0.3s ease all;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: var(--secondary-ui-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content {
|
||||||
|
font-family: $serif;
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: $sansSerif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin: 100px auto;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
position: relative;
|
||||||
|
color: var(--content-text-color);
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#q {
|
||||||
|
display: block;
|
||||||
|
width: 75%;
|
||||||
|
margin: 10px auto;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: var(--content-background-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--content-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
display: block;
|
||||||
|
margin: 10px auto;
|
||||||
|
background-color: var(--ui-background-color);
|
||||||
|
border: 1px solid var(--accent-color);
|
||||||
|
color: var(--accent-color);
|
||||||
|
line-height: 2rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
-webkit-transition: 0.3s ease-out;
|
||||||
|
transition: 0.3s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: var(--ui-background-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon > svg {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent-color);
|
||||||
|
|
||||||
|
&.fancy-link {
|
||||||
|
position: relative;
|
||||||
|
color: var(--ui-text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: 0.3s ease all;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
position: absolute;
|
||||||
|
transition: 0.3s ease all;
|
||||||
|
font-family: $monospace;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "{";
|
||||||
|
left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "}";
|
||||||
|
right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: -0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: -0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
font-family: $monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
figcaption {
|
||||||
|
font-family: $sansSerif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--secondary-ui-text-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
.site-header {
|
||||||
|
padding: 20px 0;
|
||||||
|
background-color: var(--ui-background-color);
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: 0 10px 15px var(--shadow-color);
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2em;
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-description {
|
||||||
|
color: var(--secondary-ui-text-color);
|
||||||
|
font-variant: small-caps;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 1em;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.site-nav {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
.site-footer {
|
||||||
|
padding: 20px 0;
|
||||||
|
background-color: var(--ui-background-color);
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: 0 -10px 15px var(--shadow-color);
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
margin: 0;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 1em;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.social-links {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
.pagination {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.pagination-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
span:not(.arrow) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-left {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
margin-right: -5px;
|
||||||
|
border-left: 2px solid var(--accent-color);
|
||||||
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.arrow-right {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-right: 2px solid var(--accent-color);
|
||||||
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media Queries
|
||||||
|
@media (min-width: 540px) {
|
||||||
|
.container {
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.container {
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1140px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input { /* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select { /* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Atom One color scheme by Daniel Gamage
|
||||||
|
Modified to use colors from CSS vars, defined in theme.scss
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: var(--atom-mono-1);
|
||||||
|
background: var(--atom-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: var(--atom-mono-3);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-formula {
|
||||||
|
color: var(--atom-hue-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-subst {
|
||||||
|
color: var(--atom-hue-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: var(--atom-hue-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-meta-string {
|
||||||
|
color: var(--atom-hue-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-class .hljs-title {
|
||||||
|
color: var(--atom-hue-6-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-number {
|
||||||
|
color: var(--atom-hue-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-title {
|
||||||
|
color: var(--atom-hue-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>Shadowfacts<% if (category) { %> (<%= category %>)<% } %></title>
|
||||||
|
<subtitle>
|
||||||
|
<% if (category) { %>
|
||||||
|
Only <%= category %> posts.
|
||||||
|
<% } else { %>
|
||||||
|
Just my various ramblings.
|
||||||
|
<% } %>
|
||||||
|
</subtitle>
|
||||||
|
<link rel="alternate" type="text/html" href="https://shadowfacts.net<%= permalink %>" />
|
||||||
|
<link rel="self" href="https://shadowfacts.net<%= feedPath %>" type="application/atom+xml" />
|
||||||
|
<id>https://shadowfacts.net<%= feedPath %></id>
|
||||||
|
<updated><%= new Date().toISOString() %></updated>
|
||||||
|
<author>
|
||||||
|
<name>Shadowfacts</name>
|
||||||
|
</author>
|
||||||
|
<% for (const post of posts) { %>
|
||||||
|
<entry>
|
||||||
|
<id>https://shadowfacts.net<%= post.metadata.permalink %></id>
|
||||||
|
<title><%= post.metadata.title %></title>
|
||||||
|
<updated><%= post.metadata.date.toISOString() %></updated>
|
||||||
|
<link rel="alternate" type="text/html" href="https://shadowfacts.net<%= post.metadata.permalink %>" />
|
||||||
|
<% if (post.metadata.category) { %>
|
||||||
|
<category term="<%= post.metadata.category %>" />
|
||||||
|
<% } %>
|
||||||
|
<content type="html"><![CDATA[
|
||||||
|
<%- post.text %>
|
||||||
|
]]></content>
|
||||||
|
</entry>
|
||||||
|
<% } %>
|
||||||
|
</feed>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<p class="article-meta">
|
||||||
|
on
|
||||||
|
<span>
|
||||||
|
<% const formatted = formatDate(metadata.date, "MMM Do, YYYY") %>
|
||||||
|
<time itemprop="datePublished" datetime="<%= metadata.date.toISOString() %>"><%= formatted %></time>
|
||||||
|
</span>
|
||||||
|
<% if (metadata.category) { %>
|
||||||
|
in
|
||||||
|
<span itemprop="articleSection"><a href="/<%= metadata.category %>/"><%= metadata.category %></a></span>
|
||||||
|
<% } else if (metadata.series) { %>
|
||||||
|
in
|
||||||
|
<span itemprop="articleSection"><a href="/tutorials/<%= metadata.series %>"><%= metadata.seriesName %></a></span>
|
||||||
|
<% } %>
|
||||||
|
by
|
||||||
|
<span itemprop="author">Shadowfacts</span>
|
||||||
|
</p>
|
|
@ -0,0 +1,44 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Shadowfacts"
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<% for (const post of posts) { %>
|
||||||
|
<article itemscope itemtype="https://schema.org/BlogPosting">
|
||||||
|
<h2 class="article-title" itemprop="headline">
|
||||||
|
<a href="<%= post.metadata.permalink %>" class="fancy-link" itemprop="url mainEntityOfPage">
|
||||||
|
<%= post.metadata.title %>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<%- include("includes/article-meta.html.ejs", { metadata: post.metadata }) %>
|
||||||
|
<div class="article-content" itemprop="description">
|
||||||
|
<%- post.metadata.excerpt %>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<p>
|
||||||
|
<span class="pagination-link">
|
||||||
|
<% if (pagination.prevLink) { %>
|
||||||
|
<a href="<%= pagination.prevLink %>">
|
||||||
|
<span class="arrow arrow-left"></span> <span>Previous</span>
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="arrow arrow-left"></span> <span>Previous</span>
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
|
Page <%= pagination.current %> of <%= pagination.total %>
|
||||||
|
<span class="pagination-link">
|
||||||
|
<% if (pagination.nextLink) { %>
|
||||||
|
<a href="<%= pagination.nextLink %>">
|
||||||
|
<span>Next</span> <span class="arrow arrow-right"></span>
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<span>Next</span> <span class="arrow arrow-right"></span>
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,14 @@
|
||||||
|
```
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<article itemscope itemtype="https://schema.org/BlogPosting">
|
||||||
|
<meta itemprop="mainEntityOfPage" content="https://shadowfacts.net<%= metadata.permalink %>">
|
||||||
|
<h1 class="article-title" itemprop="headline"><%= metadata.title %></h1>
|
||||||
|
<%- include("../includes/article-meta.html.ejs", { metadata }) %>
|
||||||
|
<div class="article-content" itemprop="articleBody">
|
||||||
|
<%- content %>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title><%= metadata.title %></title>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
let theme = localStorage.getItem("theme");
|
||||||
|
if (theme !== "light" && theme !== "dark") {
|
||||||
|
theme = "light";
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}
|
||||||
|
document.write(`<link rel="stylesheet" href="/css/${theme}.css">`);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<noscript>
|
||||||
|
<link rel="stylesheet" href="/css/light.css">
|
||||||
|
</noscript>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="site-title"><a href="/" class="fancy-link">Shadowfacts</a></h1>
|
||||||
|
<p class="site-description">The outper part of a shadow is called the penumbra.</p>
|
||||||
|
<nav class="site-nav">
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://mememachine.shadowfacts.net" class="fancy-link">Meme Machine</a></li>
|
||||||
|
<li><a href="https://rtfm.shadowfacts.net" class="fancy-link">RTFM</a></li>
|
||||||
|
<li><a href="https://type.shadowfacts.net" class="fancy-link">Type</a></li>
|
||||||
|
<li><a href="/tutorials/" class="fancy-link">Tutorials</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<%- content %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<div>
|
||||||
|
<h2 class="site-title">Shadowfacts</h2>
|
||||||
|
<p class="ui-controls">
|
||||||
|
<label for="dark-theme">Dark Theme: </label>
|
||||||
|
<input type="checkbox" name="" id="dark-theme">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<nav class="social-links">
|
||||||
|
<ul>
|
||||||
|
<li><a href="mailto:me@shadowfacts.net" class="fancy-link">Email</a></li>
|
||||||
|
<li><a href="/feed.xml" class="fancy-link">RSS</a></li>
|
||||||
|
<li><a href="https://github.com/shadowfacts" class="fancy-link">GitHub</a></li>
|
||||||
|
<li><a href="https://twitter.com/ShadowfactsDev" class="fancy-link">Twitter</a></li>
|
||||||
|
<li><a href="https://social.shadowfacts.net/users/shadowfacts" class="fancy-link">Mastodon</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const el = document.getElementById("dark-theme");
|
||||||
|
el.checked = localStorage.getItem("theme") === "dark";
|
||||||
|
el.onclick = function() {
|
||||||
|
const theme = this.checked ? "dark" : "light";
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
.ui-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</noscript>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,7 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Hello, World!"
|
||||||
|
metadata.category = "meta"
|
||||||
|
metadata.date = "2016-05-06 11:13:18 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
Hello, again, world! Welcome to the third iteration of my website. Originally my site was hosted on GitHub pages and only available at [shadowfacts.github.io](https://shadowfacts.github.io). I wrote a couple of tutorials on using [Forge](http://minecraftforge.net) to mod 1.6.4, but never really finished anything other than super basic setup/recipes. Later, after I got [shadowfacts.net](https://shadowfacts.net), I decided to set up a propper website using [WordPress](https://wordpress.org). I copied over all of the old tutorials from my old GitHub pages site, but never really did anything else with it. After my website being offline for almost a year, I've finally decided to switch back to GitHub for the simplicity (also I <3 [Jekyll](https://jekyllrb.com)). Using Jekyll, I've got a structure in place that I can use to easily publish tutorials in a structured format. There is one tutorial series that I'm currently writing and that is [Forge Mods in 1.9](/tutorials/forge-modding-19/), and hopefully more series will follow.
|
|
@ -0,0 +1,44 @@
|
||||||
|
```
|
||||||
|
metadata.title = "1.9.4 Porting Spree"
|
||||||
|
metadata.category = "minecraft"
|
||||||
|
metadata.date = "2016-05-21 17:47:18 -0400"
|
||||||
|
metadata.oldPermalink = "/mods/2016/05/21/194-porting-spree/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that Forge for 1.9.4 is [out](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.9.4.html), I've begun the log and arduous process of porting my mods to 1.9.4 (if by long and arduous, you mean short and trivial).
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
<div class="mod">
|
||||||
|
<h3 class="mod-name"><a href="http://minecraft.curseforge.com/projects/shadowmc">ShadowMC</a></h3>
|
||||||
|
<span class="mod-version"><a href="http://minecraft.curseforge.com/projects/shadowmc/files/2301829">3.3.0</a></span>
|
||||||
|
<p class="mod-desc">
|
||||||
|
The library mod required by all of my other mods.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mod">
|
||||||
|
<h3 class="mod-name"><a href="http://minecraft.curseforge.com/projects/sleeping-bag">Sleeping Bag</a></h3>
|
||||||
|
<span class="mod-version"><a href="http://minecraft.curseforge.com/projects/sleeping-bag/files/2301830">1.2.0</a></span>
|
||||||
|
<p class="mod-desc">
|
||||||
|
Adds a simple sleeping bag item that is usable anywhere and doens't set your spawn which makes it quite handy for bringing on adventures.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mod">
|
||||||
|
<h3 class="mod-name"><a href="http://minecraft.curseforge.com/projects/ye-olde-tanks">Ye Olde Tanks</a></h3>
|
||||||
|
<span class="mod-version"><a href="http://minecraft.curseforge.com/projects/ye-olde-tanks/files/2301852">1.7.0</a></span>
|
||||||
|
<p class="mod-desc">
|
||||||
|
Fluid stuff: Fluid barrels, creative fluid barrels, fluid barrel minecarts, infinite water buckets.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mod">
|
||||||
|
<h3 class="mod-name"><a href="http://minecraft.curseforge.com/projects/discordchat">DiscordChat</a></h3>
|
||||||
|
<span class="mod-version"><a href="http://minecraft.curseforge.com/projects/discordchat/files/2301839">1.2.0</a></span>
|
||||||
|
<p class="mod-desc">
|
||||||
|
Merges a Discord channel with Minecraft chat, primarily intended for servers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mod">
|
||||||
|
<h3 class="mod-name"><a href="http://minecraft.curseforge.com/projects/shadowtweaks">ShadowTweaks</a></h3>
|
||||||
|
<span class="mod-version"><a href="http://minecraft.curseforge.com/projects/shadowtweaks/files/2302146">1.9.0</a></span>
|
||||||
|
<p class="mod-desc">A little tweaks mod with a variety of client/server tweaks.</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Introducing RTFM"
|
||||||
|
metadata.category = "minecraft"
|
||||||
|
metadata.date = "2016-06-29 12:00:00 -0400"
|
||||||
|
metadata.oldPermalink = "/meta/2016/06/29/introducing-rtfm/"
|
||||||
|
```
|
||||||
|
|
||||||
|
[RTFM](https://rtfm.shadowfacts.net/) is the brand new website that will contain the documentation for all of my projects, currently it only contains documentation for MC mods. Like this website, it is [hosted on GitHub](https://github.com/shadowfacts/RTFM) using GitHub pages.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
![XKCD #293 RTFM](https://imgs.xkcd.com/comics/rtfm.png)
|
|
@ -0,0 +1,8 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Forge Modding Tutorials for 1.10.2"
|
||||||
|
metadata.category = "minecraft"
|
||||||
|
metadata.date = "2016-06-30 10:35:00 -0400"
|
||||||
|
metadata.oldPermalink = "/meta/2016/06/30/forge-1102-tutorials/"
|
||||||
|
```
|
||||||
|
|
||||||
|
The Forge modding tutorials have all the been [updated to MC 1.10.2](/tutorials/forge-modding-1102/) as has the [GitHub repo](https://github.com/shadowfacts/TutorialMod/).
|
|
@ -0,0 +1,149 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Introducing Mirror"
|
||||||
|
metadata.category = "java"
|
||||||
|
metadata.date = "2016-07-28 16:45:00 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
Allow me to introduce my latest project, Mirror. Mirror is a [reflection][] library for Java designed to take advantage of the streams, lambdas, and optionals introduced in Java 8.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
The source code is publicly available on [GitHub][source] under the MIT license and the JavaDocs are viewable [here][docs].
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
All version of Mirror are [available on my Maven][maven].
|
||||||
|
|
||||||
|
### Maven
|
||||||
|
```xml
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>shadowfacts</id>
|
||||||
|
<url>http://mvn.rx14.co.uk/shadowfacts/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.shadowfacts</groupId>
|
||||||
|
<artifactId>Mirror</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gradle
|
||||||
|
```groovy
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name "shadowfacts"
|
||||||
|
url "http://mvn.rx14.co.uk/shadowfacts/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile group: "net.shadowfacts", name: "Mirror", version: "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
A couple of simple examples for getting started with Mirror.
|
||||||
|
|
||||||
|
For more complex examples of everything possible with Mirror, you can look at the [unit tests][tests].
|
||||||
|
|
||||||
|
### General Overview
|
||||||
|
The `Mirror.of` methods are used to retrieve mirrors on which operations can be performed. The types of mirrors are:
|
||||||
|
|
||||||
|
- [`MirrorClass`][class]
|
||||||
|
- [`MirrorEnum`][enum]
|
||||||
|
- [`MirrorConstructor`][constructor]
|
||||||
|
- [`MirrorMethod`][method]
|
||||||
|
- [`MirrorField`][field]
|
||||||
|
|
||||||
|
The `Mirror.ofAll` methods are used to create mirror stream wrappers for a given stream/collection/array of reflection objects or mirrors.
|
||||||
|
|
||||||
|
These examples will use the following classes:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Test {
|
||||||
|
public static String name = "Mirror";
|
||||||
|
public static String author;
|
||||||
|
|
||||||
|
public static String reverse(String str) {
|
||||||
|
return new StringBuilder(str).reverse().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Test2 {
|
||||||
|
public static String name = "Test 2";
|
||||||
|
|
||||||
|
public static void doSomething() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Fields
|
||||||
|
```java
|
||||||
|
// get the field
|
||||||
|
Optional<MirrorField> optional = Mirror.of(Test.class).field("name");
|
||||||
|
// unwrap the optional
|
||||||
|
MirrorField field = optional.get();
|
||||||
|
// get the value of the field
|
||||||
|
// we pass null as the instance because the field is static
|
||||||
|
field.get(null); // "Mirror"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Fields
|
||||||
|
```java
|
||||||
|
// get the field
|
||||||
|
Optional<MirrorField> optional = Mirror.of(Test.class).field("author");
|
||||||
|
// unwrap the optional
|
||||||
|
MirrorField field = optional.get();
|
||||||
|
// set the value of the field
|
||||||
|
// we once again pass null as the instance because the field is static
|
||||||
|
field.set(null, "Shadowfacts");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invoking Methods
|
||||||
|
```java
|
||||||
|
// get the method using the name and the types of the arguments it accepts
|
||||||
|
Optional<MirrorMethod> optional = Mirror.of(Test.class).method("reverse", String.class);
|
||||||
|
// unwrap the optional
|
||||||
|
MirrorMethod method = optional.get();
|
||||||
|
// invoke the method
|
||||||
|
method.invoke(null, "Mirror"); // "rorriM";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Class Streams
|
||||||
|
```java
|
||||||
|
Mirror.ofAllUnwrapped(Test.class, Test2.class) // create the stream of classes
|
||||||
|
.unwrap() // map the MirrorClasses to their Java versions
|
||||||
|
.toArray(); // [Test.class, Test2.class]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Streams
|
||||||
|
```java
|
||||||
|
Mirror.ofAllUnwrapped(Test.class, Test2.class) // create the stream of classes
|
||||||
|
.flatMapToFields() // flat map the classes to their fields
|
||||||
|
.get(null) // get the value of the fields on null
|
||||||
|
.toArray(); // ["Mirror", "Shadowfacts", "Tesst 2"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method Streams
|
||||||
|
```java
|
||||||
|
Mirror.ofAllUnwrapped(Test.class, Test2.class) // create the stream of classes
|
||||||
|
.flatMapToMethods() // flat map the classes to their methods
|
||||||
|
.filter(m -> Arrays.equals(m.parameterTypes(), new MirrorClass<?>[]{Mirror.of(String.class)})) // filter the methods by which accept only a String
|
||||||
|
.invoke(null, "Shadowfacts") // invoke them all on nothing, passing in "Shadowfacts"
|
||||||
|
.toArray(); // ["stcafwodahS"]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[reflection]: https://en.wikipedia.org/wiki/Reflection_(computer_programming)
|
||||||
|
[source]: https://github.com/shadowfacts/Mirror/
|
||||||
|
[docs]: https://shadowfacts.net/Mirror/
|
||||||
|
[maven]: http://mvn.rx14.co.uk/shadowfacts/net/shadowfacts/Mirror
|
||||||
|
[tests]: https://github.com/shadowfacts/Mirror/tree/master/src/test/java/net/shadowfacts/mirror
|
||||||
|
[class]: https://shadowfacts.net/Mirror/net/shadowfacts/mirror/MirrorClass.html
|
||||||
|
[enum]: https://shadowfacts.net/Mirror/net/shadowfacts/mirror/MirrorEnum.html
|
||||||
|
[constructor]: https://shadowfacts.net/Mirror/net/shadowfacts/mirror/MirrorConstructor.html
|
||||||
|
[method]: https://shadowfacts.net/Mirror/net/shadowfacts/mirror/MirrorMethod.html
|
||||||
|
[field]: https://shadowfacts.net/Mirror/net/shadowfacts/mirror/MirrorField.html
|
|
@ -0,0 +1,38 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Kotlin and Minecraft Forge"
|
||||||
|
metadata.category = "minecraft"
|
||||||
|
metadata.date = "2016-08-06 16:45:30 -0400"
|
||||||
|
metadata.oldPermalink = "/forge/2016/08/06/kotlin-and-forge/"
|
||||||
|
```
|
||||||
|
|
||||||
|
So, you wanna use [Kotlin][] in your Forge mod? Well there's good news, I've just released [Forgelin][], a fork of [Emberwalker's Forgelin][EWForgelin], a library that provides utilities for using Kotlin with Minecraft/Forge.
|
||||||
|
|
||||||
|
Forgelin provides a Kotlin langauge adapter that allows your main-mod class to be a [`object`][KotlinObject]. In order to use the language adapter, you must specify the `modLanguageAdapter` property in your `@Mod` annotation to be `net.shadowfacts.forgelin.KotlinAdapter`.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
Additionally, Forgelin repackages the Kotlin standard library, reflect library, and runtime so that you don't have to, and so that end users don't have to download the 3 megabytes of Kotlin libraries multiple times.
|
||||||
|
|
||||||
|
~~Additionally, Forgelin provides a number of [extensions][KotlinExtensions] (which are viewable [here][ExtensionsList]) for working with Minecraft/Forge.~~
|
||||||
|
|
||||||
|
~~While you can shade Forgelin, it is not recommended to do so. It will increase your jar size by approximately 3 megabytes (as Forgelin itself includes the entire Kotlin, standard lib, reflect lib, and runtime) and may cause issues with other mods that shade Kotlin or Forgelin. It is recommended that you have your users download Forgelin from [CurseForge][].~~
|
||||||
|
|
||||||
|
**Update Feb 17, 2017:**
|
||||||
|
|
||||||
|
1. As of Forgelin 1.1.0, the extensions have been moved from Forgelin to [ShadowMC][].
|
||||||
|
2. As of Forgelin 1.3.0, Forgelin includes an `@Mod` annotated object. This means:
|
||||||
|
1. **Forgelin can no longer be shaded.**
|
||||||
|
2. `required-after:forgelin;` can now be used in the `dependencies` field of your `@Mod` annotation for a nicer error message when Forgelin isn't installed.
|
||||||
|
|
||||||
|
|
||||||
|
A bare-bones example mod using Forgelin is available [here][example].
|
||||||
|
|
||||||
|
[Kotlin]: https://kotlinlang.org/
|
||||||
|
[Forgelin]: https://github.com/shadowfacts/Forgelin
|
||||||
|
[EWForgelin]: https://github.com/Emberwalker/Forgelin
|
||||||
|
[KotlinObject]: https://kotlinlang.org/docs/reference/object-declarations.html
|
||||||
|
[KotlinExtensions]: https://kotlinlang.org/docs/reference/extensions.html
|
||||||
|
[ExtensionsList]: https://github.com/shadowfacts/Forgelin/tree/master/src/main/kotlin/net/shadowfacts/forgelin/extensions
|
||||||
|
[ShadowMC]: https://github.com/shadowfacts/ShadowMC/tree/1.11.2/src/main/kotlin/net/shadowfacts/forgelin/extensions
|
||||||
|
[CurseForge]: https://minecraft.curseforge.com/projects/shadowfacts-forgelin
|
||||||
|
[example]: https://github.com/shadowfacts/ForgelinExample
|
|
@ -0,0 +1,29 @@
|
||||||
|
```
|
||||||
|
metadata.title = "The Great Redesign"
|
||||||
|
metadata.category = "meta"
|
||||||
|
metadata.date = "2016-08-07 15:39:48 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
Welcome to the fourth iteration of my website. I'm still using Jekyll, however I've rewritten most of the styles from scratch. This theme is based on the [Hacker theme][HackerHexo] for [Hexo][] which is turn based on the [Hacker WordPress theme][HackerWP] but it has some notable differences.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
### 1\. It's built for Jekyll.
|
||||||
|
|
||||||
|
Because Jekyll (and more specifically, GitHub Pages) uses Sass instead of [Styl][] like Hacker, all of the styles had to be rewritten from scratch in SCSS. Most of the original [Minima][] styles were scrapped, except for a couple of code styling details and the footer design.
|
||||||
|
|
||||||
|
### 2\. It has a dark theme
|
||||||
|
|
||||||
|
This is accomplished storing the current them (`dark` or `light`) in a cookie, reading it in the head, and writing a `<link>` element based on the the value of the theme. All the styles are stored in `_sass/theme.scss` and the `css/light.scss` and `css/dark.scss` files store the variable definitions for all the colors used in the theme. Jekyll then compiles the two main SCSS files into two CSS files that each contain [Normalize.css][Normalize], the theme (compiled from the variable definitions), and the [Darcula][RougeDarcula] syntax highlighting theme.
|
||||||
|
|
||||||
|
While this does increase the load time and isn't best practice, I think providing the option of a dark theme (especially when the deafult theme is incredibly light (the majority of the page is pure white (ooh, tripple nested parentheses))) outweights the cost. Besides, when testing locally the entire script loading and executiononly cost 5 miliseconds, completely unnoticable.
|
||||||
|
|
||||||
|
The selector in the third column of the footer simply updates the cookie value based on the checkbox status and reloads the page via `window.location.reload()` triggering the changed theme CSS to be loaded.
|
||||||
|
|
||||||
|
[HackerHexo]: https://github.com/CodeDaraW/Hacker
|
||||||
|
[Hexo]: https://hexo.io/
|
||||||
|
[HackerWP]: https://wordpress.org/themes/hacker/
|
||||||
|
[Styl]: https://github.com/tj/styl
|
||||||
|
[Minima]: https://github.com/jekyll/minima
|
||||||
|
[Normalize]: https://necolas.github.io/normalize.css/
|
||||||
|
[RougeDarcula]: https://github.com/shadowfacts/RougeDarcula
|
|
@ -0,0 +1,112 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Type: A FOSS clone of typing.io"
|
||||||
|
metadata.category = "misc"
|
||||||
|
metadata.date = "2016-10-08 17:29:42 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
**TL;DR**: I made an awesome FOSS clone of [typing.io](https://typing.io) that you can check out at [type.shadowfacts.net](https://type.shadowfacts.net) and the source of which you can see [here](https://github.com/shadowfacts/type).
|
||||||
|
|
||||||
|
I've used [typing.io](https://typing.io) on and off for almost a year now, usually when I'm bored and have nothing else to do. Unfortunately, I recently completed the Java file, the C++ file, and the JavaScript file (that last one took too much time, jQuery has weird coding formatting standards, IMO) meaning I've completed pretty much everything that interests me. Now if you want to upload your own code to type, you have to pay $9.99 _a month_, which, frankly, is ridiculous. $10 a month to be able to upload code to a website only to have more than the 17 default files (one for each langauge) when I could build my own clone.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
This is my fourth attempt at building a clone of typing.io, and the first one that's actually been successful. (The first, second, and third all failed because I was trying to make a syntax highlighting engine work with too much custom code.)
|
||||||
|
|
||||||
|
Type uses [CodeMirror](https://codemirror.net/), a fantastic (and very well documented) code editor which handles [syntax highlighting](#syntax-highlighting), [themes](#themes), [cursor handling](#cursor-handling), and [input](#input).
|
||||||
|
|
||||||
|
## Input
|
||||||
|
Input was one of the first things I worked on. (I wanted to get the very basics working before I got cought up in minor details.) CodeMirorr's normal input method doesn't work for me, because in Type, all the text is in the editor beforehand and the user doesn't actually type it out. The CodeMirror instance is set to `readOnly` mode, making entering or removing text impossible. This is all well and good, but how can you practice typing if you can't type? Well, you don't actually type. The DOM `keypress` and `keydown` events are used to handle character input and handle special key input (return, backspace, tab, and escape) respectively.
|
||||||
|
|
||||||
|
The `keypress` event handler simply moves the cursor one character and marks the typed character as completed. If the character the user typed isn't the character that's in the document they are typing, a CodeMirror [TextMarker](http://codemirror.net/doc/manual.html#markText) with the `invalid` class will be used to display a red error-highlight to the user. These marks are then stored in a 2-dimensional array which is used to check if the user has actully completed the file.
|
||||||
|
|
||||||
|
The `keydown` event is used for handling special key pressed namely, return, backspace, delete, and escape.
|
||||||
|
|
||||||
|
When handling a return press, the code first checks if the user has completed the current line (This is a little bit more complicated than checking if the cursor position is at the end of the line, because Type allows you to skip typing whitespace at the beggining and end of lines because every IDE/editor under the sun handles that for you). Then, the editor moves the cursor to the beggining of the next line (see the previous parenthetical).
|
||||||
|
|
||||||
|
Backspace handling works much the same way, checking if the user is at the begging of the line, and if so, moving to the end of the previous line, or otherwise moving back 1 character. Delete also has a bit of extra functionality specific to Type. Whenever you press delete and the previous character was marked as invalid, the invalid marks needs to A) be cleared from the CodeMirror document and B) removed from the 2D array of invalid marks that's used for completion checking.
|
||||||
|
|
||||||
|
The tab key requires special handling because it's not entered as a normal character and therefore special checking has to be done to see if the next character is a tab character. Type doesn't handling using the tab key with space-indentation like most editors/IDEs because most places where you'd encounter significant amounts of whitespace in the middle of a line, it's a tab character used to line up text across multiple lines.
|
||||||
|
|
||||||
|
Escape is handled fairly simply. When escape is pressed and the editor is focused, a global `focus` variable is toggled, causing all other input-handling to be disabled, a **`Paused`** label is added/removed in the top right of the top bar, and lastly the `paused` class is toggled on the page, which, when active, gives the editor 50% opacity, giving it a nice effect that clearly indicates the paused state.
|
||||||
|
|
||||||
|
## Cursor Handling
|
||||||
|
Preventing the cursor movement (something you obviously don't want for a typing practice thing) that's still possible, even in CodeMirror's read only mode, is accomplished simply by adding an event listener on the CodeMirror `mousedown` event and calling `preventDefault` on the event to prevent the editor's default behavior from taking place and calls the `focus` method on the editor instance, focusing it and un-pauses it if it was previously paused.
|
||||||
|
|
||||||
|
## Syntax Highlighting
|
||||||
|
Syntax highlighting is handled completely using CodeMirror's [modes](http://codemirror.net/mode/index.html), so Type supports* everything that CodeMirror does. By default, Type will try to automatically detect a language mode to use based on the file's extension, falling back to plain-text if a mode can't be found. This is accomplished by searching the (ridiculously large and manually written) [langauges map](https://github.com/shadowfacts/type/blob/master/js/languages.js) that stores A) the JS CodeMirror mode file to load, B) the MIME type to pass to CodeMirror, and C) the extensions for that file-type (based on GitHub [linguis](https://github.com/github/linguist) data). Yes, I spent far too long manually writing that file when I probably could have [automated](https://xkcd.com/1319/) it. The script for the mode is then loaded using `jQuery.getScript`and the code, along with the MIME type, and a couple other things, are passed into `CodeMirror.fromTextArea`.
|
||||||
|
|
||||||
|
\* Technically it does, however only a subset of those languages can actually be used because they seem common enough** to warrant being manually added to the languages map.
|
||||||
|
|
||||||
|
** I say "common" but [Brainfuck](https://github.com/shadowfacts/type/blob/master/js/languages.js#L2) and [FORTRAN](https://github.com/shadowfacts/type/blob/master/js/languages.js#L142) aren't really common, I just added them for shits and giggles.
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
Themes are handled fairly similarly to syntax highlighting. There's a massive `<select>` dropdown which contains all the options for the [CodeMirror themes](https://github.com/codemirror/CodeMirror/tree/master/theme). When the dropdown is changed, the stylesheet for the selected theme is loaded and the `setTheme` function is called on the editor.
|
||||||
|
|
||||||
|
## Chunks
|
||||||
|
Chunks were the ultimate solution to a problem I ran into fairly early on when I was testing Type. Due to the way Type handles showing which parts of the file haven't been completed (having a single `TextMarker` going from the cursor to the end of the file and updating it when the cursor moves), performance suffers a lot for large files because of the massive amount of DOM updates and re-renders when typing quickly. The solution I came up with was splitting each file up into more managable chunks (50 lines, at most) which can more quickly be re-rendered by the browser. Alas, this isn't a perfect solution because CodeMirror's lexer can sometimes break with chunks (see [Fixing Syntax Highlighting](#fixing-syntax-highlighting)) , but it's the best solution I've come up with so far.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
One of the restrictions I imposed on myself for this project (mostly because I didn't want to pay for a server) was that Type's functionality had to be 100% client-side only. There are two primary things that result from this 1) Type is account-less and 2) therefore everything (progress, current files, theme, etc.) have to be stored client-side.
|
||||||
|
|
||||||
|
I decided to use Mozilla's [localForage](https://github.com/localForage/localForage) simply because I remembered it when I had to implement storage stuff. (If you don't know, localForage is a JS wrapper around IndexedDB and/or WebSQL with a fallback to localStorage which makes client-side persistence much nicer.)
|
||||||
|
|
||||||
|
Basic overview of what Type stores:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
root
|
||||||
|
|
|
||||||
|
+-->theme
|
||||||
|
|
|
||||||
|
+-->owner/repo/branch
|
||||||
|
|
|
||||||
|
+-->path/to/file
|
||||||
|
|
|
||||||
|
+-->chunk
|
||||||
|
|
|
||||||
|
+-->chunks array
|
||||||
|
|
|
||||||
|
+-->0
|
||||||
|
|
|
||||||
|
+-->cursor
|
||||||
|
| |
|
||||||
|
| +-->line
|
||||||
|
| |
|
||||||
|
| +-->ch
|
||||||
|
|
|
||||||
|
+-->elapsedTime
|
||||||
|
|
|
||||||
|
+-->invalids array
|
||||||
|
|
|
||||||
|
+-->invalids array
|
||||||
|
|
|
||||||
|
+-->0
|
||||||
|
|
|
||||||
|
+-->line
|
||||||
|
|
|
||||||
|
+-->ch
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
If you want to see actually what Type stores, feel free to take a look in the Indexed DB section of the Application tab of the Chrome web inspector (or the appropriate section of your favorite browser).
|
||||||
|
|
||||||
|
## WPM Tracking
|
||||||
|
WPM tracking takes place primarily in the [`updateWPM`](https://github.com/shadowfacts/type/blob/master/js/type.js#L561) function which is called every time the user presses return to move to the next line. `updateWPM` does a number of things.
|
||||||
|
|
||||||
|
1. If the editor is focused, it updates the elapsed time.
|
||||||
|
2. It gets the total number of words in the chunk. This is done by splitting the document text with a regex that matches A) any whitespace character B) a comma C) a period and getting the length of the resulting array.
|
||||||
|
3. Getting the total number of minutes from the elapsed time (which is stored in miliseconds).
|
||||||
|
4. The WPM indicator is updated (# of words / # of minutes).
|
||||||
|
|
||||||
|
# What's Next
|
||||||
|
Right now, Type is reasonably complete. It's in a perfectly useable state, but there are still more things I want to do.
|
||||||
|
|
||||||
|
## Fixing Syntax Highlighting
|
||||||
|
Because of the chunking system, in some cases syntax highlighting is utterly broken because a key thing that the lexer needs to understand what the code is isn't present because it's in the previous chunk. One relatively common example of this is block-comments. If a block-comment begins in one chunk but terminates in a later chunk, the text that's inside the comment but in a different chunk than the starting mark has completely invalid highlighting because the lexer has no idea it's a comment.
|
||||||
|
|
||||||
|
## Skipping Comments
|
||||||
|
This is a really, really nice feature that typing.io has which is that as you're typing, the cursor will completely skip over comments, both on their own lines and on the ends of other lines. This should be possible, I just need to hook into CodeMirror's syntax highlighting code and find out if the next thing that should be typed is marked as a comment and if so, skip it.
|
||||||
|
|
||||||
|
## Polishing
|
||||||
|
If you've looked at the site, you can tell it's fairly unpolished. It's got almost no styling and is fairly unintuitive. It'll probably remain minimalistic, but it'd be nice to have unified design/theme across the entire site.
|
||||||
|
|
||||||
|
## Typo Heatmap
|
||||||
|
This is a feature in the premium version of typing.io that I'd like to add to Type. It shows a heat map of all the keys you make errors on. The only thing that's preventing me from working on this currently is it would require manually writing a massive data file containing all the locations of all the keys and which characters correspond to which keys, something I don't want to do after spending too much time manually writing the [language map](https://github.com/shadowfacts/type/blob/master/js/languages.js).
|
|
@ -0,0 +1,19 @@
|
||||||
|
```
|
||||||
|
metadata.title = "The Pretty Good Minor Update"
|
||||||
|
metadata.category = "meta"
|
||||||
|
metadata.date = "2017-02-17 14:30:42 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
It's been about six months since the last time I redesigned the site, and while I didn't want to redesign it yet again, I felt it could use a little update to make sure everything's still good.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
After reading this [blog post](http://jacquesmattheij.com/the-fastest-blog-in-the-world) about optimizing sites (specifically using a static site generator, like this one does) that was posted on HN, I got to thinking about optimizing my site. I tested my site on Google's [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights) and got a mediocre score (I don't recall the exact number, but it was in the 70s or 80s). I haven't gone anywhere near as all-out with the optimization as the blog post described, but I'll still go over the couple things I did do:
|
||||||
|
|
||||||
|
- Removing custom fonts. The only custom font I previously used was [Hack](https://github.com/chrissimpkins/Hack) for code blocks, so removing that shaved off several extra requests and quite a bit of time without changing much.
|
||||||
|
- Replace [js-cookie](https://github.com/js-cookie/js-cookie) with a [couple functions](https://github.com/shadowfacts/shadowfacts.github.io/blob/master/_includes/head.html#L17-L34) included in the inline script, saving ~2 kilobytes and an additional HTTP request.
|
||||||
|
- [CloudFlare](https://www.cloudflare.com) provides a number of optimizations (caching and HTML/CSS minification).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Additionally, there's now a fallback `<noscript>` tag so that if the viewer has JavaScript disabled, the site will still look normal (JavaScript being disabled does mean the theme can't be changed, so the user will always see the light theme). And lastly, there's now a custom 404 page so if you end up at the wrong URL, you'll see something nicer than the default 404 page for GitHub Pages.
|
|
@ -0,0 +1,37 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Comments Powered by GitHub"
|
||||||
|
metadata.category = "meta"
|
||||||
|
metadata.date = "2017-04-23 09:05:42 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
After seeing [this article][orig] the other morning about replacing the Disqus comments on a blog powered by a static site generator (like this one) with comments backed by a GitHub issue and some front-end JavaScript to load and display them, I thought it would be fun to implement something similar. First I only built the code for displaying comments, similar to the aforementioned article, but I decided to take it one step further by allowing users to submit comments directly from my site.
|
||||||
|
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
|
||||||
|
You might be wondering, *Why even use weird, front-end comments like this when you could just use a database on the backend and generate the page dynamically?* Well, there are a couple reasons:
|
||||||
|
|
||||||
|
Firstly, it's a lot simpler to code (I don't have to handle any of the backend stuff like storage/moderation tools/etc.)
|
||||||
|
|
||||||
|
Secondly, and more important, my site can remain entirely static. This second reason was the key factor for me. Being static allows my site to be hosted for free on [GitHub Pages](https://pages.github.com/) so I don't have to handle any of the server-side stuff myself. It also makes the site ridiculously fast. A draft of this post has all the content and styles loaded within ~150ms and has the comments loaded after ~300ms.
|
||||||
|
|
||||||
|
So, how did I implement it? Well the first part is fairly simple and based on the [original article][orig]. It simply sends a request to the GitHub API [endpoint](https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue), parses the resulting JSON, generates some HTML, and injects it back into the page.
|
||||||
|
|
||||||
|
The second part is a bit more complicated, as it handles authentication with the GitHub API and posting comments directly from my site. Since this is a fair bit more complicated with several possible paths to the desired behavior, I'll go through this (twice, the reasoning for which will become clear soon) as would actually happen:
|
||||||
|
|
||||||
|
1. The user enters some comment into the textarea and clicks the submit button. At this point, since the user's never submitted a comment via the website before, we need to authorize with GitHub before we can submit.
|
||||||
|
2. When the user clicks the submit button, the forms submits to a separate backend helper application that handles the OAuth authorization flow.
|
||||||
|
3. The server app then temporarily stores the submitted comment with a random ID and redirects the user to the GitHub authorization page where the user grants access to their account.
|
||||||
|
4. From there, GitHub redirects back to the helper app with the same random ID and an OAuth code.
|
||||||
|
5. The helper app then sends a request to GitHub with the OAuth code, client ID, and client secret (this is why the helper is necessary, to keep the secret secret) getting an authorization token in response.
|
||||||
|
6. The helper app uses the random ID to retrieve the comment being submitted and the URL being submitted from, redirecting the user back to the original URL with the comment and auth token in the URL hash.
|
||||||
|
7. The client loads the comment and auth token from the hash and the clears the hash.
|
||||||
|
8. The auth token is stored in a cookie for future use.
|
||||||
|
9. Finally, the client then sends a POST request to the GitHub API [endpoint](https://developer.github.com/v3/issues/comments/#create-a-comment) with the comment, issue ID, and the token to submit the comment.
|
||||||
|
|
||||||
|
This is the flow for when the client's never submitted a comment before, but as stated in step 8, the auth token is cached on the client, making things simpler the next time someone wants to submit a comment. When the comment submit button is pressed and there is an auth token cached, we simply cancel the form submission and send the POST request to GitHub, submitting the comment.
|
||||||
|
|
||||||
|
All of the code for this is open source. The front-end JS is available [here](https://github.com/shadowfacts/shadowfacts.github.io/blob/master/js/comments.js) and the backend GitHub API helper is [here](https://github.com/shadowfacts/gh-comment-poster).
|
||||||
|
|
||||||
|
And that's it! So, do you like this system? Hate it? Have suggestions for how it could be improved? Well now you can leave a comment.
|
||||||
|
|
||||||
|
[orig]: http://donw.io/post/github-comments/
|
|
@ -0,0 +1,44 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Reincarnation"
|
||||||
|
metadata.category = "meta"
|
||||||
|
metadata.date = "2019-01-02 23:02:42 -0400"
|
||||||
|
```
|
||||||
|
|
||||||
|
Welcome, to ***Reincarnation***.
|
||||||
|
<!-- excerpt-end -->
|
||||||
|
`// TODO: write the actual blog post`
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct CharacterCounter {
|
||||||
|
|
||||||
|
static let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
|
static let mention = try! NSRegularExpression(pattern: "(@[a-z0-9_]+)(?:@[a-z0-9\\-\\.]+[a-z0-9]+)?", options: .caseInsensitive)
|
||||||
|
|
||||||
|
public static func count(text: String) -> Int {
|
||||||
|
let mentionsRemoved = removeMentions(in: text)
|
||||||
|
var count = mentionsRemoved.count
|
||||||
|
for match in linkDetector.matches(in: mentionsRemoved, options: [], range: NSRange(location: 0, length: mentionsRemoved.utf16.count)) {
|
||||||
|
count -= match.range.length
|
||||||
|
count += 23 // Mastodon link length
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeMentions(in text: String) -> String {
|
||||||
|
var mut = text
|
||||||
|
for match in mention.matches(in: mut, options: [], range: NSRange(location: 0, length: mut.utf16.count)).reversed() {
|
||||||
|
let replacement = mut[Range(match.range(at: 1), in: mut)!]
|
||||||
|
mut.replaceSubrange(Range(match.range, in: mut)!, with: replacement)
|
||||||
|
}
|
||||||
|
return mut
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="https://github.com/shadowfacts.png" alt="Test" />
|
||||||
|
<figcaption>Example photo</figcaption>
|
||||||
|
</figure>
|
|
@ -0,0 +1,16 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Redirect"
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
```
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.location = "<%= newPermalink %>";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<div class="article-content">
|
||||||
|
<p>
|
||||||
|
This page has moved to a new URL. If you are not automatically redirected, click <a href="<%= newPermalink %>">here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
|
@ -0,0 +1,21 @@
|
||||||
|
```
|
||||||
|
metadata.layout = "default.html.ejs";
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<h1 class="page-heading"><%= metadata.title %></h1>
|
||||||
|
<p class="rss">
|
||||||
|
Subscribe to just <%= metadata.title %> tutorials via <a href="/tutorials/<%= metadata.group %>/feed.xml">RSS</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% for (const tutorial of tutorials) { %>
|
||||||
|
<article>
|
||||||
|
<h2 class="article-title">
|
||||||
|
<a href="<%= tutorial.metadata.permalink %>" class="fancy-link">
|
||||||
|
<%= tutorial.metadata.title %>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<%- include("includes/article-meta.html.ejs", { metadata: tutorial.metadata }) %>
|
||||||
|
</article>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
|
@ -0,0 +1,22 @@
|
||||||
|
```
|
||||||
|
metadata.layout = "default.html.ejs"
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<% for (const series of allSeries) { %>
|
||||||
|
<article>
|
||||||
|
<h2 class="article-title">
|
||||||
|
<a href="<%= series.index.metadata.permalink %>" class="fancy-link">
|
||||||
|
<%= series.index.metadata.title %>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<p class="article-meta">
|
||||||
|
last updated on
|
||||||
|
<span>
|
||||||
|
<% const formatted = formatDate(series.index.metadata.lastUpdated, "MMM Do, YYYY") %>
|
||||||
|
<time datetime="<%= series.index.metadata.lastUpdated.toISOString() %>"><%= formatted %></time>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
|
@ -0,0 +1,62 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Advanced Creative Tabs"
|
||||||
|
metadata.date = "2016-06-15 11:42:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Searchable Tab
|
||||||
|
Let's make our creative tab searchable, just like the Search Items tab.
|
||||||
|
|
||||||
|
There are two main parts to this:
|
||||||
|
1. Returning `true` from the `hasSearchBar` method of our creative tab class.
|
||||||
|
2. Setting the texture name for the background image of our creative tab, so the search bar appears.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.client;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class TutorialTab extends CreativeTabs {
|
||||||
|
|
||||||
|
public TutorialTab() {
|
||||||
|
super(TutorialMod.modId);
|
||||||
|
setBackgroundImageName("item_search.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Item getTabIconItem() {
|
||||||
|
return ModItems.ingotCopper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasSearchBar() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we are returning `true` from `hasSearchBar` so Minecraft will allow us to type in our tab and filter the visible items.
|
||||||
|
|
||||||
|
We're also calling `setBackgroundImageName` with `"item_search.png"`. Minecraft will use this string to find the texture to use for the background. It will look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_BACKGROUND_NAME` where `BACKGROUND_NAME` is what you passed into `setBackgroundImageName`. `tag_item_search.png` is provided by Minecraft, so we don't need to do anything else.
|
||||||
|
|
||||||
|
![Searchable Creative Tab](http://i.imgur.com/C34Nh4R.png)
|
||||||
|
|
||||||
|
## Custom Background
|
||||||
|
As explained above, we can use custom backgrounds for our creative tabs.
|
||||||
|
|
||||||
|
> Minecraft will use this string to find the texture to use for the background. It will look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_BACKGROUND_NAME` where `BACKGROUND_NAME` is what you passed into `setBackgroundImageName`.
|
||||||
|
|
||||||
|
By passing a different string into `setBackgroundImageName` and adding the texture into the correct folder of our `src/main/resources` folder, we can use a custom background.
|
||||||
|
|
||||||
|
In our constructor, let's call `setBackgroundImageName` with `"tutorialmod.png"`. This will tell Minecraft to look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png`
|
||||||
|
|
||||||
|
Download [this](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png) texture and save it to `src/main/resources/assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png` in your mod folder.
|
||||||
|
|
||||||
|
That's it! When you open up the creative tab, you should now see our nice custom texture!
|
||||||
|
|
||||||
|
![Creative Tab with Custom Background](http://i.imgur.com/pP2W6h0.png)
|
|
@ -0,0 +1,206 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Armor"
|
||||||
|
metadata.date = "2016-09-17 16:53:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we can create armor, we'll need to create an armor material to use for our copper armor.
|
||||||
|
|
||||||
|
We'll add a new field to our main mod class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static final ItemArmor.ArmorMaterial copperArmorMaterial = EnumHelper.addArmorMaterial("COPPER", modId + ":copper", 15, new int[]{2, 5, 6, 2}, 9, SoundEvents.ITEM_ARMOR_EQUIP_IRON, 0.0F);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnumHelper.addArmorMaterial` takes a number of parameters:
|
||||||
|
- `"COPPER"`: The name of the new enum value, this is completely capitalized, following the enum naming convention.
|
||||||
|
- `modId + ":copper"`: This is the texture that will be used for our armor. We prefix it with our mod ID to use our mod's domain instead of the default `minecraft` domain.
|
||||||
|
- `15`: The maximum damage factor.
|
||||||
|
- `new int[]{2, 5, 6, 2}`: The damage reduction factors for each armor piece.
|
||||||
|
- `9`: The enchantibility of the armor.
|
||||||
|
- `SoundEvents.ITEM_ARMOR_EQUIP_IRON`: The sound event that is played when the armor is equipped.
|
||||||
|
- `0.0F`: The toughness of the armor.
|
||||||
|
|
||||||
|
Next we'll need the textures for the armor material that are used to render the on-player overlay.
|
||||||
|
Download the layer 1 texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/models/armor/copper_layer_1.png) and save it to `src/main/resources/assets/tutorial/textures/model/armor/copper_layer_1.png`. Download the layer 2 texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/models/armor/copper_layer_2.png) and save it to `src/main/resources/assets/tutorial/textures/models/armor/copper_layer_2.png`.
|
||||||
|
|
||||||
|
## Armor Item Base Class
|
||||||
|
Before we can begin creating armor items, we'll need to create a base class that implements our `ItemModelProvider` interface so it can be used with our registration helper method.
|
||||||
|
|
||||||
|
We'll create a class called `ItemArmor` in our `item` package that extends the Vanilla `ItemArmor` class and implements `ItemModelProvider`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.inventory.EntityEquipmentSlot;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemArmor extends net.minecraft.item.ItemArmor implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemArmor(ArmorMaterial material, EntityEquipmentSlot slot, String name) {
|
||||||
|
super(material, 0, slot);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Helmet
|
||||||
|
Firstly, we'll create a field for our copper helmet item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperHelmet;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperHelmet = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.HEAD, "copperHelmet"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copperHelmet.json`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperHelmet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperHelmet.png) and save it to `src/main/resources/assets/tutorial/textures/items/copperHelmet.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copperHelmet.name=Copper Helmet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Chestplate
|
||||||
|
First, we'll create a field for our copper chestplate item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperChestplate;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperChestplate = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.CHEST, "copperChestplate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copperChestplate.json`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperChestplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperChestplate.png) and save it to `src/main/resources/assets/tutorial/textures/items/copperChestplate.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copperChestplate.name=Copper Chestplate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Leggings
|
||||||
|
First, we'll create a field for our copper leggings item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperLeggings;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperLeggings = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.LEGS, "copperLeggings"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copperLeggings.json`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperLeggings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperLeggings.png) and save it to `src/main/resources/assets/tutorial/textures/items/copperLeggings.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the leggings.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copperLeggings.name=Copper Leggings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Boots
|
||||||
|
First, we'll create a field for our copper boots item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperBoots;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperBoots = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.FEET, "copperBoots"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copperBoots.json`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperBoots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperBoots.png) and save it to `src/main/resources/assets/tutorial/textures/items/copperBoots.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copperBoots.name=Copper Boots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Done!
|
||||||
|
Now, when we run the game, we can obtain our copper armor from the Combat creative tab, and when we equip it, we can see the player overlay being rendered and the armor value being show on the HUD:
|
||||||
|
|
||||||
|
![copper armor screenshot](https://i.imgur.com/Vv8Qzne.png)
|
|
@ -0,0 +1,152 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Basic Blocks"
|
||||||
|
metadata.date = "2016-05-07 21:45:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
For our first block, we are going to make a Copper Ore to go along with our Copper Ingot.
|
||||||
|
|
||||||
|
### Base Block
|
||||||
|
We're going to do something similar to what we did for [Basic Items](/tutorials/forge-modding-1102/basic-items/), create a base class for all of our blocks to extend to make our life a bit easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemBlock;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class BlockBase extends Block {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public BlockBase(Material material, String name) {
|
||||||
|
super(material);
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
setRegistryName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel(ItemBlock itemBlock) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(itemBlock, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockBase setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is almost exactly the same as our `ItemBase` class except it extends `Block` instead of `Item`. It sets the unlocalized and registry names, has a method to register the item model, and has an overridden version of `Block#setCreativeTab` that returns a `BlockBase`.
|
||||||
|
|
||||||
|
We'll also create a `BlockOre` class which extends `BlockBase` to make adding ore's a little easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
|
||||||
|
public class BlockOre extends BlockBase {
|
||||||
|
|
||||||
|
public BlockOre(String name) {
|
||||||
|
super(Material.ROCK, name);
|
||||||
|
|
||||||
|
setHardness(3f);
|
||||||
|
setResistance(5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockOre setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ModBlocks`
|
||||||
|
|
||||||
|
Now let's create a `ModBlocks` class similar to `ModItems` to assist us when registering blocks.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemBlock;
|
||||||
|
import net.minecraftforge.fml.common.registry.GameRegistry;
|
||||||
|
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block, ItemBlock itemBlock) {
|
||||||
|
GameRegistry.register(block);
|
||||||
|
GameRegistry.register(itemBlock);
|
||||||
|
|
||||||
|
if (block instanceof BlockBase) {
|
||||||
|
((BlockBase)block).registerItemModel(itemBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block) {
|
||||||
|
ItemBlock itemBlock = new ItemBlock(block);
|
||||||
|
itemBlock.setRegistryName(block.getRegistryName());
|
||||||
|
return register(block, itemBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This class is slightly different than our `ModItems` class due to the way blocks work in 1.9. In 1.9, we register the block and the `ItemBlock` separately whereas previously Forge would register the default `ItemBlock` automatically.
|
||||||
|
|
||||||
|
**Brief aside about how `ItemBlock`s work:** The `ItemBlock` for a given block is what is used as the inventory form of a given block. In the game, when you have a piece of Cobblestone in your inventory, you don't actually have on of the Cobblestone blocks in your inventory, you have one of the Cobblestone _`ItemBlock`s_ in your inventory.
|
||||||
|
|
||||||
|
Once again, we'll need to update our `preInit` method to call the `init` method of our `ModBlocks` class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
ModItems.init();
|
||||||
|
ModBlocks.init();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Copper Ore
|
||||||
|
|
||||||
|
Now, because we have our `BlockBase` and `ModBlocks` classes in place, we can quickly add a new block:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static BlockOre oreCopper;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
oreCopper = register(new BlockOre("oreCopper").setCreativeTab(CreativeTabs.MATERIALS));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Create a new `BlockOre` with the name `oreCopper`.
|
||||||
|
2. Sets the creative tab of the block to the Materials tab.
|
||||||
|
3. Registers the block with the `GameRegistry`.
|
||||||
|
4. Registers the default `ItemBlock` with the `GameRegistry`.
|
||||||
|
|
||||||
|
|
||||||
|
Now, in the game, we can see our (untextured) copper ore block!
|
||||||
|
|
||||||
|
![Copper Ore Screenshot](http://i.imgur.com/uWdmyA5.png)
|
||||||
|
|
||||||
|
Next, we'll look at how to make a simple model for our copper ore block.
|
|
@ -0,0 +1,37 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Forge Blockstates"
|
||||||
|
metadata.date = "2016-05-07 21:45:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got our copper ore block, let's add a simple blockstate to give it a texture. This will go in a file at `src/main/resources/assets/tutorial/blockstates/oreCopper.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"defaults": {
|
||||||
|
"textures": {
|
||||||
|
"all": "tutorial:blocks/oreCopper"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"normal": {
|
||||||
|
"model": "cube_all"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"model": "cube_all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `forge_marker` (L2): This tells Forge to use its custom blockstate parser instead of Minecraft's which isn't as good. (See [here](https://mcforge.readthedocs.io/en/latest/blockstates/forgeBlockstates/) for more info about Forge's blockstate format)
|
||||||
|
- `defaults` (L3-L7): Defaults are things to apply for all variants, a feature added by Forge's blockstate format.
|
||||||
|
- `textures` (L4-L6): This specifies which textures to use for the `cube_all` model. This uses the same texture format as explained in the [JSON Item Models](https://shadowfacts.net/tutorials/forge-modding-1102/json-item-models/) tutorial.
|
||||||
|
- `variants` (L8-L15): Inside of this block are where all of our individual variants go. Because we don't have any custom block properties, we have the `normal` variant which is the normal, in-world variant. The `inventory` variant is used when rendering our item in inventory and in the player's hand.
|
||||||
|
- `"model": "cube_all"` (L10 & L13): This uses the `cube_all` model for both variants. This is a simple model included in Minecraft which uses the same `#all` texture for every side of the block. We can't include this in the `defaults` block because Forge expects there to be at least one thing in each variant block.
|
||||||
|
|
||||||
|
Now, we just need to download the [copper ore texture](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/blocks/oreCopper.png) to `src/main/resources/assets/tutorial/textures/blocks/oreCopper.png` and we're all set!
|
||||||
|
|
||||||
|
![Textured Copper Ore Screenshot](http://i.imgur.com/wJ1iJUg.png)
|
|
@ -0,0 +1,134 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Basic Items"
|
||||||
|
metadata.date = "2016-05-07 16:32:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got the basic structure of our mod set up, we can create our first item. This item will be fairly simple, just a copper ingot.
|
||||||
|
|
||||||
|
|
||||||
|
### Base Item
|
||||||
|
|
||||||
|
Before we actually begin creating items, we'll want to create a base class just to make things easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemBase extends Item {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public ItemBase(String name) {
|
||||||
|
this.name = name;
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
setRegistryName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel() {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemBase setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `ItemBase` class will make it simpler to add basic items quickly. `ItemBase` primarily has a convenience constructor that sets both the unlocalized and the registry names.
|
||||||
|
|
||||||
|
- The unlocalized name is used for translating the name of the item into the currently active language.
|
||||||
|
- The registry name is used when registering our item with Forge and should _never, ever change_.
|
||||||
|
|
||||||
|
The `setCreativeTab` method is an overridden version that returns `ItemBase` instead of `Item` so we can use it in our `register` method without casting, as you'll see later.
|
||||||
|
|
||||||
|
You will have an error because we haven't created the `registerItemRenderer` method yet, so let's do that now. In the `CommonProxy` class add a new method called `registerItemRenderer` that accepts an `Item`, an `int`, and a `String`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public void registerItemRenderer(Item item, int meta, String id) {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll leave this method empty, because it's in the common proxy so it can't access any client-only code, but it still needs to be here because `TutorialMod.proxy` is of type `CommonProxy` so any client-only methods still need to have an empty stub in the `CommonProxy`.
|
||||||
|
|
||||||
|
To our `ClientProxy` we'll add the actual implementation of `registerItemRenderer`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void registerItemRenderer(Item item, int meta, String id) {
|
||||||
|
ModelLoader.setCustomModelResourceLocation(item, meta, new ModelResourceLocation(TutorialMod.modId + ":" + id, "inventory"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This method calls `ModelLoader.setCustomModelResourceLocation` which will tell Minecraft which item model to use for our item.
|
||||||
|
|
||||||
|
Lastly, we'll need to update our `preInit` method to call `ModItems.init` to actually create and register our items.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
ModItems.init();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ModItems`
|
||||||
|
|
||||||
|
Create a class called `ModItems`. This class will contain the instances of all of our items. In Minecraft, items are singletons so we'll only ever have on instance, and a reference to this instance will be kept in our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraftforge.fml.common.registry.GameRegistry;
|
||||||
|
|
||||||
|
public class ModItems {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Item> T register(T item) {
|
||||||
|
GameRegistry.register(item);
|
||||||
|
|
||||||
|
if (item instanceof ItemBase) {
|
||||||
|
((ItemBase)item).registerItemModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Right now the `init` method is empty, but this is where we'll put the calls to `register` to register our items. The `register` method does a couple of things:
|
||||||
|
|
||||||
|
1. Registers our item with the `GameRegistry`.
|
||||||
|
2. Registers the item model if one is present.
|
||||||
|
|
||||||
|
### Copper Ingot
|
||||||
|
|
||||||
|
Now to create our actual item, the copper ingot. Because we've created the `ItemBase` helper class, we won't need to create any more classes. We'll simply add a field for our new item and create/register/set it in the `init` method of our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static ItemBase ingotCopper;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
ingotCopper = register(new ItemBase("ingotCopper").setCreativeTab(CreativeTabs.MATERIALS));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Create a new `ItemBase` with the name `ingotCopper`
|
||||||
|
2. Set the creative tab to the Materials tab.
|
||||||
|
3. Register our item with the `GameRegistry`.
|
||||||
|
|
||||||
|
Now, if you load up the game and go into the Materials creative tab, you should see our new copper ingot item (albeit without a model)! Next time we'll learn how to make basic JSON models and add a model to our copper ingot!
|
||||||
|
|
||||||
|
![Copper Ingot Item Screenshot](http://i.imgur.com/6uHudqH.png)
|
|
@ -0,0 +1,87 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Crafting/Smelting Recipes"
|
||||||
|
metadata.date = "2016-06-30 10:49:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Before we implement any recipes, we'll create a little class in which we'll register all of our recipes. We'll call this class `ModRecipes`, following the same convention as our `ModItems` and `ModBlocks` classes. We'll put this into the `recipe` package inside our main package so once when we implement more complex custom recipes, we'll have a place to group them together.
|
||||||
|
|
||||||
|
You'll want an empty `init` static method in `ModRecipes` which is where we'll register our recipes in just a moment.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.recipe;
|
||||||
|
|
||||||
|
public class ModRecipes {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ModRecipes.init` method should be called from `TutorialMod#init`. The init method is called during the initialization phase which occurs after the pre-initialization phase. We want to register our recipes here instead of in the pre-init or post-init phases because all mod items/blocks (including those from other mods) should be registered at this point.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void init(FMLInitializationEvent event) {
|
||||||
|
ModRecipes.init();
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Crafting Recipes
|
||||||
|
There are two kinds of crafting recipes: shaped recipes and shapeless recipes.
|
||||||
|
|
||||||
|
In a shapeless recipe, the ingredients can be placed in any arrangement on the crafting grid. An example of a shapeless recipe is the [Pumpkin Pie recipe](http://minecraft.gamepedia.com/Pumpkin_Pie#Crafting).
|
||||||
|
|
||||||
|
Shaped recipes require their ingredients to be placed in a specific arrangement. An example of a shaped recipe is the [Cake recipe](http://minecraft.gamepedia.com/Cake#Crafting).
|
||||||
|
|
||||||
|
## Shapeless Recipe
|
||||||
|
Our shapeless recipe is going to be a simple recipe that lets people craft 1 corn into 1 corn seed. All this requires is 1 line in `ModRecipes`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void init() {
|
||||||
|
GameRegistry.addShapelessRecipe(new ItemStack(ModItems.cornSeed), ModItems.corn);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GameRegistry.addShapelessRecipe` does exactly as the name says, it registers a shapeless recipe. The first argument is an `ItemStack` that is the output of the recipe, in this case a corn seed. The second argument is a varargs array of `Object`s that can be `Item`s, `Block`s, or `ItemStack`s.
|
||||||
|
|
||||||
|
![Shapeless Recipe](http://i.imgur.com/tFZdyK3.png)
|
||||||
|
|
||||||
|
## Shaped Recipe
|
||||||
|
Our shaped recipe is going to be an additional recipe for Rabbit Stew that accepts corn instead of carrots. This requires a call to `GameRegistry.addShapedRecipe` which, you guessed it, registers a shaped recipe.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
GameRegistry.addShapedRecipe(new ItemStack(Items.RABBIT_STEW), " R ", "CPM", " B ", 'R', Items.COOKED_RABBIT, 'C', ModItems.corn, 'P', Items.BAKED_POTATO, 'M', Blocks.BROWN_MUSHROOM, 'B', Items.BOWL);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument to `GameRegistry.addShapedRecipe` is an `ItemStack` that is the output of the recipe. The next arguments should be anywhere from 1 to 3 `String` arguments that determine the pattern of the recipe. A space in a pattern string represents an empty slot and each character represents an item/block/stack specified by the following arguments. The remaining arguments should be pairs of characters and `Item`/`Block`/`ItemStack`. The character should be the same (including case) as used in the pattern strings. The item/block/stack should be what is used for the instances of that character in the pattern.
|
||||||
|
|
||||||
|
Our finished recipe looks like this:
|
||||||
|
|
||||||
|
![Shaped Recipe](http://i.imgur.com/KaatGDN.png)
|
||||||
|
|
||||||
|
## Smelting Recipe
|
||||||
|
Our furnace recipe is going to be a simple 1 Copper Ore to 1 Copper Ingot recipe. All this requires is 1 call to `GameRegistry.addSmelting`
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
GameRegistry.addSmelting(ModBlocks.oreCopper, new ItemStack(ModItems.ingotCopper), 0.7f);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GameRegistry.addSmelting` takes 3 parameters, the item/block/stack input, the `ItemStack` output, and the amount of experience to be given to the player (per smelt).
|
||||||
|
|
||||||
|
![Smelting Recipe](http://i.imgur.com/aU1ZiqG.png)
|
|
@ -0,0 +1,94 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Creative Tabs"
|
||||||
|
metadata.date = "2016-06-14 16:26:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
In this tutorial, we are going to create a custom creative tab that players can use to access all of our items when in creative mode.
|
||||||
|
|
||||||
|
## Creative Tab
|
||||||
|
First off, let's create our creative tab class. Create a class called `TutorialTab` that extends `CreativeTabs`. It will need a couple things:
|
||||||
|
|
||||||
|
1. A no-args constructor that calls the super constructor with the correct label.
|
||||||
|
2. An overridden `getTabIconItem` which returns the item to render as the icon.
|
||||||
|
|
||||||
|
The `String` passed into the super constructor is the label. The label is used to determine the localization key for the tab. For the label, we are going to pass in `TutorialMod.modId` so Minecraft uses our mod's ID to determine the localization key.
|
||||||
|
|
||||||
|
The item we return from the `getTabIconItem` will be rendered on the tab in the creative inventory GUI. We'll use `ModItems.ingotCopper` as the icon item so our creative tab has a nice distinctive icon.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.client;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class TutorialTab extends CreativeTabs {
|
||||||
|
|
||||||
|
public TutorialTab() {
|
||||||
|
super(TutorialMod.modId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Item getTabIconItem() {
|
||||||
|
return ModItems.ingotCopper;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's add a field to our `TutorialMod` class that stores the instance of our creative tab.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static final TutorialTab creativeTab = new TutorialTab();
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating Everything Else
|
||||||
|
Now that we've got our creative tab all setup, let's change all of our items and blocks to use it.
|
||||||
|
|
||||||
|
Let's add a line to the end of our `BlockBase` and `ItemBase` constructors that calls `setCreativeTab` with our creative tab.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public BlockBase(Material material, String name) {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ItemBase(String name) {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also need to add this line to the `BlockCropCorn` and `ItemCornSeed` classes because they don't extend our base item/block classes.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public BlockCropCorn() {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ItemCornSeed() {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, we'll need to update our `ModBlocks` and `ModItems` classes so we're no longer setting the creative tabs to other tabs.
|
||||||
|
|
||||||
|
Remove the `setCreativeTab` call from the end of the BlockOre constructor on the line where we register/create the copper ore block.
|
||||||
|
|
||||||
|
Remove the `setCreativeTab` calls from the copper ingot and corn items in `ModItems`.
|
||||||
|
|
||||||
|
## All Done!
|
||||||
|
Now when we start the game and open the creative inventory, we should be able to see our creative tab on the second page.
|
||||||
|
|
||||||
|
![our creative tab in action](http://i.imgur.com/JfEhwvu.png)
|
|
@ -0,0 +1,259 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Crops"
|
||||||
|
metadata.date = "2016-05-29 10:29:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
Before we get started making our corn crop, we'll need to make a couple of changes to the base item/block infrastructure we've already created. Specifically what we need to change has to do with item models. Currently, `BlockBase` and `ItemBase` have their own `registerItemModel` methods. We're going to move this into the `ItemModelProvider` interface so that blocks and items we create that don't extend `BlockBase` or `ItemBase` can still use our system for registering item models.
|
||||||
|
|
||||||
|
Create a new `ItemModelProvider` interface and add one method to it:
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
|
||||||
|
public interface ItemModelProvider {
|
||||||
|
|
||||||
|
void registerItemModel(Item item);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This interface functions exactly the same as the `registerItemModel` methods in `BlockBase` and `ItemBase`.
|
||||||
|
|
||||||
|
Next, we'll change `BlockBase` to implement `ItemModelProvider`. Just add the `implements ItemModelProvider` after the class declaration, change the `reigsterItemModel` method to accept an `Item` instead of an `ItemBlock` and add `@Override` to the `registerItemModel` method.
|
||||||
|
|
||||||
|
We'll repeat a similar process for `ItemBase`. Add `implements ItemModelProvider`, change `registerItemModel` to accept an `Item`, and add `@Override` to it.
|
||||||
|
|
||||||
|
Now that we've changed our `BlockBase` and `ItemBase` classes, we'll need to make some changes to our `ModItems` and `ModBlocks` classes to ensure that `ItemModelProvider#registerItemModel` is called even if the block or item isn't a subclass of our block or item base classes.
|
||||||
|
|
||||||
|
In `ModBlocks`, simply change `block instanceof BlockBase` to `block instanceof ItemModelProvider` and change the cast from `BlockBase` to `ItemModelProvider`. Do the same for the `ModItems` class, replacing `ItemBase` with `ItemModelProvider` in the appropriate section of the code.
|
||||||
|
|
||||||
|
Due to the way we are going to implement our crop, we'll need to make another modification to our `ModBlocks` class. Before we make this modification, let me explain why it's necessary.
|
||||||
|
|
||||||
|
Our crop is going to have 3 important parts:
|
||||||
|
|
||||||
|
1. The crop block
|
||||||
|
2. The seed item
|
||||||
|
3. The food item
|
||||||
|
|
||||||
|
Because we have a separate seed item, the crop block won't have an `ItemBlock` to go along with it.
|
||||||
|
|
||||||
|
In our `register(T block, ItemBlock itemBlock)` method, we'll need to add a null check to `itemBlock` so we don't attempt to register `itemBlock` if it's null.
|
||||||
|
|
||||||
|
Lastly, we'll need to make one change in our main `TutorialMod` class. Due to the way Minecraft's `ItemSeed` works, our blocks need to be initialized before we can call the constructor of our seed. Simply move the `ModItems.init` call in `TutorialMod.preInit` to after the `ModBlocks.init` call.
|
||||||
|
|
||||||
|
## Crop
|
||||||
|
The crop we are going to create will be corn.
|
||||||
|
|
||||||
|
As I mentioned before, our crop will be divided into 3 main parts:
|
||||||
|
|
||||||
|
1. The crop block (corn crop)
|
||||||
|
2. The seed item (corn seed)
|
||||||
|
3. The food item (corn)
|
||||||
|
|
||||||
|
We're going to create these one at a time. At the intermediate stages, our code will contain errors because we are referencing things we haven't created yet, but everything should be fine at the end.
|
||||||
|
|
||||||
|
### Corn Crop
|
||||||
|
Let's create a class called `BlockCropCorn` that extends Minecraft's `BlockCrops`. The crop block won't have an `ItemBlock` so this class won't implement `ItemModelProvider` and it is why we need that null check in `ModBlocks.register`.
|
||||||
|
|
||||||
|
In this class, we'll need to override 2 methods to return our own items instead of the vanilla ones. `getSeed` should return `ModItems.cornSeed` and `getCrop` should return `ModItems.corn`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.BlockCrops;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class BlockCropCorn extends BlockCrops {
|
||||||
|
|
||||||
|
public BlockCropCorn() {
|
||||||
|
setUnlocalizedName("cropCorn");
|
||||||
|
setRegistryName("cropCorn");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Item getSeed() {
|
||||||
|
return ModItems.cornSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Item getCrop() {
|
||||||
|
return ModItems.corn;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Minecraft will use the seed and crop we specified to determine what to drop when our crop block is broken. Let's register our block in the `ModBlocks` class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static BlockCropCorn cropCorn;
|
||||||
|
|
||||||
|
public static init() {
|
||||||
|
// ...
|
||||||
|
cropCorn = register(new BlockCropCorn(), null);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The last thing we'll need to do is create a model. Download the textures from [here](https://github.com/shadowfacts/TutorialMod/tree/master/src/main/resources/assets/tutorial/textures/blocks/corn/) and save them into `src/main/resources/assets/tutorial/textures/blocks/corn/` and have there filenames the `0.png` through `7.png`.
|
||||||
|
|
||||||
|
Let's create a blockstate for our crop. Create `src/main/resources/assets/tutorial/blockstates/cropCorn.json`. We're once again going to be using the Forge blockstates format because this is a fairly complicated blockstate.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"defaults": {
|
||||||
|
"model": "cross"
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"age": {
|
||||||
|
"0": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `model` specified in the `defaults` section is Minecraft's `cross` model which is just the same texture rendered twice. You can see what this model looks like by looking at the various flowers in vanilla.
|
||||||
|
|
||||||
|
The `age` property is the age of the crop. All the objects inside the `age` object are for one value of the property. In our case, `age` can have a value 0 through 7 so we'll need separate JSON objects for each of those. For each value of age, we'll have a different texture that is specified in the `textures` object with the name `cross`.
|
||||||
|
|
||||||
|
### Corn Seed
|
||||||
|
Now let's make our corn seed item. Create a new class called `ItemCornSeed` that extends `ItemSeeds` and implements `ItemModelProvider`.
|
||||||
|
|
||||||
|
In our constructor, we'll need to pass a couple of things to the `ItemSeeds` constructor, `ModBlocks.cropCorn` and `Blocks.FARMLAND`. The first parameter of the `ItemSeeds` constructor is the crop block and the second is the soil block. Since we implemented `ItemModelProvider`, we'll need to provide an implementation for `registerItemModel` which will just use our `registerItemRenderer` proxy method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.init.Blocks;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemSeeds;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.block.ModBlocks;
|
||||||
|
|
||||||
|
public class ItemCornSeed extends ItemSeeds implements ItemModelProvider {
|
||||||
|
|
||||||
|
public ItemCornSeed() {
|
||||||
|
super(ModBlocks.cropCorn, Blocks.FARMLAND);
|
||||||
|
setUnlocalizedName("cornSeed");
|
||||||
|
setRegistryName("cornSeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(item, 0, "cornSeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's register our corn seed in `ModItems`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static ItemCornSeed cornSeed;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
cornSeed = register(new ItemCornSeed());
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, we'll create a simple JSON model for the corn seed. First you'll want to download the texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/cornSeed.png) and save it to `src/main/resources/assets/tutorial/textures/items/cornSeed.png`. Now create a JSON file in the `models/item` folder called `cornSeed.json`. This model with be fairly similar to our copper ingot model, it will just have a parent of `item/generated` and a layer 0 texture of `tutorial:items/cornSeed`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/cornSeed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Corn Item
|
||||||
|
For now, our corn item is going to be a simple instance of our `ItemBase` class which means you won't be able to eat it (yet!). Let's add our corn item to our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static ItemBase corn;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
corn = register(new ItemBase("corn").setCreativeTab(CreativeTabs.FOOD));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's also make a simple model for our corn item. Download the texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/corn.png) and save it as `corn.png` in the `textures/items` folder. Now let's create a `corn.json` file for our model. This model will also be very simple, with a parent of `item/generated` and a layer 0 texture of `tutorial:items/corn`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/corn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
Now let's quickly add localization for new items:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.corn.name=Corn
|
||||||
|
item.cornSeed.name=Corn Seed
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finished
|
||||||
|
Now, you should be able to launch game from inside the IDE and see our corn seed in the materials creative tab, plant it, grow it with bone meal, and break it to get corn and more seeds.
|
||||||
|
|
||||||
|
![Corn Screenshot](http://i.imgur.com/1G1k8Sh.png)
|
|
@ -0,0 +1,439 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Dynamic Tile Entity Rendering"
|
||||||
|
metadata.date = "2017-03-30 17:03:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that our Pedestal is capable of storing an item and has a GUI with which players can interact, let's add some rendering code so the stored item actually renders in the world.
|
||||||
|
|
||||||
|
## Updating the `TileEntity`
|
||||||
|
The first thing we'll need to do is make some modifications to our `TileEntityPedestal` to accommodate the new rendering code.
|
||||||
|
|
||||||
|
First off, we'll add a `long` field called `lastChangeTime` to our TE. This field will store the world time, in ticks, of the last time the TE's inventory was modified. This number will be used in our rendering code to make sure that, when the pedestal's item is bobbing up and down (similar to item entities), they're not all synchronized.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
|
||||||
|
private ItemStackHandler inventory = new ItemStackHandler(1);
|
||||||
|
public long lastChangetime;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also add this to the `writeToNBT` and `readFromNBT` methods so that it persists between launches:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||||
|
compound.setTag("inventory", inventory.serializeNBT());
|
||||||
|
compound.setLong("lastChangeTime", lastChangeTime);
|
||||||
|
return super.writeToNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFromNBT(NBTTagCompound compound) {
|
||||||
|
inventory.deserializeNBT(compound.getCompoundTag("inventory"));
|
||||||
|
lastChangeTime = compound.getLong("lastChangeTime");
|
||||||
|
super.readFromNBT(compound);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll make our `inventory` field public and change our `ItemStackHandler` instance to override the `onContentsChanged` method on instantiation. We need this because, when our TE's inventory is updated on the server, we'll need to notify the client of the change so that it renders the correct item.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
|
||||||
|
public ItemStackHandler inventory = new ItemStackHandler(1) {
|
||||||
|
@Override
|
||||||
|
protected void onContentsChanged(int slot) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
lastChangeTime = world.getTotalWorldTime();
|
||||||
|
TutorialMod.network.sendToAllAround(new PacketUpdatedPedestal(TileEntityPedestal.this), new NetworkRegistry.TargetPoint(world.provider.getDimension(), pos.getX(), pos.getY(), pos.getZ(), 64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `onContentsChanged` method is called by Forge's `ItemStackHandler` every time what's stored in a slot changes. We surround our code in a `!world.isRemote` check because we don't want this code, which sends a packet, to be executed on the client and the server, but just the server.
|
||||||
|
|
||||||
|
This code updates the `lastChangeTime` field with the current world time and sends a `PacketUpdatePedestal` (which we'll create in the next section) to every player within 64 meters of our block's position.
|
||||||
|
|
||||||
|
Nextly, we'll override the `onLoad` method. This method will (on the client side) send a packet to the server requesting an update, which will inform the client of what's stored in the pedestal and the last time it was modified. We specifically request a packet when a client loads the TE because the TE is only saved on the server, and the client isn't aware of what data is stored in it, so we specifically request an update from the server.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public void onLoad() {
|
||||||
|
if (world.isRemote) {
|
||||||
|
TutorialMod.network.sendToServer(new PacketRequestUpdatePedestal(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `world.isRemote` check makes sure that the packet is only sent when `onLoad` is being called on the client side and `sendToServer`, as the name suggests, sends the packet to the server. The packet itself, `PacketRequestUpdatePedestal`, will be created in the next section.
|
||||||
|
|
||||||
|
Lastly for the tile entity, we'll override the `getRenderBoundingBox` method. This method returns an Axis Aligned Bounding Box (AABB) which is used by Minecraft to check if our tile entity should be rendered. If the AABB is in view on the player, it will be rendered, otherwise it won't. Because of the way our tile entity will render the item (floating above the pedestal base), we need to use a larger box than normal, so that if the base is out of view but the item isn't, the item is still rendered.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public AxisAlignedBB getRenderBoundingBox() {
|
||||||
|
return new AxisAlignedBB(getPos(), getPos().add(1, 2, 1));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we simply return a new box with the first position being at the block's position and the second position being at the opposite corner that's two above the base of the block.
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
This is the first feature in our mod for which we'll need networking and packets to transmit information between the client and server.
|
||||||
|
|
||||||
|
For networking in our mod, we'll use Forge's SimpleImpl system which is split into three parts:
|
||||||
|
|
||||||
|
1. A channel (a `SimpleNetworkWrapper` instance) which is unique to our mod and is managed by Forge.
|
||||||
|
2. An `IMessage` implementation. This handles serializing/deserializing for transmission over the network.
|
||||||
|
3. An `IMessageHandler` implementation which is used to run code when the packet is received.
|
||||||
|
|
||||||
|
Firstly, we'll need to setup our `SimpleNetworkWrapper` instance. Let's add a field to our main mod class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static SimpleNetworkWrapper wrapper;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then we'll set it up in our `preInit` method:
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
network = NetworkRegistry.INSTANCE.newSimpleChannel(modId);
|
||||||
|
network.registerMessage(new PacketUpdatePedestal.Handler(), PacketUpdatePedestal.class, 0, Side.CLIENT);
|
||||||
|
network.registerMessage(new PacketRequestUpdatePedestal.Handler(), PacketRequestUpdatePedestal.class, 1, Side.SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this code, we:
|
||||||
|
|
||||||
|
1. Call `newSimpleChannel` with our mod ID to obtain a `SimpleNetworkWrapper` instance specific to our mod.
|
||||||
|
2. Call `registerMessage` on our channel to register our two packets. For each packet, we pass in the `IMessageHandler` instance, the class of the `IMessage` implementation, the ID (unique to our channel) of the packet, and the `Side` on which it's received.
|
||||||
|
|
||||||
|
The two packets we register are: the `PacketUpdatePedestal`, which is sent from the server to the client and updates the item stored in the pedestal on the client side, and the `PacketRequestUpdatedPedestal` , which is sent from the client to the server to request an update from the server. The `PacketUpdatePedestal` is used whenever the item changes on the server to notify the client. The `PacketRequestUpdatePedestal` is sent to the server when the client first joins to get the stored stack (because the data is only saved on the server, not the client).
|
||||||
|
|
||||||
|
Let's start with the `PacketUpdatePedestal` class. We'll create it in a new `network` package and we'll make it implement Forge's `IMessage` interface.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.network;
|
||||||
|
|
||||||
|
import net.minecraftforge.fml.network.simpleimpl.IMessage;
|
||||||
|
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first thing we'll need to do is add a couple fields to the packet. The fields we'll need are: `BlockPos pos` for the tile entity's position, `ItemStack stack` for the stack that's stored in the pedestal, and `long lastChangeTime` for the last time the pedestal's constant has changed.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
|
||||||
|
private BlockPos pos;
|
||||||
|
private ItemStack stack;
|
||||||
|
private long lastChangeTime;
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll add a couple constructors. The first constructor will take parameters for all three fields and assign them as expected. The second one will be a convince constructor that takes a parameter for the `TileEntityPedestal` and calls the first constructor will all the values determined from the TE. The last constructor will take no parameters and won't initialize any of the fields. This is necessary because Forge's SimpleImpl will call it via reflection and then call the `fromBytes` method which will initialize the fields.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public PacketUpdatePedestal(BlockPos pos, ItemStack stack, long lastChangeTime) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.stack = stack;
|
||||||
|
this.lastChangeTime = lastChangeTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketUpdatePedestal(TileEntityPedestal te) {
|
||||||
|
this(te.getPos(), te.inventory.getStackInSlot(0), te.lastChangeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketUpdatePedestal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll implement the `toBytes` and `fromBytes` methods which respectively serialize to a `ByteBuf` for transmission over the network and deserialize it back from a `ByteBuf`. One key thing about these two methods is that because they're serializing/deserializing from a `ByteBuf`, which is just a sequence of bytes, we need to do everything in the same order in both methods.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toBytes(ByteBuf buf) {
|
||||||
|
buf.writeLong(pos.toLong());
|
||||||
|
ByteBufUtils.writeItemStack(buf, stack);
|
||||||
|
buf.writeLong(lastChangeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromBytes(ByteBuf buf) {
|
||||||
|
pos = BlockPos.fromLong(buf.readLong());
|
||||||
|
stack = ByteBufUtils.readItemStack(buf);
|
||||||
|
lastChangeTime = buf.readLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First, we use `BlockPos`' `toLong` and `fromLong` to serialize that, then we use the helper methods in `ByteBufUtils` to serialize the stack, and lastly we write the `lastChangeTime`. Note that we're reading/writing things in the same order in both these methods. This is very important, and if something's out of order, we'll end up getting the wrong data when our packet is received.
|
||||||
|
|
||||||
|
Lastly, we'll add the handler. Let's create a static inner `Handler` class in our `PacketUpdatePedestal` class that implements `IMessageHandler`. This interface takes two generic type parameters: the type of the packet that the handler's handling and the type of the packet that the handler responds with. The first type will obviously be `PacketUpdatePedestal` and, because we don't want to respond with a packet, the return packet type will just be `IMessage` and we'll return `null` from the handler method.
|
||||||
|
|
||||||
|
What we're going to do in the handler's `onMessage` method is get the tile entity from the world and update its inventory and `lastChangeTime`. Unfortunately, there's a caveat to this so it's a bit more complicated. With Netty (the library Minecraft and Forge use for networking), packets are handled on a different thread that's not the main thread. Because we're going to be interacting with and modifying the world, we can't just do it from a different thread because it could potentially cause a `ConcurrentModificationException` to be thrown. To deal with this, we'll call the `Minecraft.addScheduledTask` method which executes the given `Runnable` on the main thread as soon as possible, so in this runnable, we _can_ interact with the world.
|
||||||
|
|
||||||
|
In the runnable, we simply get the tile entity from the client world (`Minecraft.getMinecraft().world`) and modify its inventory and set its `lastChangeTime` field.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public static class Handler implements IMessageHandler<PacketUpdatePedestal, IMessage> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IMessage onMessage(PacketUpdatePedestal message, MessageContext ctx) {
|
||||||
|
Minecraft.getMinecraft().addScheduledTask(() -> {
|
||||||
|
TileEntityPedestal te = (TileEntityPedestal)Minecraft.getMinecraft().world.getTileEntity(message.pos);
|
||||||
|
te.inventory.setStackInSlot(0, message.stack);
|
||||||
|
te.lastChangeTime = message.lastChangeTime;
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly for networking, we'll add one more packet: `PacketRequestUpdatePedestal`. This packet is sent from the client to the server when the client loads the tile entity and needs to get its data from the server. This packet will be fairly similar to the previous one, so I won't go over it in as much detail.
|
||||||
|
|
||||||
|
This packet has a position and a dimension ID. (This one needs a dimension ID unlike the previous one because this will be received on the server which has multiple worlds, and we need a way to determine which one to use, whereas the previous packet was received on the client which only ever has one world.) These are serialized/deserialized as you'd expect.
|
||||||
|
|
||||||
|
The handler class, however, has a slight difference. Unlike the `PacketUpdatePedestal`, this packet has a response packet. So for the generic types we'll use `PacketRequestUpdatePedestal` and `PacketUpdatePedestal`. In the `onMessage` method, we'll call `FMLCommonHandler.instance().getMinecraftServerInstance()` to obtain the instance of `MinecraftServer`, which stores all the worlds. On that instance we'll call `worldServerForDimension` with the dimension from the packet to obtain the `World` instance. We then get the tile entity and return a new `PacketUpdatePedestal` from it which is sent back to the client.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketRequestUpdatePedestal implements IMessage {
|
||||||
|
|
||||||
|
private BlockPos pos;
|
||||||
|
private int dimension;
|
||||||
|
|
||||||
|
public PacketRequestUpdatePedestal(BlockPos pos, int dimension) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.dimension = dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketRequestUpdatePedestal(TileEntityPedestal te) {
|
||||||
|
this(te.getPos(), te.getWorld().provider.getDimension());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketRequestUpdatePedestal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toBytes(ByteBuf buf) {
|
||||||
|
buf.writeLong(pos.toLong());
|
||||||
|
buf.writeInt(dimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromBytes(ByteBuf buf) {
|
||||||
|
pos = BlockPos.fromLong(buf.readLong());
|
||||||
|
dimension = buf.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Handler implements IMessageHandler<PacketRequestUpdatePedestal, PacketUpdatePedestal> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PacketUpdatePedestal onMessage(PacketRequestUpdatePedestal message, MessageContext ctx) {
|
||||||
|
World world = FMLCommonHandler.instance().getMinecraftServerInstance().worldServerForDimension(message.dimension);
|
||||||
|
TileEntityPedestal te = (TileEntityPedestal)world.getTileEntity(message.pos);
|
||||||
|
if (te != null) {
|
||||||
|
return new PacketUpdatePedestal(te);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** See the [official Forge documentation](http://mcforge.readthedocs.io/en/latest/networking/simpleimpl/) for more information about the SimpleImpl networking system.
|
||||||
|
|
||||||
|
## `TileEntitySpecialRenderer`
|
||||||
|
|
||||||
|
Now that we've updated the tile entity and finished all the networking code, we can finally write the renderer itself.
|
||||||
|
|
||||||
|
Let's create a class called `TESRPedestal` in our `block.pedestal` package that extends `TileEntitySpecialRenderer`. The generic type parameter is the type of our tile entity, so we'll use `TileEntityPedestal`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
|
||||||
|
|
||||||
|
public class TESRPedestal extends TileEntitySpecialRenderer<TileEntityPedestal> {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll override the `renderTileEntityAt` method. First, we'll get the stored stack from the tile entity, and then, if the stack isn't `null`, setup the GL state, render the stack, and reset the GL state.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TESRPedestal extends TileEntitySpecialRenderer<TileEntityPedestal> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderTileEntityAt(TileEntityPedestal te, double x, double y, double z, float partialTicks, int destroyStage) {
|
||||||
|
ItemStack stack = te.inventory.getStackInSlot(0);
|
||||||
|
if (stack != null) {
|
||||||
|
GlStateManager.enableRescaleNormal();
|
||||||
|
GlStateManager.alphaFunc(GL11.GL_GREATER, 0.1f);
|
||||||
|
GlStateManager.enableBlend();
|
||||||
|
RenderHelper.enableStandardItemLighting();
|
||||||
|
GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, 1, 0);
|
||||||
|
GlStateManager.pushMatrix();
|
||||||
|
double offset = Math.sin((te.getWorld().getTotalWorldTime() - te.lastChangeTime + partialTicks) / 8) / 4.0;
|
||||||
|
GlStateManager.translate(x + 0.5, y + 1.25 + offset, z + 0.5);
|
||||||
|
GlStateManager.rotate((te.getWorld().getTotalWorldTime() + partialTicks) * 4, 0, 1, 0);
|
||||||
|
|
||||||
|
IBakedModel model = Minecraft.getMinecraft().getRenderItem().getItemModelWithOverrides(stack, te.getWorld(), null);
|
||||||
|
model = ForgeHooksClient.handleCameraTransforms(model, ItemCameraTransforms.TransformType.GROUND, false);
|
||||||
|
|
||||||
|
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE);
|
||||||
|
Minecraft.getMinecraft().getRenderItem().renderItem(stack, model);
|
||||||
|
|
||||||
|
GlStateManager.popMatrix();
|
||||||
|
GlStateManager.disableRescaleNormal();
|
||||||
|
GlStateManager.disableBlend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After setting up the GL state (lighting, blending, etc.), we perform the translation (`GlStateManager.translate`) and rotation (`GlStateManager.rotate`).
|
||||||
|
|
||||||
|
There are two parts to the translation: we translate it to `x + 0.5`, `y + 1.25`, and `z + 0.5` which is above the center of our block, on top of the pedestal. The second part is the part that changes: the `offset` in the `y` value. The offset is the height of the item for any given frame. We recalculate this each time because we want it to be animating bouncing up and down. We calculate this by:
|
||||||
|
|
||||||
|
1. Taking the time (in ticks) since the pedestal was modified by subtracting the `lastChangeTime` from the current total world time.
|
||||||
|
2. Adding the partial ticks. (The partial ticks is a fractional value representing the amount of time that's passed between the last full tick and now. We use this because otherwise the animation would be jittery because there are fewer ticks per second than frames per second.)
|
||||||
|
3. Dividing that by 8 to slow the movement down.
|
||||||
|
4. Taking the sine of that to produce a value that's oscillating back and forth.
|
||||||
|
5. Dividing that by 4 to compress the sine wave vertically so the item doesn't move up and down as much.
|
||||||
|
|
||||||
|
Nextly, we perform the rotation. For this, we take the total world time and add `partialTicks` to it, and multiply it by 4. Unlike for the translation, we don't use a sine wave because we want the item to rotate at a fixed speed. With a sine wave, the rate of rotation would change and it would look rather weird. The last three parameters to the `GlStateManager.rotate` is the vector about which it will be rotated. Because we want to rotate it horizontally we use the vector along the Y-axis: (0, 1, 0).
|
||||||
|
|
||||||
|
Now, that the GL state is all setup the way we want it, we can actually render the model. We call `getItemModelWithOverrides` on the `RenderItem` instance obtained from `Minecraft.getMinecraft()` with parameters for the `ItemStack` to be rendered, the `World` it'll be rendered in, and `null` for the entity parameter to indicate that there is no entity. This gives the `IBakedModel` instance for the `ItemStack`.
|
||||||
|
|
||||||
|
`IBakedModel` is a sort of "compiled" representation of a model. It has all of the data from the JSON model (or another source) compressed down into a list of `BakedQuad`s that can be passed directly to OpenGL to be rendered.
|
||||||
|
|
||||||
|
Now that we've got the `IBakedModel` instance, we call `ForgeHooksClient.handleCameraTransforms` with some parameters: the model that it should handle the transformations for, the type of transformations that should be applied (in this case, `TransformType.GROUND` because on the ground is the closest to what we want because we're rendering it in the world), and `false` for the last parameter because we are not rendering the item in the left hand.
|
||||||
|
|
||||||
|
The `handleCameraTransforms` method handles everything necessary for one of Forge's features: `IPerspectiveAwareModel`. This is an extension of the `IBakedModel` interface which allows the model to be overridden depending on how it's being rendered and provide custom transformations from code. If the model we've gotten from `getItemModelWithOverriddes` is an `IPerspectiveAwareModel`, Forge will call the correct method to get the transformation for the given type (in this case, `GROUND`) and apply those transformations to the current GL state.
|
||||||
|
|
||||||
|
Now that we've got the model, we call `bindTexture` on the `TextureManager` instance obtained from `Minecraft.getMinecraft().getTextureManager()` with the ID of the texture we want to bind. In this case, because we want to be bind the main texture map which contains all of the textures that are used in the models, we use the ID `TextureMap.LOCATION_BLOCKS_TEXTURE`. This field name is a bit of a misnomer because it's not just for blocks, it stores the textures for items as well.
|
||||||
|
|
||||||
|
With the texture map bound, we can finally render the item itself. We call `renderItem` with the `ItemStack` we're rendering and the `IBakedModel` to render on the `RenderItem` instance.
|
||||||
|
|
||||||
|
Once we've finished rendering, we reset the GL state back to what it was before our TESR started rendering.
|
||||||
|
|
||||||
|
Lastly, we'll need to register our TESR. Let's create a new method in our proxies called `registerRenderers`. In our `CommonProxy` this method won't do anything because we only want to register our renderers on the client side. In our `ClientProxy` we'll bind our TESR to our tile entity so that it gets rendered.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class CommonProxy {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public void registerRenderers() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ClientProxy extends CommonProxy {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerRenderers() {
|
||||||
|
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityPedestal.class, new TESRPedestal());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We call `bindTileEntitySpecialRenderer` to bind a new instance of our `TESRPedestal` to the `TileEntityPedestal` class. This way, for every instance of the `TileEntityPedestal` class that's in the world, the `renderTileEntityAt` method of our TESR will be called.
|
||||||
|
|
||||||
|
Lastly, we'll update our main mod class to call `registerRenderers` on our proxy:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
proxy.registerRenderers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now that we've finally got all the code done, we can launch Minecraft and take a look at how our Pedestals render:
|
||||||
|
|
||||||
|
![Items in Pedestals Rendering In-World](https://fat.gfycat.com/MenacingRipeCod.gif)
|
|
@ -0,0 +1,76 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Food"
|
||||||
|
metadata.date = "2016-08-12 18:04:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
We've already made a Corn item for our [crop](/tutorials/forge-modding-1102/crops/), however, we were unable to eat the corn (defeating its very purpose). Let's make our Corn behave as actual food.
|
||||||
|
|
||||||
|
First we'll need to create an `ItemCorn` class that will be our new corn item, instead of just using `ItemOre`. Our class will extend `ItemFood` so it inherits all of the vanilla food-handling logic. We'll also want our class to implement `ItemModelProvider` and `ItemOreDict` so it retains the functionality from the existing corn item.
|
||||||
|
|
||||||
|
The `ItemFood` constructor takes 3 parameters:
|
||||||
|
|
||||||
|
1. The amount of hunger restored by this food.
|
||||||
|
2. The saturation given by this food.
|
||||||
|
3. If this food is edible by wolves.
|
||||||
|
|
||||||
|
We'll pass in `3`, `0.6f`, and `false` for the hunger, saturation, and wolf food values, the same values as the Carrot. Also in the constructor, we'll call `setUnlocalizedName` and `setRegistryName` with the same value as we used for the original corn item (`corn`). We'll also call `setCreativeTab` with our custom creative tab.
|
||||||
|
|
||||||
|
We'll also need to override the `registerItemModel` and `initOreDict` methods from the interfaces we implemented.
|
||||||
|
|
||||||
|
In `registerItemModel`, we'll use our proxy `registerItemRenderer` method to register an item model for our corn item. We'll use `corn` as the model name, the same as our original item.
|
||||||
|
|
||||||
|
We'll also override `initOreDict` and call the `OreDictionary.registerOre` method with `cropCorn` as the ore name, the same as our original item.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemFood;
|
||||||
|
import net.minecraftforge.oredict.OreDictionary;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemCorn extends ItemFood implements ItemModelProvider, ItemOreDict {
|
||||||
|
|
||||||
|
public ItemCorn() {
|
||||||
|
super(3, 0.6f, false);
|
||||||
|
setUnlocalizedName("corn");
|
||||||
|
setRegistryName("corn");
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, "corn");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initOreDict() {
|
||||||
|
OreDictionary.registerOre("cropCorn", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `ModItems` class, we'll also need to change the `corn` field.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemCorn corn;
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
corn = register(new ItemCorn());
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We simply need to change the `corn` field to by of item `ItemCorn` and the registration call to instantiate `ItemCorn` instead of `ItemOre`.
|
||||||
|
|
||||||
|
Now we've got an edible corn item!
|
||||||
|
|
||||||
|
![Edible Corn](http://i.imgur.com/aT5BZ5x.png)
|
|
@ -0,0 +1,206 @@
|
||||||
|
```
|
||||||
|
metadata.title = "JSON Block Models"
|
||||||
|
metadata.date = "2016-08-08 13:58:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
We're going to add a new block that has a custom JSON model (that is, one defined completely by us, not one of Mojang's).
|
||||||
|
|
||||||
|
The first thing we'll need to do is create a block class. We need to create a new class instead of just using the `BlockBase` class because we'll need to override a couple of methods to have the model render properly. Our `BlockPedestal` class will extend our `BlockBase` class so we can use the code we've already written for item model registration.
|
||||||
|
|
||||||
|
The two methods we'll be override are `isOpaqueCube` and `isFullCube`. In both of these methods, we'll want to return false from both of these methods in order to change some of the default Minecraft behavior.
|
||||||
|
|
||||||
|
`isOpaqueCube` is used to determine if this block should cull faces of the adjacent block. Since our block doesn't take up the entirety of the 1m^3 cube, we'll want to return `false` so the faces of adjacent blocks can be seen behind our block.
|
||||||
|
|
||||||
|
`isFullCube` is used to determine if light should pass through the block. Once again, we'll want to return `false` because our block is less than 1m^3 so we'll want light to propagate through it.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.block.state.IBlockState;
|
||||||
|
|
||||||
|
public class BlockPedestal extends BlockBase {
|
||||||
|
|
||||||
|
public BlockPedestal() {
|
||||||
|
super(Material.ROCK, "pedestal");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public boolean isOpaqueCube(IBlockState state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public boolean isFullCube(IBlockState state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to register our pedestal block in our `ModBlocks` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
pedestal = register(new BlockPedestal());
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to add a localization for the pedestal block!
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Blocks
|
||||||
|
# ...
|
||||||
|
tile.pedestal.name=Pedestal
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we'll need to create a blockstate file at `assets/tutorial/blockstates/pedestal.json` that tells Forge which model to use for the normal variant and the inventory variant.
|
||||||
|
|
||||||
|
**Note:** See [Basic Forge Blockstates](/tutorials/forge-modding-1102/basic-forge-blockstates/) for more information about the Forge blockstate format.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"variants": {
|
||||||
|
"normal": {
|
||||||
|
"model": "tutorial:pedestal"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"model": "tutorial:pedestal",
|
||||||
|
"transform": "forge:default-block"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our blockstate file does a couple of things.
|
||||||
|
|
||||||
|
1. It instructs Forge to use the model at `assets/tutorial/models/block/pedestal.json` for both the `normal` and `inventory` variants.
|
||||||
|
2. It uses the `forge:default-block` transformation for the inventory variant. This makes the block appear at the proper angle in the inventory and in the hand.
|
||||||
|
|
||||||
|
Now we need to create the Pedestal model itself. The model will be located at `assets/tutorial/models/block/pedestal.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"textures": {
|
||||||
|
"pedestal": "blocks/stonebrick",
|
||||||
|
"particle": "blocks/stonebrick"
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"from": [3, 0, 3],
|
||||||
|
"to": [13, 11, 13],
|
||||||
|
"faces": {
|
||||||
|
"down": {
|
||||||
|
"uv": [0, 0, 10, 10],
|
||||||
|
"texture": "#pedestal",
|
||||||
|
"cullface": "down"
|
||||||
|
},
|
||||||
|
"north": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": [2, 11, 2],
|
||||||
|
"to": [14, 12, 14],
|
||||||
|
"faces": {
|
||||||
|
"down": {
|
||||||
|
"uv": [0, 0, 12, 12],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"north": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": [1, 12, 1],
|
||||||
|
"to": [15, 13, 15],
|
||||||
|
"faces": {
|
||||||
|
"down": {
|
||||||
|
"uv": [0, 0, 14, 14],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"uv": [0, 0, 14, 14],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"north": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The model has two primary parts, the `textures` and the `elements`.
|
||||||
|
|
||||||
|
The `textures` section contains a map of texture name to the location of the texture itself. We define the `pedestal` texture as `blocks/stonebrick` and then reference it using `#pedestal` in the face `texture` attribute. (The `particle` texture is used by Minecraft to generate the block breaking particle.)
|
||||||
|
|
||||||
|
The `elements` section is an array of the elements in the model.
|
||||||
|
|
||||||
|
Each element has 3 properties:
|
||||||
|
|
||||||
|
1. `from`: This is the bottom/left/backmost point of the element.
|
||||||
|
2. `to`: This is the top/right/frontmost point of the element. With the `from` property, this is used to determine the size of the element.
|
||||||
|
3. `faces`: This is an object containing a map of directions to faces. All the faces are optional.
|
||||||
|
|
||||||
|
Each face has several properties:
|
||||||
|
|
||||||
|
1. `texture`: This is the texture to use for the face. This can be a reference to a predefined texture (e.g. `#pedestal`) or a direct reference (e.g. `blocks/stonebrick`).
|
||||||
|
2. `uv`: This is an array of 4 integer elements representing the minimum U, minimum V, maximum U, and maximum V (in that order).
|
||||||
|
3. `cullface`: This is optional. If specified, this face will be culled if there is a solid block against the specified face of the block.
|
||||||
|
|
||||||
|
![Finished Pedestal Model](http://i.imgur.com/Axt5iiE.png)
|
|
@ -0,0 +1,36 @@
|
||||||
|
```
|
||||||
|
metadata.title = "JSON Item Models"
|
||||||
|
metadata.date = "2016-05-07 16:32:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Models for items and blocks are created using Mojang's fairly simple JSON format. We're going to create a simple item model for our copper ingot.
|
||||||
|
|
||||||
|
Before we do this, we'll need to add our copper ingot texture. Download the copper ingot texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/ingotCopper.png) and save it to `src/main/resources/assets/tutorial/textures/items/ingotCopper.png` so we can use it from our model.
|
||||||
|
|
||||||
|
|
||||||
|
Now we'll create a simple JSON model and save it to `src/main/resources/assets/tutorial/models/item/ingotCopper.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/ingotCopper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's go over what each bit does:
|
||||||
|
|
||||||
|
- `parent`: specifies which model to use as the parent. We're using `item/generated` which is a code model that is part of Minecraft that generates that nice 3D look that items have.
|
||||||
|
- `textures`: specifies all the textures to use for this model.
|
||||||
|
- `layer0`: Our model only has one texture so we only need to specify one layer.
|
||||||
|
- `tutorial:items/ingotCopper`: There are two important parts of this bit.
|
||||||
|
- `tutorial` specifies that the texture is part of the `tutorial` domain.
|
||||||
|
- `items/ingotCopper` specifies what path the texture is at.
|
||||||
|
- These are all combined to get the entire path to the texture `assets/tutorial/textures/items/ingotCopper.png`
|
||||||
|
|
||||||
|
Now our item has a nice texture and nice model in-game!
|
||||||
|
|
||||||
|
![Copper Ingot Model/Texture Screenshot](http://i.imgur.com/cup7xwW.png)
|
|
@ -0,0 +1,30 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Localization"
|
||||||
|
metadata.date = "2016-05-08 09:15:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
If you recall, we used the `setUnlocalizedName` method in both our `BlockBase` and `ItemBase` classes. The name that we passed into that method is what Minecraft uses when localizing the name of our block or item for the currently active language.
|
||||||
|
|
||||||
|
In these tutorials, we are only going to add English localizations however you can easily add more localizations by following the same pattern.
|
||||||
|
|
||||||
|
Language files are located at `src/main/resources/assets/tutorial/lang/IDENTIFIER.lang` where `IDENTIFIER` is the locale code of the language. Let's create a localization file with the identifier `en_US` (see [here](http://minecraft.gamepedia.com/Language) for more locale codes).
|
||||||
|
|
||||||
|
Language files are written in a simple `key=value` format with one entry per line. The `value` is obviously the translated name, this obviously differs for every language file. The `key` is the key that Minecraft uses when translating things. This is slightly different for blocks and items. For blocks the key is `tile.UNLOCALIZED.name`. For items the key is `item.UNLOCALIZED.name`. Where `UNLOCALIZED` is what we passed into `setUnlocalizedName`.
|
||||||
|
|
||||||
|
**Note:** Lines starting with `#` are comments and won't be parsed.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
item.ingotCopper.name=Copper Ingot
|
||||||
|
|
||||||
|
# Blocks
|
||||||
|
tile.oreCopper.name=Copper Ore
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, both our Copper Ore and Copper Ingot have properly localized names!
|
||||||
|
|
||||||
|
![Copper Ore Screenshot](http://i.imgur.com/f6T09kI.png)
|
||||||
|
![Copper Ingot Screenshot](http://i.imgur.com/oafpj5q.png)
|
|
@ -0,0 +1,50 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Main Mod Class"
|
||||||
|
metadata.date = "2016-05-07 15:27:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Every mod has a main mod class that Forge loads and uses as a starting point when it runs your mod. Before getting started, you'll want to delete all the exist code that comes in the MDK by deleting the `com.example.examplemod` package. For this tutorial, I'll be putting all of the code in the `net.shadowfacts.tutorial` package, so you'll need to create that in your IDE. Next, create a class called `TutorialMod`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial;
|
||||||
|
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
|
||||||
|
import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
|
||||||
|
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
|
||||||
|
|
||||||
|
@Mod(modid = TutorialMod.modId, name = TutorialMod.name, version = TutorialMod.version, acceptedMinecraftVersions = "[1.10.2]")
|
||||||
|
public class TutorialMod {
|
||||||
|
|
||||||
|
public static final String modId = "tutorial";
|
||||||
|
public static final String name = "Tutorial Mod";
|
||||||
|
public static final String version = "1.0.0";
|
||||||
|
|
||||||
|
@Mod.Instance(modId)
|
||||||
|
public static TutorialMod instance;
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
System.out.println(name + " is loading!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void init(FMLInitializationEvent event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void postInit(FMLPostInitializationEvent event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if we run the Minecraft Client through IDEA, `Tutorial Mod is loading!` should be printed out in the console. Now that we've got some code that's actually running, let's take a look at what it does.
|
||||||
|
|
||||||
|
- `@Mod(...)` (L8): This marks our `TutorialMod` class as a main mod class so that Forge will load it.
|
||||||
|
- `@Mod.Instance(modId)` (L15-L16): The `@Mod.Instance` annotation marks this field so that Forge will inject the instance of our mod that is used into it. This will become more important later when we're working with GUIs.
|
||||||
|
- `@Mod.EventHandler` methods (L15, L20, L25): This annotation marks our `preInit`, `init`, and `postInit` methods to be called by Forge. Forge determines which method to call for which lifecycle event by checking the parameter of the method, so these methods can be named anything you want.
|
|
@ -0,0 +1,188 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Ore dictionary"
|
||||||
|
metadata.date = "2016-08-08 11:28:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Forge's Ore Dictionary system provides an API that modders can use to mark items/blocks as equivalent to one another. This was originally created because multiple mods were all adding their own versions of the same ores and ingots (copper, tin, etc.). The way this system works is each `ItemStack` as a list of `String` ore names associated with it.
|
||||||
|
|
||||||
|
Let's create an `ItemOreDict` interface in the `item` package of our mod. This interface will be used to mark our items/blocks to be registered with the Ore Dictionary. This interface will have a single abstract method called `initOreDict` that performs the registration.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
public interface ItemOreDict {
|
||||||
|
|
||||||
|
void initOreDict();
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also create a `ItemOre` class that extends `ItemBase` and implements `ItemOreDict` to give us a nice fully implemented class for ore-dictionaried items.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraftforge.oredict.OreDictionary;
|
||||||
|
|
||||||
|
public class ItemOre extends ItemBase implements ItemOreDict {
|
||||||
|
|
||||||
|
private String oreName;
|
||||||
|
|
||||||
|
public ItemOre(String name, String oreName) {
|
||||||
|
super(name);
|
||||||
|
|
||||||
|
this.oreName = oreName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initOreDict() {
|
||||||
|
OreDictionary.registerOre(oreName, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This class simply takes a second `String` parameter in its constructor that is the ore-dictionary name and then uses that in the `initOreDict` method.
|
||||||
|
|
||||||
|
We'll do something similar for our `BlockOre` class, that is, have it implement `ItemOreDict` and `initOreDict` and have a second parameter for the ore dictionary name.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraftforge.oredict.OreDictionary;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemOreDict;
|
||||||
|
|
||||||
|
public class BlockOre extends BlockBase implements ItemOreDict {
|
||||||
|
|
||||||
|
private String oreName;
|
||||||
|
|
||||||
|
public BlockOre(String name, String oreName) {
|
||||||
|
super(Material.ROCK, name);
|
||||||
|
|
||||||
|
this.oreName = oreName;
|
||||||
|
|
||||||
|
setHardness(3f);
|
||||||
|
setResistance(5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initOreDict() {
|
||||||
|
OreDictionary.registerOre(oreName, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockOre setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll need to make some changes to our `ModItems` and `ModBlocks` classes so they call the `initOreDict` method after the item/block is registered with the `GameRegistry`.
|
||||||
|
|
||||||
|
We'll first check if the item implements our `ItemOreDict` interface (because not all our items will use the Ore Dictionary) and if so, call the `initOreDict` method on it.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private static <T extends Item> T register(T item) {
|
||||||
|
GameRegistry.register(item);
|
||||||
|
|
||||||
|
if (item instanceof ItemModelProvider) {
|
||||||
|
((ItemModelProvider)item).registerItemModel(item);
|
||||||
|
}
|
||||||
|
if (item instanceof ItemOreDict) {
|
||||||
|
((ItemOreDict)item).initOreDict();
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll do this similarly in the `ModBlocks` class except we'll check `instanceof ItemOreDict` and call `initOreDict` on both the block itself and the associated `ItemBlock`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block, ItemBlock itemBlock) {
|
||||||
|
GameRegistry.register(block);
|
||||||
|
if (itemBlock != null) {
|
||||||
|
GameRegistry.register(itemBlock);
|
||||||
|
|
||||||
|
if (block instanceof ItemModelProvider) {
|
||||||
|
((ItemModelProvider)block).registerItemModel(itemBlock);
|
||||||
|
}
|
||||||
|
if (block instanceof ItemOreDict) {
|
||||||
|
((ItemOreDict)block).initOreDict();
|
||||||
|
}
|
||||||
|
if (itemBlock instanceof ItemOreDict) {
|
||||||
|
((ItemOreDict)itemBlock).initOreDict();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got all our base classes setup, we're going to modify some of our items and blocks to given the ore dictionary names!
|
||||||
|
|
||||||
|
The only block that will have an ore dictionary name is the Copper Ore block. Following with the conventions for ore dictionary names (if you look in the `OreDictionary` class, you can get a general idea for what these conventions are), our Copper Ore block will have an ore dictionary name of `oreCopper`.
|
||||||
|
|
||||||
|
We'll simply change our registration call for the Copper Ore block to have a second parameter that is also `"oreCopper"`, telling the `BlockOre` class to use `oreCopper` as the ore dictionary name for that block.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
oreCopper = register(new BlockOre("oreCopper", "oreCopper"));
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll now change both our Copper Ingot and Corn items to have ore dictionary names `ingotCopper` and `cropCorn` respectively. All this requires is changing the `ItemBase` instantiations to `ItemOre` instantiations and passing in the desired ore dictionary name as the second constructor parameter.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
ingotCopper = register(new ItemOre("ingotCopper", "ingotCopper"));
|
||||||
|
corn = register(new ItemOre("corn", "cropCorn"));
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recipes
|
||||||
|
Now that we've got ore dictionary names for some of our items and blocks, let's add recipes that utilize them. Forge adds the `ShapedOreRecipe` and `ShapelessOreRecipe` classes that are identical to the vanilla shaped and shapeless recipes, except instead of just accepting an item/block/stack for the input, they can also accept a string of an ore dictionary name that will match anything with that given name.
|
||||||
|
|
||||||
|
These recipes need to be instantiated manually and registered using `GameRegistry.addRecipe` unlike normal shaped/shapeless recipes which have convenience methods in `GameRegistry`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModRecipes {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
GameRegistry.addRecipe(new ShapedOreRecipe(Items.BUCKET, "I I", " I ", 'I', "ingotCopper"));
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This recipe is the same as the vanilla bucket recipe, except it matches any item with the `ingotCopper` ore dictionary name instead of just iron ingots.
|
||||||
|
|
||||||
|
![Shaped Ore Recipe](http://i.imgur.com/OICDDTJ.png)
|
|
@ -0,0 +1,60 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Overview"
|
||||||
|
metadata.date = "2016-05-06 10:00:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### About
|
||||||
|
This series of tutorials teaches modding [Minecraft](https://minecraft.net) version 1.10.2 using [Forge](http://minecraftforge.net).
|
||||||
|
|
||||||
|
**This tutorial series does not teach Java. You should already know Java before you try to mod Minecraft.**
|
||||||
|
|
||||||
|
### Tutorials
|
||||||
|
- [Setting up the Development Environment](/tutorials/forge-modding-1102/workspace-setup/)
|
||||||
|
- [Main Mod Class](/tutorials/forge-modding-1102/main-mod-class/)
|
||||||
|
- [Proxy System](/tutorials/forge-modding-1102/proxy-system/)
|
||||||
|
- [Basic Items](/tutorials/forge-modding-1102/basic-items/)
|
||||||
|
- [JSON Item Models](/tutorials/forge-modding-1102/json-item-models/)
|
||||||
|
- [Basic Blocks](/tutorials/forge-modding-1102/basic-blocks/)
|
||||||
|
- [Basic Forge Blockstates](/tutorials/forge-modding-1102/basic-forge-blockstates/)
|
||||||
|
- [Localization](/tutorials/forge-modding-1102/localization/)
|
||||||
|
- [Crops](/tutorials/forge-modding-1102/crops/)
|
||||||
|
- [Creative Tabs](/tutorials/forge-modding-1102/creative-tabs/)
|
||||||
|
- [Advanced Creative Tabs](/tutorials/forge-modding-1102/advanced-creative-tabs/)
|
||||||
|
- [Crafting/Smelting Recipes](/tutorials/forge-modding-1102/crafting-smelting-recipes/)
|
||||||
|
- [Ore Dictionary](/tutorials/forge-modding-1102/ore-dictionary/)
|
||||||
|
- [JSON Block Models](/tutorials/forge-modding-1102/json-block-models/)
|
||||||
|
- [Food](/tutorials/forge-modding-1102/food/)
|
||||||
|
- [Tools](/tutorials/forge-modding-1102/tools/)
|
||||||
|
- [Armor](/tutorials/forge-modding-1102/armor/)
|
||||||
|
- [World Generation: Ore](/tutorials/forge-modding-1102/world-generation-ore/)
|
||||||
|
- World Generation: Tree
|
||||||
|
- World Generation: Structure
|
||||||
|
- mcmod.info
|
||||||
|
- [Tile Entities](/tutorials/forge-modding-1102/tile-entities/)
|
||||||
|
- [Tile Entities with Inventory](/tutorials/forge-modding-1102/tile-entities-inventory/)
|
||||||
|
- [Tile Entities with Inventory GUI](/tutorials/forge-modding-1102/tile-entities-inventory-gui/)
|
||||||
|
- [Dynamic Tile Entity Rendering](/tutorials/forge-modding-1102/dynamic-tile-entity-rendering/)
|
||||||
|
- Advanced GUIs with Widgets
|
||||||
|
- Energy API - RF - Items
|
||||||
|
- Energy API - RF - Blocks
|
||||||
|
- Energy API - RF - GUI Widget
|
||||||
|
- Energy API - Tesla - Items
|
||||||
|
- Energy API - Tesla - Blocks
|
||||||
|
- Energy API - Tesla - GUI Widget
|
||||||
|
- Energy API - Forge Energy - Items
|
||||||
|
- Energy API - Forge Energy - Blocks
|
||||||
|
- Energy API - Forge Energy - GUI Widget
|
||||||
|
- Configuration
|
||||||
|
- Packets and Packet Handlers
|
||||||
|
- Event Handling
|
||||||
|
- Keybindings
|
||||||
|
- Commands
|
||||||
|
|
||||||
|
### Other Resources
|
||||||
|
- [Forge Forum](http://minecraftforge.net/)
|
||||||
|
- [Forge Docs](https://mcforge.readthedocs.io/en/latest/)
|
||||||
|
- #minecraftforge ([esper](https://esper.net)) IRC
|
||||||
|
- [TheGreyGhost's Blog](http://greyminecraftcoder.blogspot.com.au/p/list-of-topics.html)
|
||||||
|
- [MinecraftByExample](https://github.com/TheGreyGhost/MinecraftByExample)
|
|
@ -0,0 +1,19 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Proxy System"
|
||||||
|
metadata.date = "2016-05-07 15:41:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Minecraft (and therefore Forge) are split up between the client and the server, so certain things can only be done on the client. Because some classes only exist on the client, we'll be using Forge's proxy system to access those classes without having to worry about crashes on dedicated servers.
|
||||||
|
|
||||||
|
We'll need to create a pair of classes to act as the proxy. One will contain code common to both sides, and the other will contain client-specific code. Create two new classes, `CommonProxy` and `ClientProxy` (I usually use a `proxy` package to keep this contained from the rest of the code) and have `ClientProxy` extend `CommonProxy`. You'll see specifically how we'll be using this proxy structure when we get to custom items and blocks.
|
||||||
|
|
||||||
|
In order to have Forge load the correct proxy class for the correct side, we'll need to use the `@SidedProxy` annotation. Add this (replacing my package with yours) to your main mod class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SidedProxy(serverSide = "net.shadowfacts.tutorial.proxy.CommonProxy", clientSide = "net.shadowfacts.tutorial.proxy.ClientProxy")
|
||||||
|
public static CommonProxy proxy;
|
||||||
|
```
|
||||||
|
|
||||||
|
At runtime, Forge will detect which side our mod is running on and inject the correct proxy into our `proxy` field using reflection.
|
|
@ -0,0 +1,326 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tile Entities with Inventory GUI"
|
||||||
|
metadata.date = "2017-03-29 18:58:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got the inventory for our tile entity working, let's add a GUI so people can easily see what's inside of it.
|
||||||
|
|
||||||
|
Of course, in Minecraft, GUIs only exist on the client-side. This poses a problem because inventory's can only be interacted with from the server-side. In order to handle this, we use something called a Container which bridges the gap between the client and the server. There are two identical instances of the `Container` subclass that exist on both the client and the server. Whenever a change is made on the server, the change is automatically sent to the client, and vice versa: whenever a change is made on the client, it's automatically sent to the server.
|
||||||
|
|
||||||
|
## Container
|
||||||
|
|
||||||
|
First off, we'll create our container class. We'll create a new class called `ContainerPedestal` in the `block.pedestal` package of our mod that extends Minecraft's `Container` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.inventory.*;
|
||||||
|
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will cause an error because we need to implement the abstract method `canInteractWith` from the `Container` class. This method determines if a given player can open the container. For our pedestal, we don't have any special restrictions so we'll just return true regardless of the player.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canInteractWith(EntityPlayer player) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll override the `transferStackInSlot` method from `Container`. This method handle's when a player tries to quick-transfer a stack by shift-clicking it. The implementation of this method is copied from [my library mod](https://github.com/shadowfacts/ShadowMC/blob/1.11/src/main/java/net/shadowfacts/shadowmc/inventory/ContainerBase.java) which is designed to work with any container regardless of the number of slots. This differs from the vanilla implementations of this method are dependent on the number of slots being a specific number. This method is fairly complicated and not that important so I won't go over super in-detail here. The gist of it is that it tries to add the stack that's been shift-clicked into the opposite inventory leaving anything that can't be transferred in the slot that was shift-clicked.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack transferStackInSlot(EntityPlayer player, int index) {
|
||||||
|
ItemStack itemstack = null;
|
||||||
|
Slot slot = inventorySlots.get(index);
|
||||||
|
|
||||||
|
if (slot != null && slot.getHasStack()) {
|
||||||
|
ItemStack itemstack1 = slot.getStack();
|
||||||
|
itemstack = itemstack1.copy();
|
||||||
|
|
||||||
|
int containerSlots = inventorySlots.size() - player.inventory.mainInventory.length;
|
||||||
|
|
||||||
|
if (index < containerSlots) {
|
||||||
|
if (!this.mergeItemStack(itemstack1, containerSlots, inventorySlots.size(), true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (!this.mergeItemStack(itemstack1, 0, containerSlots, false)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemstack1.stackSize == 0) {
|
||||||
|
slot.putStack(null);
|
||||||
|
} else {
|
||||||
|
slot.onSlotChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemstack1.stackSize == itemstack.stackSize) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.onPickupFromSlot(player, itemstack1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemstack;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll add the most important part of the container, our constructor. In the constructor, we'll add all the slots for the player inventory and the slot for the pedestal's inventory. The container stores a `List` of `Slot` objects, each of which has a position on the GUI to render at and represents one inventory slot (either from the player's inventory or anything else). Minecraft's built in `Slot` class expects an `IInventory` but obviously, our pedestal doesn't use `IInventory`, it uses Forge's `IItemHandler` capability. In order to have containers still work with `IItemHandler`s, Forge provides a `SlotItemHandler` which takes an `IItemHandler` and creates a dummy `IInventory` object and overrides all the necessary methods to use the `IItemHandler`. We'll have a single one of these slots for our pedestal's inventory. We'll also have 36 normal slots which are for the player's inventory. We'll use some for loops add in all 36 of the slots at the correct positions so we don't have to type them all out.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
|
||||||
|
public ContainerPedestal(InventoryPlayer playerInv, final TileEntityPedestal pedestal) {
|
||||||
|
IItemHandler inventory = pedestal.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.NORTH);
|
||||||
|
addSlotToContainer(new SlotItemHandler(inventory, 0, 80, 35) {
|
||||||
|
@Override
|
||||||
|
public void onSlotChanged() {
|
||||||
|
pedestal.markDirty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
for (int j = 0; j < 9; j++) {
|
||||||
|
addSlotToContainer(new Slot(playerInv, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int k = 0; k < 9; k++) {
|
||||||
|
addSlotToContainer(new Slot(playerInv, k, 8 + k * 18, 142));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One important thing to note is how in our `SlotItemHandler` instance, we override the `onSlotChanged` method to call `markDirty` on our pedestal. This makes it so that when the contents of the slot changes, the tile entity will be marked as dirty so Minecraft knows it needs to be saved to disk. If we were using a normal `Slot` instead of a `SlotItemHandler`, this wouldn't be necessary because `IInventory` has a `markDirty` method that the slot can call. However, because we're using Forge's `IItemHandler` which obeys separation of concerns, no equivalent method exists, meaning we need to handle it ourselves.
|
||||||
|
|
||||||
|
## GUI
|
||||||
|
|
||||||
|
Now that we've finished our container class, we need to make the client-side GUI itself. We'll create a new class called `GuiPedestal` that extends `GuiContainer` in our `block.pedestal` package.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.GuiContainer;
|
||||||
|
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll have a constructor that takes a `Container` and a `InventoryPlayer`. It will pass the container to the super-constructor so that `GuiContainer` can render our slots and handle interaction with them. It will also save the `InventoryPlayer` to a field for later.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ..
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
private InventoryPlayer playerInv;
|
||||||
|
|
||||||
|
public GuiPedestal(Container container, InventoryPlayer playerInv) {
|
||||||
|
super(container);
|
||||||
|
this.playerInv = playerInv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight%}
|
||||||
|
|
||||||
|
Before we can move on to the next method, we'll add a `private static final` to store the `ResourceLocation` for the background texture of the GUI.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
private static final ResourceLocation BG_TEXTURE = new ResourceLocation(TutorialMod.modId, "textures/gui/pedestal.png");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for the pedestal GUI [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/gui/pedestal.png). You'll want to save it to `src/main/resources/assets/tutorial/textures/gui/pedestal.png`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
In our `GuiPedestal` class, we'll need to override two methods: `drawGuiContainerBackgroundLayer` and `drawGuiContainerForegroundLayer`. These two methods respectively handle rendering the background (the stuff that renders _behind_ the slots) and rendering the foreground (the stuff that renders _in front of_ the background and the slots).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Firstly, the background method:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) {
|
||||||
|
GlStateManager.color(1, 1, 1, 1);
|
||||||
|
mc.getTextureManager().bindTexture(BG_TEXTURE);
|
||||||
|
int x = (width - xSize) / 2;
|
||||||
|
int y = (height - ySize) / 2;
|
||||||
|
drawTexturedModalRect(x, y, 0, 0, xSize, ySize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In our `drawGuiContainerBackgroundLayer` method, we:
|
||||||
|
|
||||||
|
1. Call `GlStateManager.color(1, 1, 1, 1)`. This resets the GL color to solid white, instead of potentially something else. If we don't reset it, and the color isn't already white, our texture would be tinted with that color.
|
||||||
|
2. Call `bindTexture(BG_TEXTURE)`. This binds the background texture that we've specified in our `BG_TEXTURE` field to Minecraft's rendering engine, so that when we render a rectangle with a texture on it, the correct texture is used.
|
||||||
|
3. Calculate the X and Y positions to draw our texture at. We want our texture to be centered on screen, so we take half the width and height of the screen and subtract half the x-size and y-size of our GUI from it, giving us the position of the upper left hand corner of the GUI, which is the position the texture should be drawn at.
|
||||||
|
4. Call `drawTexturedModalRect`. This actually draws the texture. We pass in:
|
||||||
|
1. The `x` and `y` positions.
|
||||||
|
2. The point (0, 0) for the UV position. This is where on the texture rendering should start from. Because in our image, the GUI is at the top left corner, we use (0, 0).
|
||||||
|
3. The x-size and the y-size for the dimensions of the drawn texture.
|
||||||
|
|
||||||
|
Lastly for our GUI, we override the `drawGuiContainerForegroundLayer` method:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class GuiPedestal {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) {
|
||||||
|
String name = I18n.format(ModBlocks.pedestal.getUnlocalizedName() + ".name");
|
||||||
|
fontRendererObj.drawString(name, xSize / 2 - fontRendererObj.getStringWidth(name) / 2, 6, 0x404040);
|
||||||
|
fontRendererObj.drawString(playerInv.getDisplayName().getUnformattedText(), 8, ySize - 94, 0x404040);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this method, we:
|
||||||
|
|
||||||
|
1. Call `I18n.format` with our pedestal block's unlocalized name. This converts the unlocalized name of our block (`tile.pedestal.name`) into the correct name for the current locale as specified in our localization files. For English, this will be `Pedestal`.
|
||||||
|
2. Draw the localized name of our block on the screen. We draw it at the top center of our GUI, so we subtract half of width of our localized name (as calculated by `getStringWidth`) from half of the x-size of the GUI, giving use the X position that will result in it being centered in the GUI. We also pass 6 as the Y coordinate, so 6 pixels from the top of the GUI, and `0x404040` as the color, in hexadecimal, for our string.
|
||||||
|
3. Draw the localized name of the player's inventory on the screen. We call `playerInv.getDisplayName()` which returns an `ITextComponent` and call `getUnformattedText()` on it to get the string to render. We draw it at X position 8, just offset from the left side of our GUI, and at the Y position which is 94 pixels (the height of the player's inventory in our GUI) above the bottom of our GUI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## GUI Handler
|
||||||
|
Now that we've got our container and GUI classes finished, we need to add a GUI handler. This class will have methods which are called by Forge that will be responsible for creating the correct instances of our GUI and container classes from some pieces of information.
|
||||||
|
|
||||||
|
We'll create a class called `ModGuiHandler` that implements the `IGuiHandler` interface in our root mod package.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial;
|
||||||
|
|
||||||
|
import net.minecraftforge.fml.common.network.IGuiHandler;
|
||||||
|
|
||||||
|
public class ModGuiHandler implements IGuiHandler {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First off, we'll add a constant field to our GUI handler for the Pedestal's GUI ID. Forge uses integer IDs to differentiate between which GUI should be opened, and since this is our first GUI, its ID will be `0`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModGuiHandler implements IGuiHandler {
|
||||||
|
public static final int PEDESTAL = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll implement the `getServerGuiElement` method. This method returns the appropriate instance (or `null`, if there is none) for the given ID, player, world, and position.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModGuiHandler implements IGuiHandler {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Container getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
|
||||||
|
switch (ID) {
|
||||||
|
case PEDESTAL:
|
||||||
|
return new ContainerPedestal(player.inventory, (TileEntityPedestal)world.getTileEntity(new BlockPos(x, y, z)));
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** We've changed the return type of our `getServerGuiElement` method to `Container` from `Object`. Forge uses `Object` because client-only classes aren't present on servers, so there can't be any references to them in the signatures of methods that will exist on both sides. Forge also uses this logic for the container method, but because `Container` is present on both sides, we can change the return type to `Container` from `Object` without issue.
|
||||||
|
|
||||||
|
In this method, we switch over the GUI ID, and if it's the pedestal's ID, return a new `ContainerPedestal` instance using the player's inventory, and the `TileEntityPedestal` that's in the world. Otherwise, if the ID doesn't match any of the one's we've added (this should never happen, but it's necessary just to satisfy the Java compiler), we return `null`.
|
||||||
|
|
||||||
|
Nextly, we add the `getClientGuiElement` method which returns the appropriate `GuiScreen` instance given the same data as the `getServerGuiElement`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
|
||||||
|
switch (ID) {
|
||||||
|
case PEDESTAL:
|
||||||
|
return new GuiPedestal(getServerGuiElement(ID, player, world, x, y, z), player.inventory);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly to the `getServerGuiElement` method, we switch on the ID, and if it's the pedestal's, we return a new instance of `GuiPedestal` with a new container instance and the player's inventory.
|
||||||
|
|
||||||
|
Finally, we need to register our GUI handler.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
NetworkRegistry.INSTANCE.registerGuiHandler(this, new ModGuiHandler());
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This tells Forge that our GUI handler instance corresponds to our mod instance, so it knows which GUI handler to use when we actually open our GUI.
|
||||||
|
|
||||||
|
## Updating the Block
|
||||||
|
Lastly, in order to open the GUI, we need to modify our Block's `onBlockActivated` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
// ...
|
||||||
|
if (!player.isSneaking()) {
|
||||||
|
// ...
|
||||||
|
} else {
|
||||||
|
player.openGui(TutorialMod.instance, ModGuiHandler.PEDESTAL, world, pos.getX(), pos.getY(), pos.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll remove the code that prints messages to chat and replace it with a call to `player.openGui` with our mod instance, the Pedestal's GUI ID, the world, and the positions which get passed into our GUI handler to open the GUI.
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now that we've finished, we can launch Minecraft, and once we shift right-click on the pedestal block, we can see and interact with our GUI:
|
||||||
|
|
||||||
|
![Pedestal GUI](http://i.imgur.com/0ajo2b2.png)
|
|
@ -0,0 +1,237 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tile Entities with Inventory"
|
||||||
|
metadata.date = "2016-11-27 13:07:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've learned the basics of making tile entities, let's make a more complicated one that has an inventory.
|
||||||
|
|
||||||
|
**Note:** If you haven't already completed the [tile entities tutorial](/tutorials/forge-modding-1102/tile-entities/), you'll want to do that so you'll have the foundations that this tutorial builds on.
|
||||||
|
|
||||||
|
## The Block
|
||||||
|
|
||||||
|
Firstly, we'll move the `BlockPedestal` from the `block` package to the `block.pedestal` package. Next, we'll change `BlockPedestal` so it extends `BlockTileEntity` instead of `BlockBase`. We'll also add a generic type parameter of `TileEntityPedestal`, which will be the tile entity class for our pedestal. Next, we'll need to implement the abstract methods provided by `BlockTileEntity` (`getTileEntityClass` and `createTileEntity`):
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<TileEntityPedestal> getTileEntityClass() {
|
||||||
|
return TileEntityPedestal.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public TileEntityPedestal createTileEntity(World world, IBlockState state) {
|
||||||
|
return new TileEntityPedestal();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From the `getTileEntityClass` method, we'll return `TileEntityPedestal.class` (this will cause errors because we haven't created the tile entity class yet) and from the `createTileEntity` method, we'll return a new instance of the `TileEntityPedestal` class.
|
||||||
|
|
||||||
|
Next, we'll add the `onBlockActivated` method which will handle our block being right-clicked. The logic for this method will be something like this:
|
||||||
|
|
||||||
|
1. Check that we're running on the server (see the [Sides section](/tutorials/forge-modding-1102/tile-entities/#sides) of the previous tutorial).
|
||||||
|
1. Retrieve the `TileEntity` and the `IItemHandler` instance.
|
||||||
|
2. If the player is sneaking:
|
||||||
|
1. If the player's hand is empty:
|
||||||
|
1. Take what's in the pedestal's `IItemHandler` and put it in the player's hand.
|
||||||
|
2. Otherwise:
|
||||||
|
1. Take what's in the player's hand and attempt to insert it into the pedestal
|
||||||
|
3. Mark the tile entity as dirty so Minecraft knows it needs to be saved to disk.
|
||||||
|
3. Otherwise:
|
||||||
|
1. Retrieve the `ItemStack` currently in the pedestal
|
||||||
|
2. If there is a stack (i.e. `stack != null`)
|
||||||
|
1. Send a chat message to the player with count and name of the item.
|
||||||
|
3. Otherwise
|
||||||
|
1. Send a chat message to the player telling them that the pedestal's empty
|
||||||
|
2. Return `true`
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, @Nullable ItemStack heldItem, EnumFacing side, float hitX, float hitY, float hitZ) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
TileEntityPedestal tile = getTileEntity(world, pos);
|
||||||
|
IItemHandler itemHandler = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, side);
|
||||||
|
if (!player.isSneaking()) {
|
||||||
|
if (heldItem == null) {
|
||||||
|
player.setHeldItem(hand, itemHandler.extractItem(0, 64, false));
|
||||||
|
} else {
|
||||||
|
player.setHeldItem(hand, itemHandler.insertItem(0, heldItem, false));
|
||||||
|
}
|
||||||
|
tile.markDirty();
|
||||||
|
} else {
|
||||||
|
ItemStack stack = itemHandler.getStackInSlot(0);
|
||||||
|
if (stack != null) {
|
||||||
|
String localized = TutorialMod.proxy.localize(stack.getUnlocalizedName() + ".name");
|
||||||
|
player.addChatMessage(new TextComponentString(stack.stackSize + "x " + localized));
|
||||||
|
} else {
|
||||||
|
player.addChatMessage(new TextComponentString("Empty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `IItemHandler` and capability stuff might look a bit confusing, but that's okay, it will be explained in more detail later on. For now, suffice it to say that the `IItemHandler` is the object that stores the pedestal's inventory.
|
||||||
|
|
||||||
|
Before we can continue, we'll also need to add a new method to our proxy class. This method will take an unlocalized name (e.g. `item.diamond.name`) and translate it into the correct version (e.g. `Diamond`). This needs to be a method in our proxy class because there are two different ways of localizing things depending if you're on the client or the server. If you're on the server, you need to use `net.minecraft.util.text.translation.I18n` whereas if you're on the client, you need to use `net.minecraft.client.resources.I18n`. In our `CommonProxy` class, we'll add the server-side version of this:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
import net.minecraft.util.text.translation.I18n;
|
||||||
|
|
||||||
|
public class CommonProxy {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public String localize(String unlocalized, Object... args) {
|
||||||
|
return I18n.translateToLocalFormatted(unlocalized, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the `ClientProxy`, we'll add the client-side version of this:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
import net.minecraft.client.resources.I18n;
|
||||||
|
|
||||||
|
public class ClientProxy extends CommonProxy {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String localize(String unlocalized, Object... args) {
|
||||||
|
return I18n.format(unlocalized, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The very last thing we'll need to add to our block class is the `breakBlock` method. This method is called when our block is destroyed in the world, and we'll use it to drop the contents of the pedestal's inventory.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void breakBlock(World world, BlockPos pos, IBlockState state) {
|
||||||
|
TileEntityPedestal tile = getTileEntity(world, pos);
|
||||||
|
IItemHandler itemHandler = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.NORTH);
|
||||||
|
ItemStack stack = itemHandler.getStackInSlot(0);
|
||||||
|
if (stack != null) {
|
||||||
|
EntityItem item = new EntityItem(world, pos.getX(), pos.getY(), pos.getZ(), stack);
|
||||||
|
world.spawnEntityInWorld(item);
|
||||||
|
}
|
||||||
|
super.breakBlock(world, pos, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the break block method, we'll:
|
||||||
|
|
||||||
|
1. Get the tile entity instance, the `IItemHandler` instance, and the `ItemStack` stored in the inventory.
|
||||||
|
2. If there is a stack (i.e. `stack != null`):
|
||||||
|
1. Create a new `EntityItem` instance at the correct position with the stack
|
||||||
|
2. Spawn the entity in the world so the item is dropped
|
||||||
|
3. Call the `super.breakBlock` method to remove our block and tile entity from the world.
|
||||||
|
|
||||||
|
## The Tile Entity
|
||||||
|
|
||||||
|
Like in the previous tutorial, the tile entity class itself will be fairly simple. This is possible because of Forge's `IItemHandler` capability and its `ItemStackHandler` class which handles all the logic for storing items, reading/writing them to/from NBT, and inserting/extracting items.
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
Forge provides a simple Entity Component System called capabilities. Capabilities allow mod developers to easily add/use functionality without having to implement lots of interfaces or perform lots of `instanceof` checks and casts. In this tutorial we'll use the Forge-provided `IItemHandler` capability which is a replacement for Vanilla's `IInventory` and `ISidedInventory`. We'll be using the `ItemStackHandler` implementation of the `IItemHandler` interface which is provided by Forge. By overriding the `hasCapability` and `getCapability` methods of our tile entity, we can "register" the capability object and make it accessible to everyone else.
|
||||||
|
|
||||||
|
1. `insertItem`: This method takes 3 parameters: `int slot`, `ItemStack stack`, and `boolean simulate` and returns an `ItemStack`.
|
||||||
|
1. `int slot`: The index of the slot in the inventory that we want to insert into.
|
||||||
|
2. `ItemStack stack`: The stack that we are attempting to insert.
|
||||||
|
3. `boolean simulate`: If true, no modification of the `IItemHandler`'s internal inventory will be performed. This is useful if you want to test if an interaction can be performed.
|
||||||
|
4. `ItemStack` return: The remainder of the stack that could not be inserted. If the stack was fully inserted, this will be `null`.
|
||||||
|
2. `extractItem`: This method takes 3 parameters: `int slot`, `int amount`, `boolean simulate` and returns an `ItemStack`.
|
||||||
|
1. `int slot`: The index of the slot in the inventory that we want to extract from.
|
||||||
|
2. `int amount`: The amount of items we want to extract from the slot.
|
||||||
|
3. `boolean simulate`: If true, no modification of the `IItemHandler`'s internal inventory will be performed. This is useful if you want to test if an interaction can be performed.
|
||||||
|
4. `ItemStack` return: The stack that was extracted from the inventory.
|
||||||
|
|
||||||
|
**Note:** If you want to know more about capabilities, you can checkout the [official Forge documentation](http://mcforge.readthedocs.io/en/latest/datastorage/capabilities/) on the subject.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.NBTTagCompound;
|
||||||
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
import net.minecraft.util.EnumFacing;
|
||||||
|
import net.minecraftforge.common.capabilities.Capability;
|
||||||
|
import net.minecraftforge.items.CapabilityItemHandler;
|
||||||
|
import net.minecraftforge.items.ItemStackHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
|
||||||
|
private ItemStackHandler inventory = new ItemStackHandler(1);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||||
|
compound.setTag("inventory", inventory.serializeNBT());
|
||||||
|
return super.writeToNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFromNBT(NBTTagCompound compound) {
|
||||||
|
inventory.deserializeNBT(compound.getCompoundTag("inventory"));
|
||||||
|
super.readFromNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasCapability(Capability<?> capability, @Nullable EnumFacing facing) {
|
||||||
|
return capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY || super.hasCapability(capability, facing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <T> T getCapability(Capability<T> capability, @Nullable EnumFacing facing) {
|
||||||
|
return capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY ? (T)inventory : super.getCapability(capability, facing);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In our tile entity, we'll have a `private ItemStackHandler inventory` field which is initialized to a `new ItemStackHandler(1)`. The first parameter of the `ItemStackHandler` constructor is the number of slots it should have. In our case, this is 1 because the pedestal can only hold 1 stack at a time.
|
||||||
|
|
||||||
|
`ItemStackHandler` also provides `serializeNBT` and `deserializeNBT` methods making it very easy to save our inventory. In the `writeToNBT` method, we'll call `inventory.serializeNBT()` to create an `NBTTagCompound` that represents the inventory and set that to the key `inventory` on the root compound. Similarly, in the `readFromNBT` method, we'll retrieve the tag compound that has the key `inventory` and pass it to `inventory.deserializeNBT` so that the items that were saved to NBT are loaded back into our `ItemStackHandler` object.
|
||||||
|
|
||||||
|
Lastly, we'll override the `hasCapability` and `getCapability` methods. In `hasCapability` we'll return if the capability being tested is the `IItemHandler` capability instance, or if it's provided by the super method*. Likewise, in the `getCapability` method, we'll check if the capability being requested is the `IItemHandler` capability and if so, return our `inventory`, and otherwise, delegate to the super method\*.
|
||||||
|
|
||||||
|
*: We delegate back to the super method because Forge provides an `AttachCapabilitiesEvent` which allows other mods to add capabilities to tile entities and other objects that they don't own.
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now we can launch Minecraft and see how our pedestal can now store an item in its inventory:
|
||||||
|
|
||||||
|
![Pedestal Gif](https://zippy.gfycat.com/DescriptiveWhoppingBangeltiger.gif)
|
|
@ -0,0 +1,266 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tile Entities"
|
||||||
|
metadata.date = "2016-11-27 11:11:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
In Minecraft, the `Block` class is used to represent a _type_ of block, not a single block in the world. The `Block` instance has the properties for every single instance of your block that exists. If we want to have data that is unique to an instance of a block in the world, we need to use a `TileEntity`.
|
||||||
|
|
||||||
|
A common myth that exists in the world of modding is that tile entities are bad, especially for performance. **This is not true.** Tile entities can be bad for performance if they're implemented poorly, just like anything else, but they are not bad just by virtue of existing.
|
||||||
|
|
||||||
|
There are two varieties of tile entities: _ticking_ and _non-ticking_. Ticking tile entities, as the name implies, are updated (or ticked) every single game tick (usually 20 times per second). Tick tile entities are the more performance intensive kind because they're updated so frequently and as such, need to be written carefully. Non-ticking tile entities on the other hand don't tick at all, they exist simply for storing data. In this tutorial we'll be making a non-ticking tile entity. Ticking tile entities we'll get to later.
|
||||||
|
|
||||||
|
## Helper Stuff
|
||||||
|
|
||||||
|
Before we create the tile entity, we'll add some more code to our mod that will make it easier to add more tile entities in the future.
|
||||||
|
|
||||||
|
Firstly, we'll create a `BlockTileEntity` class in our `block` package.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.block.state.IBlockState;
|
||||||
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.IBlockAccess;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public abstract class BlockTileEntity<TE extends TileEntity> extends BlockBase {
|
||||||
|
|
||||||
|
public BlockTileEntity(Material material, String name) {
|
||||||
|
super(material, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Class<TE> getTileEntityClass();
|
||||||
|
|
||||||
|
public TE getTileEntity(IBlockAccess world, BlockPos pos) {
|
||||||
|
return (TE)world.getTileEntity(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTileEntity(IBlockState state) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public abstract TE createTileEntity(World world, IBlockState state);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `BockTileEntity` class will provide a couple of things:
|
||||||
|
|
||||||
|
1. It will extend `BlockBase` so we still have access to all our existing helpers.
|
||||||
|
2. It will have a generic parameter `TE` which will be the type of our tile entity class. This will be used to create a simple helper method to reduce the number of casts necessary to obtain the instance of our tile entity for a specific position in the world and to ensure that the `TileEntity` we create is of the correct type for our block instance.
|
||||||
|
3. It will override the `hasTileEntity(IBlockState)` method from Minecraft's `Block` class to return `true`. This will tell Minecraft that our block has a tile entity associated with it that needs to be created.
|
||||||
|
4. It will have two abstract methods:
|
||||||
|
1. `getTileEntityClass`: From here, we'll return the `Class` that our tile entity is so it can automatically be registered when our block is registered.
|
||||||
|
2. `createTileEntity`: This is a more specific version of the `Block` class' `createTileEntity`. This method will be called by Minecraft whenever it needs to create a new instance of our tile entity, like when our block has been placed.
|
||||||
|
|
||||||
|
Nextly, we'll add another check to the `register` method in our `ModBlocks` class. This will check if the block being registered is an instance of our `BlockTileEntity` class and if so, register the tile entity class that's associated with the block to the name of the block. (This tile entity registration is necessary so that Minecraft knows which class to create for a tile entity that's been saved to and reloaded from the disk.)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block, ItemBlock itemBlock) {
|
||||||
|
GameRegistry.register(block);
|
||||||
|
// ...
|
||||||
|
|
||||||
|
if (block instanceof BlockTileEntity) {
|
||||||
|
GameRegistry.registerTileEntity(((BlockTileEntity<?>)block).getTileEntityClass(), block.getRegistryName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Block
|
||||||
|
|
||||||
|
Now that we've got all the necessary helpers out of the way it's time to create the block.
|
||||||
|
|
||||||
|
We'll create a new class called `BlockCounter` in the `block.counter` package of our mod. This class will be block class that extends `BlockTileEntity`. (This class will have some errors because we haven't created the tile entity class itself yet.)
|
||||||
|
|
||||||
|
```java
|
||||||
|
|
||||||
|
package net.shadowfacts.tutorial.block.counter;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.block.state.IBlockState;
|
||||||
|
import net.minecraft.entity.player.EntityPlayer;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.util.EnumFacing;
|
||||||
|
import net.minecraft.util.EnumHand;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.util.text.TextComponentString;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import net.shadowfacts.tutorial.block.BlockTileEntity;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class BlockCounter extends BlockTileEntity<TileEntityCounter> {
|
||||||
|
|
||||||
|
public BlockCounter() {
|
||||||
|
super(Material.ROCK, "counter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, @Nullable ItemStack heldItem, EnumFacing side, float hitX, float hitY, float hitZ) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
TileEntityCounter tile = getTileEntity(world, pos);
|
||||||
|
if (side == EnumFacing.DOWN) {
|
||||||
|
tile.decrementCount();
|
||||||
|
} else if (side == EnumFacing.UP) {
|
||||||
|
tile.incrementCount();
|
||||||
|
}
|
||||||
|
player.addChatMessage(new TextComponentString("Count: " + tile.getCount()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<TileEntityCounter> getTileEntityClass() {
|
||||||
|
return TileEntityCounter.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public TileEntityCounter createTileEntity(World world, IBlockState state) {
|
||||||
|
return new TileEntityCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our block class will extend `BlockTileEntity` and have a generic parameter of `TileEntityCounter` because that's the type of tile entity that belongs to this block.
|
||||||
|
|
||||||
|
In the constructor, we'll simply call the super constructor with the material `ROCK` and the name `"counter"`.
|
||||||
|
|
||||||
|
In the `getTileEntityClass` method, we'll return `TileEntityCounter.class` (this will cause an error, but don't worry, we haven't created this class yet). This will allow our `ModBlocks` class to automatically register our `TileEntityCounter.class` to the name `tutorial:counter`.
|
||||||
|
|
||||||
|
In the `createTileEntity` class, we'll simply return a new instance of our `TileEntityCounter ` class.
|
||||||
|
|
||||||
|
Lastly, and most importantly, in the `onBlockActivated` method, which is called when our block is right-clicked, we'll do a number of things:
|
||||||
|
|
||||||
|
1. Check that we're operating on the server[*](#sides).
|
||||||
|
1. Retrieve the `TileEntityCounter` instance.
|
||||||
|
2. If the player hit the bottom side:
|
||||||
|
1. Decrement the counter.
|
||||||
|
3. Or if the player hit the top side:
|
||||||
|
1. Increment the counter.
|
||||||
|
4. Send a chat message to the player with the current value of the counter.
|
||||||
|
2. Return true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2 id="sides">Sides</h2>
|
||||||
|
|
||||||
|
As I mentioned above, before we modify the counter, we check that we're on the server. We need to do this because the Minecraft client and the server are completely separated and some methods are called on both.
|
||||||
|
|
||||||
|
In a multiplayer scenario, there are multiple clients connect to one server. In this case, the distinction between client and server is fairly obvious, but in a single player scenario, things get more complicated. In a multiplayer scenario, the server that everybody's connecting to is referred to as the **physical server** and all of the individual clients are the **physical clients**.
|
||||||
|
|
||||||
|
In a single player world, the client and the server are still decoupled, even though they are running on the same computer (and even in the same JVM, just on different threads). In singleplayer, the client connects to a local, private server that functions very similarly to a physical server. In this case, the server thread is referred to as the **logical server** and the client thread as the **logical client** because both logical sides are running on the same physical side.
|
||||||
|
|
||||||
|
The `World.isRemote` field is used to check which logical side we're operating on (be it logical or physical). The field is `true` for the physical client in a multiplayer scenario and for the logical client in a single-player scenario. The reverse is also true. The field is `false` for the physical server in a multiplayer scenario and for the logical server in the single-player scenario. So by checking `!world.isRemote`, we ensure that the code inside the `if` statement will only be run on the server (be it logical or physical).
|
||||||
|
|
||||||
|
If you want to know more about sides in Minecraft and how they work, you can see [here](http://mcforge.readthedocs.io/en/latest/concepts/sides/) for the official Forge documentation.
|
||||||
|
|
||||||
|
## The `TileEntity`
|
||||||
|
|
||||||
|
Now that our block is finished, we can finally create the tile entity itself.
|
||||||
|
|
||||||
|
We'll create a new class called `TileEntityCounter` which will also reside in the `block.counter` package of our mod (this is my preferred package structure, however, many people also prefer to have all the tile entity classes reside in a separate `tileentity` package and the block classes reside in the `block` package).
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.counter;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.NBTTagCompound;
|
||||||
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
|
||||||
|
public class TileEntityCounter extends TileEntity {
|
||||||
|
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||||
|
compound.setInteger("count", count);
|
||||||
|
return super.writeToNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFromNBT(NBTTagCompound compound) {
|
||||||
|
count = compound.getInteger("count");
|
||||||
|
super.readFromNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementCount() {
|
||||||
|
count++;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decrementCount() {
|
||||||
|
count--;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `TileEntityCounter` class is fairly simple. It will:
|
||||||
|
|
||||||
|
- Extend Minecraft's `TileEntity` class so Minecraft knows what to do with it.
|
||||||
|
- Have a private `int count` field which will store the value of the counter.
|
||||||
|
- Override the `writeToNBT` and `readFromNBT` methods so Minecraft is able to properly save and load it from the disk.
|
||||||
|
- Provide `getCount`, `incrementCount`, and `decrementCount` methods for accessing and modifying the value of the field.
|
||||||
|
|
||||||
|
Additionally, in the `incrementCount` and `decrementCount` methods, we call the `markDirty` method from the Vanilla `TilEntity` class. This method call tells Minecraft that our TE has changed since it was last saved to disk and therefore must be re-saved.
|
||||||
|
|
||||||
|
### The NBT (Named Binary Tag) Format
|
||||||
|
|
||||||
|
NBT is a format for storing all types of data into a key/value tree structure that can easily be serialized to bytes and saved to the disk. You can read more about the internal structure of the NBT format [here](http://wiki.vg/NBT). You can look at the `NBTTagCompound` class in Minecraft to see all the types of things that can be stored. Vanilla code is also a good example of how to store more complex things in NBT.
|
||||||
|
|
||||||
|
In this case, we'll store our `count` integer field with the `count` key in the `NBTTagCompound` in the `writeToNBT` method and read it back from the tag compound in the `readFromNBT` method.
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
Lastly, we'll need to add our counter to our `ModBlocks` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
public static BlockCounter counter;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
counter = register(new BlockCounter());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now that we've got everything done, we can run Minecraft, grab one of our counters from our creative tab, place it, and see how the counter changes when the top and bottom of the block are right-clicked.
|
||||||
|
|
||||||
|
![Click on the top](http://i.imgur.com/zD1x2m0.png)
|
||||||
|
|
||||||
|
![Click on the bottom](http://i.imgur.com/UCqVJSI.png)
|
|
@ -0,0 +1,412 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tools"
|
||||||
|
metadata.date = "2016-08-14 15:04:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's make some copper tools!
|
||||||
|
|
||||||
|
First we'll need to create a tool material for our new tools to use. We'll use Forge's `EnumHelper` class to add a value to the Minecraft `Item.ToolMaterial` enum.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static final Item.ToolMaterial copperToolMaterial = EnumHelper.addToolMaterial("COPPER", 2, 500, 6, 2, 14);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each tool is going to be quite similar, so feel free to skip ahead to the one you want.
|
||||||
|
|
||||||
|
- [Sword](#sword)
|
||||||
|
- [Pickaxe](#pickaxe)
|
||||||
|
- [Axe](#axe)
|
||||||
|
- [Shovel](#shovel)
|
||||||
|
- [Hoe](#hoe)
|
||||||
|
|
||||||
|
## Sword
|
||||||
|
First we'll create an `ItemSword` class in the `item.tool` package inside our mod package. This class will extend the vanilla `ItemSword` class and implement our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In the constructor, we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the name for use in item model registration
|
||||||
|
|
||||||
|
We'll also override `registerItemModel` and use the stored `name` to register our item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemSword extends net.minecraft.item.ItemSword implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemSword(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper sword to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemSword copperSword;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperSword = register(new ItemSword(TutorialMod.copperToolMaterial, "copperSword"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also create our JSON item model at `assets/tutorial/models/item/copperSword.json`. Unlike our other item models, the parent for the model will be `item/handheld` instead of `item/generated`. `item/handheld` provides the transformations used by handheld items, such as tools.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperSword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also need the texture, which you can download [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperSword.png).
|
||||||
|
|
||||||
|
And lastly, we'll add a localization entry for the sword.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copperSword.name=Copper Sword
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Sword](http://i.imgur.com/ye5yMy4.png)
|
||||||
|
|
||||||
|
## Pickaxe
|
||||||
|
Let's create an `ItemPickaxe` class in the `item.tool` package of our mod. This class will extend the vanilla `ItemPickaxe` and implement our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In our `ItemPickaxe` constructor we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material
|
||||||
|
- Set the unlocalized name and registry names
|
||||||
|
- Store the name for use in the item model registration
|
||||||
|
|
||||||
|
We'll also override `registerItemModel` and use the stored `name` field to register our item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemPickaxe extends net.minecraft.item.ItemPickaxe implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemPickaxe(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper pickaxe to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemPickaxe copperPickaxe;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperPickaxe = register(new ItemPickaxe(TutorialMod.copperToolMaterial, "copperPickaxe"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll create a JSON model for our item at `assets/tutorial/models/item/copperPickaxe.json`. This model will have a parent of `item/handheld` instead of `item/generated` so it inherits the transformations for handheld models.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperPickaxe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for the copper pickaxe [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperPickaxe.png).
|
||||||
|
|
||||||
|
Lastly, we'll need a localization entry for the pick.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copperPickaxe.name=Copper Pickaxe
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Pickaxe](http://i.imgur.com/FsbvVur.png)
|
||||||
|
|
||||||
|
## Axe
|
||||||
|
First off, we'll need an `ItemAxe` class that extends the vanilla `ItemAxe` class and implements our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
If you look at the vanilla `ItemAxe` class, you'll notice that it has two constructors. One of them takes only a `ToolMaterial` whereas the other takes a `ToolMaterial` and two `float`s. Only vanilla `ToolMaterial`s will work with the `ToolMaterial` only constructor, any modded materials will cause an `ArrayIndexOutOfBoundsException` because of the hardcoded values in the `float` arrays in the `ItemAxe` class. Forge provides the secondary constructor that accepts the two `float`s as well, allowing modders to add axes with their own tool materials.
|
||||||
|
|
||||||
|
In the `ItemPickaxe` constructor, we will:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material and the damage and attack speeds used by the vanilla iron axe.
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the name for use in the item model registration
|
||||||
|
|
||||||
|
Additionally, we'll override `registerItemModel` and use the stored `name` to register our model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemAxe extends net.minecraft.item.ItemAxe implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemAxe(ToolMaterial material, String name) {
|
||||||
|
super(material, 8f, -3.1f);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper axe to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemAxe copperAxe;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperAxe = register(new ItemAxe(TutorialMod.copperToolMaterial, "copperAxe"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, we'll need a JSON item model. We'll create it at `assets/tutorials/models/item/copperAxe.json`.
|
||||||
|
|
||||||
|
Our model will have a parent of `item/handheld` instead of `item/generated` so it has the same transformations used by other hand-held items.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperAxe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for the copper axe [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperAxe.png).
|
||||||
|
|
||||||
|
Lastly, we'll need a localization entry for our axe.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copperAxe.name=Copper Axe
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Axe](http://i.imgur.com/5E3vjTo.png)
|
||||||
|
|
||||||
|
## Shovel
|
||||||
|
Firstly we'll create an `ItemShovel` class that extends the vanilla `ItemSpade` class and implements our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In the `ItemShovel` constructor, we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material used
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the name to be used for item model registration
|
||||||
|
|
||||||
|
We'll also need to implement `registerItemModel` and register an item model for our shovel.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemSpade;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemShovel extends ItemSpade implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemShovel(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper shovel to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemShovel copperShovel;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperShovel = register(new ItemShovel(TutorialMod.copperToolMaterial, "copperShovel"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll create a JSON item model for our shovel at `assets/tutorial/models/item/copperShovel.json`. This model will have a parent of `item/handheld`, unlike our previous item models, so it inherits the transformations used by handheld items.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperShovel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for our shovel [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperShovel.png).
|
||||||
|
|
||||||
|
We'll also need a localization entry for our shovel.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copperShovel.name=Copper Shovel
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Shovel](http://i.imgur.com/l1VMi6L.png)
|
||||||
|
|
||||||
|
## Hoe
|
||||||
|
Let's create an `ItemHoe` class that extends the vanilla `ItemHoe` class and implements our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In the `ItemHoe` constructor, we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the item name to be used for item model registration
|
||||||
|
|
||||||
|
We'll also need to implement `registerItemModel` and register an item model for our hoe.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemHoe extends net.minecraft.item.ItemHoe implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemHoe(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our hoe to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemHoe copperHoe;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperHoe = register(new ItemSword(TutorialMod.copperToolMaterial, "copperHoe"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll create a JSON item model for our hoe at `assets/tutorial/models/item/copperHoe.json`. This model will have a parent of `item/handheld` instead of `item/generated` so it inherits the transformations used by vanilla handheld items.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copperHoe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper hoe texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.10.2/src/main/resources/assets/tutorial/textures/items/copperHoe.png).
|
||||||
|
|
||||||
|
Lastly, we'll need a localization entry for our hoe.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copperHoe.name=Copper Hoe
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Hoe](http://i.imgur.com/8PZ3MdD.png)
|
|
@ -0,0 +1,50 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Setting up the Development Environment"
|
||||||
|
metadata.date = "2016-05-06 11:16:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Java
|
||||||
|
This series does not cover learning Java or installing the JDK. You should have the Java 8 JDK installed already.
|
||||||
|
|
||||||
|
### IntelliJ IDEA
|
||||||
|
I will be using [IntelliJ IDEA](https://jetbrains.com/idea/) throughout this series as it is my IDE of choice. You can download the free community version of IDEA [here](https://www.jetbrains.com/idea/). It is possible to use [Eclipse](https://www.eclipse.org/) if you prefer.
|
||||||
|
|
||||||
|
### Forge MDK
|
||||||
|
From the [Forge files site](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.10.2.html), download the latest MDK for 1.10.2. (Click the button with the floppy disk icon labeled `MDK` on the left.) After download, unzip the MDK to a new folder wherever you like. After unzipping the MDK, we can delete a number of extraneous files that are part of the MDK. You can delete every file in the folder that's not one of these:
|
||||||
|
|
||||||
|
- `src/`
|
||||||
|
- `build.gradle`
|
||||||
|
- `gradle/`
|
||||||
|
- `gradlew`
|
||||||
|
- `gradlew.bat`
|
||||||
|
|
||||||
|
### Gradle
|
||||||
|
Before we setup Forge and IDEA, we need to configure Gradle (the build system Forge mods use) to have more RAM available, otherwise we will not be able to decompile and deobfuscate Minecraft. Open the file at `~/.gradle/gradle.properties` (where `~` is your user directory) and create it if it does not exist. Add this to the file to instruct Gradle to use at most 3 gigabytes of memory:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
org.gradle.jvmargs=-Xmx3G
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll need to make a couple of additions to the `build.gradle` file that is part of the Forge MDK. This will configure IDEA and Gradle to use Java 8 to compile our project, allowing us to use the [shiny new Java 8 features](http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html).
|
||||||
|
|
||||||
|
```properties
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forge
|
||||||
|
Now, to setup Forge and create the IDEA configurations we will need, run this command. (Replace `idea` with `eclipse` if you are using Eclipse and remove the leading `./` if you are using Windows)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew setupDecompWorkspace idea
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This may take a while to run, depending on the speed of your computer.
|
||||||
|
|
||||||
|
Now, if everything ran successfully, you should have a file that has the `.ipr` extension in your mod folder. Launch IDEA and after doing so, click the Import Project button and open the `.ipr` file in your mod folder and wait a moment for IDEA to reconfigure itself for the project.
|
||||||
|
|
||||||
|
**Note:** If you have not launched IDEA before, you may need to go through some first time setup options beforehand.
|
||||||
|
|
||||||
|
Now that you've got IDEA setup, check out [how to setup the main mod class](/tutorials/forge-modding-1102/main-mod-class/).
|
|
@ -0,0 +1,146 @@
|
||||||
|
```
|
||||||
|
metadata.title = "World Generation: Ore"
|
||||||
|
metadata.date = "2016-10-10 11:28:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1102"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.10.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
We've got our copper ore block, but it doesn't generate in the world so it's not very useful to players. Let's fix that.
|
||||||
|
|
||||||
|
The first thing we'll need to do is create a class called `ModWorldGen` in the `world` sub-package in our mod. This class will implement Forge's `IWorldGenerator` interface which is used to hook into Minecraft's world generation.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.world;
|
||||||
|
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import net.minecraft.world.chunk.IChunkGenerator;
|
||||||
|
import net.minecraft.world.chunk.IChunkProvider;
|
||||||
|
import net.minecraftforge.fml.common.IWorldGenerator;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `generate` method is called by Forge for every chunk that's generated and is our entry point into MC's world generation. Inside the `generate` method, we'll check if `world.provider.getDimension() == 0` because we only want our ore to generate in the overworld. If that's true, we'll call a separate method called `generateOverworld` that takes the same parameters as `generate`. In this method we'll have our generation code that's specific to the overworld.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
if (world.provider.getDimension() == 0) { // the overworld
|
||||||
|
generateOverworld(random, chunkX, chunkZ, world, chunkGenerator, chunkProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateOverworld(Random random, int chunkX, int chunkY, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we write the code that will actually add our Copper Ore into the world, let's write a little helper method to make our lives a bit easier.
|
||||||
|
|
||||||
|
This method will take a couple of things:
|
||||||
|
|
||||||
|
1. The `IBlockState` to generate.
|
||||||
|
2. The `World` to generate in.
|
||||||
|
3. The `Random` to use for generation.
|
||||||
|
4. The X and Z positions to generate the block at.
|
||||||
|
5. The minimum and maximum Y positions for which the ore can be generated.
|
||||||
|
6. The size of each ore vein.
|
||||||
|
7. The number of veins per chunk.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private void generateOre(IBlockState ore, World world, Random random, int x, int z, int minY, int maxY, int size, int chances) {
|
||||||
|
int deltaY = maxY - minY;
|
||||||
|
|
||||||
|
for (int i = 0; i < chances; i++) {
|
||||||
|
BlockPos pos = new BlockPos(x + random.nextInt(16), minY + random.nextInt(deltaY), z + random.nextInt(16));
|
||||||
|
|
||||||
|
WorldGenMinable generator = new WorldGenMinable(ore, size);
|
||||||
|
generator.generate(world, random, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This method does a couple of things:
|
||||||
|
|
||||||
|
1. Calculate the difference between the maximum Y and minimum Y values.
|
||||||
|
2. Create a `BlockPos` with X, minimum Y, and Z values passed into the method and offset by:
|
||||||
|
1. A random number from 0 to 15
|
||||||
|
2. A random number from 0 to the difference between the min and max Y values (so that the ore is generated somewhere in between)
|
||||||
|
3. A random number from 0 to 15
|
||||||
|
3. Creates a new `WorldGenMinable` instance.
|
||||||
|
4. Calls the `generate` method on it to generate our ore in the world.
|
||||||
|
5. Repeats steps 2 through 4 `chances` times.
|
||||||
|
|
||||||
|
This results in our ore being generated `chances` times per chunk with each chance having a different position inside the chunk and in between the specified Y values.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private void generateOverworld(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
generateOre(ModBlocks.oreCopper.getDefaultState(), world, random, chunkX * 16, chunkZ * 16, 16, 64, 4 + random.nextInt(4), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateOre(IBlockState ore, World world, Random random, int x, int z, int minY, int maxY, int size, int chances) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We call the `generateOre` method with:
|
||||||
|
|
||||||
|
1. The block state we want to generate (the default block state of our copper ore block).
|
||||||
|
2. The world we want generate in (the `World` we've been passed).
|
||||||
|
3. The random we want to use to generate (the `Random` we've been passed).
|
||||||
|
4. The X position we want to generate at (the `chunkX` value multiplied by 16, because chunks are 16x16).
|
||||||
|
5. The Z position we want to generate at (the `chunkZ` value multiplied by 16, because chunks are 16x16).
|
||||||
|
6. The minimum Y position we want to generate at (16).
|
||||||
|
7. The maximum Y position we want to generate at (64).
|
||||||
|
8. The size of the vein to generate (a random number from 4 to 7).
|
||||||
|
9. The number of times per chunk to generate (6).
|
||||||
|
|
||||||
|
Lastly, in the `preInit` of our main mod class, we'll need to register our world generator.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
GameRegistry.registerWorldGenerator(new ModWorldGen(), 3);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `int` parameter of `GameRegistry.registerWorldGenerator` is the weight of our mod's world generator. This usually doesn't matter, however, if you're experiencing issues with other mods interfering with your world generation, you may want to change this.
|
||||||
|
|
||||||
|
Now, if you create a new world and search around for a bit, you'll be able to find a deposit of our Copper Ore!
|
||||||
|
|
||||||
|
You may want to play around with the vein size and chances settings until you achieve the desired concentration of ore per chunk.
|
||||||
|
|
||||||
|
![Copper Ore generating in the world](http://i.imgur.com/jfeYvi0.png)
|
|
@ -0,0 +1,62 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Advanced Creative Tabs"
|
||||||
|
metadata.date = "2016-06-15 11:42:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Searchable Tab
|
||||||
|
Let's make our creative tab searchable, just like the Search Items tab.
|
||||||
|
|
||||||
|
There are two main parts to this:
|
||||||
|
1. Returning `true` from the `hasSearchBar` method of our creative tab class.
|
||||||
|
2. Setting the texture name for the background image of our creative tab, so the search bar appears.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.client;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class TutorialTab extends CreativeTabs {
|
||||||
|
|
||||||
|
public TutorialTab() {
|
||||||
|
super(TutorialMod.modId);
|
||||||
|
setBackgroundImageName("item_search.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack getTabIconItem() {
|
||||||
|
return new ItemStack(ModItems.ingotCopper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasSearchBar() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we are returning `true` from `hasSearchBar` so Minecraft will allow us to type in our tab and filter the visible items.
|
||||||
|
|
||||||
|
We're also calling `setBackgroundImageName` with `"item_search.png"`. Minecraft will use this string to find the texture to use for the background. It will look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_BACKGROUND_NAME` where `BACKGROUND_NAME` is what you passed into `setBackgroundImageName`. `tag_item_search.png` is provided by Minecraft, so we don't need to do anything else.
|
||||||
|
|
||||||
|
![Searchable Creative Tab](http://i.imgur.com/C34Nh4R.png)
|
||||||
|
|
||||||
|
## Custom Background
|
||||||
|
As explained above, we can use custom backgrounds for our creative tabs.
|
||||||
|
|
||||||
|
> Minecraft will use this string to find the texture to use for the background. It will look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_BACKGROUND_NAME` where `BACKGROUND_NAME` is what you passed into `setBackgroundImageName`.
|
||||||
|
|
||||||
|
By passing a different string into `setBackgroundImageName` and adding the texture into the correct folder of our `src/main/resources` folder, we can use a custom background.
|
||||||
|
|
||||||
|
In our constructor, let's call `setBackgroundImageName` with `"tutorialmod.png"`. This will tell Minecraft to look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png`
|
||||||
|
|
||||||
|
Download [this](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png) texture and save it to `src/main/resources/assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png` in your mod folder.
|
||||||
|
|
||||||
|
That's it! When you open up the creative tab, you should now see our nice custom texture!
|
||||||
|
|
||||||
|
![Creative Tab with Custom Background](http://i.imgur.com/pP2W6h0.png)
|
|
@ -0,0 +1,206 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Armor"
|
||||||
|
metadata.date = "2016-09-17 16:53:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we can create armor, we'll need to create an armor material to use for our copper armor.
|
||||||
|
|
||||||
|
We'll add a new field to our main mod class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static final ItemArmor.ArmorMaterial copperArmorMaterial = EnumHelper.addArmorMaterial("COPPER", modId + ":copper", 15, new int[]{2, 5, 6, 2}, 9, SoundEvents.ITEM_ARMOR_EQUIP_IRON, 0.0F);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnumHelper.addArmorMaterial` takes a number of parameters:
|
||||||
|
- `"COPPER"`: The name of the new enum value, this is completely capitalized, following the enum naming convention.
|
||||||
|
- `modId + ":copper"`: This is the texture that will be used for our armor. We prefix it with our mod ID to use our mod's domain instead of the default `minecraft` domain.
|
||||||
|
- `15`: The maximum damage factor.
|
||||||
|
- `new int[]{2, 5, 6, 2}`: The damage reduction factors for each armor piece.
|
||||||
|
- `9`: The enchantibility of the armor.
|
||||||
|
- `SoundEvents.ITEM_ARMOR_EQUIP_IRON`: The sound event that is played when the armor is equipped.
|
||||||
|
- `0.0F`: The toughness of the armor.
|
||||||
|
|
||||||
|
Next we'll need the textures for the armor material that are used to render the on-player overlay.
|
||||||
|
Download the layer 1 texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/tutorial/textures/models/armor/copper_layer_1.png) and save it to `src/main/resources/assets/tutorial/textures/model/armor/copper_layer_1.png`. Download the layer 2 texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/tutorial/textures/models/armor/copper_layer_2.png) and save it to `src/main/resources/assets/tutorial/textures/models/armor/copper_layer_2.png`.
|
||||||
|
|
||||||
|
## Armor Item Base Class
|
||||||
|
Before we can begin creating armor items, we'll need to create a base class that implements our `ItemModelProvider` interface so it can be used with our registration helper method.
|
||||||
|
|
||||||
|
We'll create a class called `ItemArmor` in our `item` package that extends the Vanilla `ItemArmor` class and implements `ItemModelProvider`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.inventory.EntityEquipmentSlot;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemArmor extends net.minecraft.item.ItemArmor implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemArmor(ArmorMaterial material, EntityEquipmentSlot slot, String name) {
|
||||||
|
super(material, 0, slot);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Helmet
|
||||||
|
Firstly, we'll create a field for our copper helmet item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperHelmet;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperHelmet = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.HEAD, "copper_helmet"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_helmet.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_helmet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_helmet.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_helmet.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_helmet.name=Copper Helmet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Chestplate
|
||||||
|
First, we'll create a field for our copper chestplate item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperChestplate;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperChestplate = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.CHEST, "copper_chestplate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_chestplate.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_chestplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_chestplate.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_chestplate.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_chestplate.name=Copper Chestplate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Leggings
|
||||||
|
First, we'll create a field for our copper leggings item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperLeggings;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperLeggings = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.LEGS, "copper_leggings"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_leggings.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_leggings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_leggings.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_leggings.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_leggings.name=Copper Leggings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Boots
|
||||||
|
First, we'll create a field for our copper boots item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperBoots;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperBoots = register(new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.FEET, "copper_boots"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_boots.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_boots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper boots texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_boots.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_boots.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the boots.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_boots.name=Copper Boots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Done!
|
||||||
|
Now, when we run the game, we can obtain our copper armor from the Combat creative tab, and when we equip it, we can see the player overlay being rendered and the armor value being show on the HUD:
|
||||||
|
|
||||||
|
![copper armor screenshot](https://i.imgur.com/Vv8Qzne.png)
|
|
@ -0,0 +1,152 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Basic Blocks"
|
||||||
|
metadata.date = "2016-05-07 21:45:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
For our first block, we are going to make a Copper Ore to go along with our Copper Ingot.
|
||||||
|
|
||||||
|
### Base Block
|
||||||
|
We're going to do something similar to what we did for [Basic Items](/tutorials/forge-modding-111/basic-items/), create a base class for all of our blocks to extend to make our life a bit easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemBlock;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class BlockBase extends Block {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public BlockBase(Material material, String name) {
|
||||||
|
super(material);
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
setRegistryName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel(ItemBlock itemBlock) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(itemBlock, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockBase setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is almost exactly the same as our `ItemBase` class except it extends `Block` instead of `Item`. It sets the unlocalized and registry names, has a method to register the item model, and has an overriden version of `Block#setCreativeTab` that returns a `BlockBase`.
|
||||||
|
|
||||||
|
We'll also create a `BlockOre` class which extends `BlockBase` to make adding ore's a little easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
|
||||||
|
public class BlockOre extends BlockBase {
|
||||||
|
|
||||||
|
public BlockOre(String name) {
|
||||||
|
super(Material.ROCK, name);
|
||||||
|
|
||||||
|
setHardness(3f);
|
||||||
|
setResistance(5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockOre setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ModBlocks`
|
||||||
|
|
||||||
|
Now let's create a `ModBlocks` class similar to `ModItems` to assist us when registering blocks.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemBlock;
|
||||||
|
import net.minecraftforge.fml.common.registry.GameRegistry;
|
||||||
|
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block, ItemBlock itemBlock) {
|
||||||
|
GameRegistry.register(block);
|
||||||
|
GameRegistry.register(itemBlock);
|
||||||
|
|
||||||
|
if (block instanceof BlockBase) {
|
||||||
|
((BlockBase)block).registerItemModel(itemBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block) {
|
||||||
|
ItemBlock itemBlock = new ItemBlock(block);
|
||||||
|
itemBlock.setRegistryName(block.getRegistryName());
|
||||||
|
return register(block, itemBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This class is slightly different than our `ModItems` class due to the way blocks work in 1.9. In 1.9, we register the block and the `ItemBlock` separately whereas previously Forge would register the default `ItemBlock` automatically.
|
||||||
|
|
||||||
|
**Brief aside about how `ItemBlock`s work:** The `ItemBlock` for a given block is what is used as the inventory form of a given block. In the game, when you have a piece of Cobblestone in your inventory, you don't actually have on of the Cobblestone blocks in your inventory, you have one of the Cobblestone _`ItemBlock`s_ in your inventory.
|
||||||
|
|
||||||
|
Once again, we'll need to update our `preInit` method to call the `init` method of our `ModBlocks` class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
ModItems.init();
|
||||||
|
ModBlocks.init();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Copper Ore
|
||||||
|
|
||||||
|
Now, because we have our `BlockBase` and `ModBlocks` classes in place, we can quickly add a new block:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static BlockOre oreCopper;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
oreCopper = register(new BlockOre("ore_copper").setCreativeTab(CreativeTabs.MATERIALS));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Create a new `BlockOre` with the name `oreCopper`.
|
||||||
|
2. Sets the creative tab of the block to the Materials tab.
|
||||||
|
3. Registers the block with the `GameRegistry`.
|
||||||
|
4. Registers the default `ItemBlock` with the `GameRegistry`.
|
||||||
|
|
||||||
|
|
||||||
|
Now, in the game, we can see our (untextured) copper ore block!
|
||||||
|
|
||||||
|
![Copper Ore Screenshot](http://i.imgur.com/uWdmyA5.png)
|
||||||
|
|
||||||
|
Next, we'll look at how to make a simple model for our copper ore block.
|
|
@ -0,0 +1,37 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Forge Blockstates"
|
||||||
|
metadata.date = "2016-05-07 21:45:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got our copper ore block, let's add a simple blockstate to give it a texture. This will go in a file at `src/main/resources/assets/tutorial/blockstates/ore_copper.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"defaults": {
|
||||||
|
"textures": {
|
||||||
|
"all": "tutorial:blocks/ore_copper"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"normal": {
|
||||||
|
"model": "cube_all"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"model": "cube_all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `forge_marker` (L2): This tells Forge to use its custom blockstate parser instead of Minecraft's which isn't as good. (See [here](https://mcforge.readthedocs.io/en/latest/blockstates/forgeBlockstates/) for more info about Forge's blockstate format)
|
||||||
|
- `defaults` (L3-L7): Defaults are things to apply for all variants, a feature added by Forge's blockstate format.
|
||||||
|
- `textures` (L4-L6): This specifies which textures to use for the `cube_all` model. This uses the same texture format as explained in the [JSON Item Models](https://shadowfacts.net/tutorials/forge-modding-1102/json-item-models/) tutorial.
|
||||||
|
- `variants` (L8-L15): Inside of this block are where all of our individual variants go. Because we don't have any custom block properties, we have the `normal` variant which is the normal, in-world variant. The `inventory` variant is used when rendering our item in inventory and in the player's hand.
|
||||||
|
- `"model": "cube_all"` (L10 & L13): This uses the `cube_all` model for both variants. This is a simple model included in Minecraft which uses the same `#all` texture for every side of the block. We can't include this in the `defaults` block because Forge expects there to be at least one thing in each variant block.
|
||||||
|
|
||||||
|
Now, we just need to download the [copper ore texture](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/blocks/ore_copper.png) to `src/main/resources/assets/tutorial/textures/blocks/ore_copper.png` and we're all set!
|
||||||
|
|
||||||
|
![Textured Copper Ore Screenshot](http://i.imgur.com/wJ1iJUg.png)
|
|
@ -0,0 +1,134 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Basic Items"
|
||||||
|
metadata.date = "2016-05-07 16:32:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got the basic structure of our mod set up, we can create our first item. This item will be fairly simple, just a copper ingot.
|
||||||
|
|
||||||
|
|
||||||
|
### Base Item
|
||||||
|
|
||||||
|
Before we actually begin creating items, we'll want to create a base class just to make things easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemBase extends Item {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public ItemBase(String name) {
|
||||||
|
this.name = name;
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
setRegistryName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel() {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemBase setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `ItemBase` class will make it simpler to add basic items quickly. `ItemBase` primarily has a convenience constructor that sets both the unlocalized and the registry names.
|
||||||
|
|
||||||
|
- The unlocalized name is used for translating the name of the item into the currently active language.
|
||||||
|
- The registry name is used when registering our item with Forge and should _never, ever change_.
|
||||||
|
|
||||||
|
The `setCreativeTab` method is an overriden version that returns `ItemBase` instead of `Item` so we can use it in our `register` method without casting, as you'll see later.
|
||||||
|
|
||||||
|
You will have an error because we haven't created the `registerItemRenderer` method yet, so let's do that now. In the `CommonProxy` class add a new method called `registerItemRenderer` that accepts an `Item`, an `int`, and a `String`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public void registerItemRenderer(Item item, int meta, String id) {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll leave this method empty, because it's in the common proxy so it can't access any client-only code, but it still needs to be here becuase `TutorialMod.proxy` is of type `CommonProxy` so any client-only methods still need to have an empty stub in the `CommonProxy`.
|
||||||
|
|
||||||
|
To our `ClientProxy` we'll add the actual implementation of `registerItemRenderer`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void registerItemRenderer(Item item, int meta, String id) {
|
||||||
|
ModelLoader.setCustomModelResourceLocation(item, meta, new ModelResourceLocation(TutorialMod.modId + ":" + id, "inventory"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This method calls `ModelLoader.setCustomModelResourceLocation` which will tell Minecraft which item model to use for our item.
|
||||||
|
|
||||||
|
Lastly, we'll need to update our `preInit` method to call `ModItems.init` to actually create and register our items.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
ModItems.init();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ModItems`
|
||||||
|
|
||||||
|
Create a class called `ModItems`. This class will contain the instances of all of our items. In Minecraft, items are singletons so we'll only ever have on instance, and a reference to this instance will be kept in our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraftforge.fml.common.registry.GameRegistry;
|
||||||
|
|
||||||
|
public class ModItems {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Item> T register(T item) {
|
||||||
|
GameRegistry.register(item);
|
||||||
|
|
||||||
|
if (item instanceof ItemBase) {
|
||||||
|
((ItemBase)item).registerItemModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Right now the `init` method is empty, but this is where we'll put the calls to `register` to register our items. The `register` method does a couple of things:
|
||||||
|
|
||||||
|
1. Registers our item with the `GameRegistry`.
|
||||||
|
2. Registers the item model if one is present.
|
||||||
|
|
||||||
|
### Copper Ingot
|
||||||
|
|
||||||
|
Now to create our actual item, the copper ingot. Because we've created the `ItemBase` helper class, we won't need to create any more classes. We'll simply add a field for our new item and create/register/set it in the `init` method of our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static ItemBase ingotCopper;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
ingotCopper = register(new ItemBase("ingot_copper").setCreativeTab(CreativeTabs.MATERIALS));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Create a new `ItemBase` with the name `ingot_copper`
|
||||||
|
2. Set the creative tab to the Materials tab.
|
||||||
|
3. Register our item with the `GameRegistry`.
|
||||||
|
|
||||||
|
Now, if you load up the game and go into the Materials creative tab, you shoulds see our new copper ingot item (albeit without a model)! Next time we'll learn how to make basic JSON models and add a model to our copper ingot!
|
||||||
|
|
||||||
|
![Copper Ingot Item Screenshot](http://i.imgur.com/6uHudqH.png)
|
|
@ -0,0 +1,87 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Crafting/Smelting Recipes"
|
||||||
|
metadata.date = "2016-06-30 10:49:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Before we implement any recipes, we'll create a little class in which we'll register all of our recipes. We'll call this class `ModRecipes`, following the same convention as our `ModItems` and `ModBlocks` classes. We'll put this into the `recipe` package inside our main package so once when we implement more complex custom recipes, we'll have a place to group them together.
|
||||||
|
|
||||||
|
You'll want an empty `init` static method in `ModRecipes` which is where we'll register our recipes in just a moment.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.recipe;
|
||||||
|
|
||||||
|
public class ModRecipes {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ModRecipes.init` method should be called from `TutorialMod#init`. The init method is called during the initialization phase which occurs after the pre-initialization phase. We want to register our recipes here instead of in the pre-init or post-init phases because all mod items/blocks (including those from other mods) should be registered at this point.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void init(FMLInitializationEvent event) {
|
||||||
|
ModRecipes.init();
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Crafting Recipes
|
||||||
|
There are two kinds of crafting recipes: shaped recipes and shapeless recipes.
|
||||||
|
|
||||||
|
In a shapeless recipe, the ingredients can be placed in any arrangement on the crafting grid. An example of a shapeless recipe is the [Pumpkin Pie recipe](http://minecraft.gamepedia.com/Pumpkin_Pie#Crafting).
|
||||||
|
|
||||||
|
Shaped recipes require their ingredients to be placed in a specific arrangement. An example of a shaped recipe is the [Cake recipe](http://minecraft.gamepedia.com/Cake#Crafting).
|
||||||
|
|
||||||
|
## Shapeless Recipe
|
||||||
|
Our shapeless recipe is going to be a simple recipe that lets people craft 1 corn into 1 corn seed. All this requires is 1 line in `ModRecipes`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void init() {
|
||||||
|
GameRegistry.addShapelessRecipe(new ItemStack(ModItems.cornSeed), ModItems.corn);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GameRegistry.addShapelessRecipe` does exactly as the name says, it registers a shapeless recipe. The first argument is an `ItemStack` that is the output of the recipe, in this case a corn seed. The second argument is a varargs array of `Object`s that can be `Item`s, `Block`s, or `ItemStack`s.
|
||||||
|
|
||||||
|
![Shapeless Recipe](http://i.imgur.com/tFZdyK3.png)
|
||||||
|
|
||||||
|
## Shaped Recipe
|
||||||
|
Our shaped recipe is going to be an additional recipe for Rabbit Stew that accepts corn instead of carrots. This requires a call to `GameRegistry.addShapedRecipe` which, you guessed it, registers a shaped recipe.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
GameRegistry.addShapedRecipe(new ItemStack(Items.RABBIT_STEW), " R ", "CPM", " B ", 'R', Items.COOKED_RABBIT, 'C', ModItems.corn, 'P', Items.BAKED_POTATO, 'M', Blocks.BROWN_MUSHROOM, 'B', Items.BOWL);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument to `GameRegistry.addShapedRecipe` is an `ItemStack` that is the output of the recipe. The next arguments should be anywhere from 1 to 3 `String` arguments that determine the pattern of the recipe. A space in a pattern string represents an empty slot and each character represents an item/block/stack specified by the following arguments. The remaining arguments should be pairs of characters and `Item`/`Block`/`ItemStack`. The character should be the same (including case) as used in the pattern strings. The item/block/stack should be what is used for the instances of that character in the pattern.
|
||||||
|
|
||||||
|
Our finished recipe looks like this:
|
||||||
|
|
||||||
|
![Shaped Recipe](http://i.imgur.com/KaatGDN.png)
|
||||||
|
|
||||||
|
## Smelting Recipe
|
||||||
|
Our furnace recipe is going to be a simple 1 Copper Ore to 1 Copper Ingot recipe. All this requires is 1 call to `GameRegistry.addSmelting`
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
GameRegistry.addSmelting(ModBlocks.oreCopper, new ItemStack(ModItems.ingotCopper), 0.7f);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GameRegistry.addSmelting` takes 3 parameters, the item/block/stack input, the `ItemStack` output, and the amount of experience to be given to the player (per smelt).
|
||||||
|
|
||||||
|
![Smelting Recipe](http://i.imgur.com/aU1ZiqG.png)
|
|
@ -0,0 +1,94 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Creative Tabs"
|
||||||
|
metadata.date = "2016-06-14 16:26:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
In this tutorial, we are going to create a custom creative tab that players can use to access all of our items when in creative mode.
|
||||||
|
|
||||||
|
## Creative Tab
|
||||||
|
First off, let's create our creative tab class. Create a class called `TutorialTab` that extends `CreativeTabs`. It will need a couple things:
|
||||||
|
|
||||||
|
1. A no-args constructor that calls the super constructor with the correct label.
|
||||||
|
2. An overriden `getTabIconItem` which returns the item to render as the icon.
|
||||||
|
|
||||||
|
The `String` passed into the super constructor is the label. The label is used to determine the localization key for the tab. For the label, we are going to pass in `TutorialMod.modId` so Minecraft uses our mod's ID to determine the localization key.
|
||||||
|
|
||||||
|
The item stack we return from the `getTabIconItem` will be rendered on the tab in the creative inventory GUI. We'll use `ModItems.ingotCopper` as the icon so our creative tab has a nice distinctive icon.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.client;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class TutorialTab extends CreativeTabs {
|
||||||
|
|
||||||
|
public TutorialTab() {
|
||||||
|
super(TutorialMod.modId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack getTabIconItem() {
|
||||||
|
return new ItemStack(ModItems.ingotCopper);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's add a field to our `TutorialMod` class that stores the instance of our creative tab.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static final TutorialTab creativeTab = new TutorialTab();
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating Everything Else
|
||||||
|
Now that we've got our creative tab all setup, let's change all of our items and blocks to use it.
|
||||||
|
|
||||||
|
Let's add a line to the end of our `BlockBase` and `ItemBase` constructors that calls `setCreativeTab` with our creative tab.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public BlockBase(Material material, String name) {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ItemBase(String name) {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also need to add this line to the `BlockCropCorn` and `ItemCornSeed` classes because they don't extend our base item/block classes.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public BlockCropCorn() {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ItemCornSeed() {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, we'll need to update our `ModBlocks` and `ModItems` classes so we're no longer setting the creative tabs to other tabs.
|
||||||
|
|
||||||
|
Remove the `setCreativeTab` call from the end of the BlockOre constructor on the line where we register/create the copper ore block.
|
||||||
|
|
||||||
|
Remove the `setCreativeTab` calls from the copper ingot and corn items in `ModItems`.
|
||||||
|
|
||||||
|
## All Done!
|
||||||
|
Now when we start the game and open the creative inventory, we should be able to see our creative tab on the second page.
|
||||||
|
|
||||||
|
![our creative tab in action](http://i.imgur.com/JfEhwvu.png)
|
|
@ -0,0 +1,259 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Crops"
|
||||||
|
metadata.date = "2016-05-29 10:29:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
Before we get started making our corn crop, we'll need to make a couple of changes to the base item/block infrastructure we've already created. Specifically what we need to change has to do with item models. Currently, `BlockBase` and `ItemBase` have their own `registerItemModel` methods. We're going to move this into the `ItemModelProvider` interface so that blocks and items we create that don't extend `BlockBase` or `ItemBase` can still use our system for registering item models.
|
||||||
|
|
||||||
|
Create a new `ItemModelProvider` interface and add one method to it:
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
|
||||||
|
public interface ItemModelProvider {
|
||||||
|
|
||||||
|
void registerItemModel(Item item);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This interface functions exactly the same as the `registerItemModel` methods in `BlockBase` and `ItemBase`.
|
||||||
|
|
||||||
|
Next, we'll change `BlockBase` to implement `ItemModelProvider`. Just add the `implements ItemModelProvider` after the class declaration, change the `reigsterItemModel` method to accept an `Item` instead of an `ItemBlock` and add `@Override` to the `registerItemModel` method.
|
||||||
|
|
||||||
|
We'll repeat a similar process for `ItemBase`. Add `implements ItemModelProvider`, change `registerItemModel` to accept an `Item`, and add `@Override` to it.
|
||||||
|
|
||||||
|
Now that we've changed our `BlockBase` and `ItemBase` classes, we'll need to make some changes to our `ModItems` and `ModBlocks` classes to ensure that `ItemModelProvider#registerItemModel` is called even if the block or item isn't a subclass of our block or item base classes.
|
||||||
|
|
||||||
|
In `ModBlocks`, simply change `block instanceof BlockBase` to `block instanceof ItemModelProvider` and change the cast from `BlockBase` to `ItemModelProvider`. Do the same for the `ModItems` class, replacing `ItemBase` with `ItemModelProvider` in the appropriate section of the code.
|
||||||
|
|
||||||
|
Due to the way we are going to implement our crop, we'll need to make another modification to our `ModBlocks` class. Before we make this modification, let me explain why it's necessary.
|
||||||
|
|
||||||
|
Our crop is going to have 3 important parts:
|
||||||
|
|
||||||
|
1. The crop block
|
||||||
|
2. The seed item
|
||||||
|
3. The food item
|
||||||
|
|
||||||
|
Because we have a separate seed item, the crop block won't have an `ItemBlock` to go along with it.
|
||||||
|
|
||||||
|
In our `register(T block, ItemBlock itemBlock)` method, we'll need to add a null check to `itemBlock` so we don't attempt to register `itemBlock` if it's null.
|
||||||
|
|
||||||
|
Lastly, we'll need to make one change in our main `TutorialMod` class. Due to the way Minecraft's `ItemSeed` works, our blocks need to be initialized before we can call the constructor of our seed. Simply move the `ModItems.init` call in `TutorialMod.preInit` to after the `ModBlocks.init` call.
|
||||||
|
|
||||||
|
## Crop
|
||||||
|
The crop we are going to create will be corn.
|
||||||
|
|
||||||
|
As I mentioned before, our crop will be divded into 3 main parts:
|
||||||
|
|
||||||
|
1. The crop block (corn crop)
|
||||||
|
2. The seed item (corn seed)
|
||||||
|
3. The food item (corn)
|
||||||
|
|
||||||
|
We're going to create these one at a time. At the intermediate stages, our code will contain errors because we are referencing things we haven't created yet, but everything should be fine at the end.
|
||||||
|
|
||||||
|
### Corn Crop
|
||||||
|
Let's create a class called `BlockCropCorn` that extends Minecraft's `BlockCrops`. The crop block won't have an `ItemBlock` so this class won't implement `ItemModelProvider` and it is why we need that null check in `ModBlocks.register`.
|
||||||
|
|
||||||
|
In this class, we'll need to override 2 methods to return our own items instead of the vanilla ones. `getSeed` should return `ModItems.cornSeed` and `getCrop` should return `ModItems.corn`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.BlockCrops;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class BlockCropCorn extends BlockCrops {
|
||||||
|
|
||||||
|
public BlockCropCorn() {
|
||||||
|
setUnlocalizedName("crop_corn");
|
||||||
|
setRegistryName("crop_corn");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Item getSeed() {
|
||||||
|
return ModItems.cornSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Item getCrop() {
|
||||||
|
return ModItems.corn;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Minecraft will use the seed and crop we specified to determine what to drop when our crop block is broken. Let's register our block in the `ModBlocks` class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static BlockCropCorn cropCorn;
|
||||||
|
|
||||||
|
public static init() {
|
||||||
|
// ...
|
||||||
|
cropCorn = register(new BlockCropCorn(), null);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The last thing we'll need to do is create a model. Download the textures from [here](https://github.com/shadowfacts/TutorialMod/tree/1.11/src/main/resources/assets/tutorial/textures/blocks/corn/) and save them into `src/main/resources/assets/tutorial/textures/blocks/corn/` and have there filenames the `0.png` through `7.png`.
|
||||||
|
|
||||||
|
Let's create a blockstate for our crop. Create `src/main/resources/assets/tutorial/blockstates/crop_corn.json`. We're once again going to be using the Forge blockstates format because this is a fairly complicated blockstate.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"defaults": {
|
||||||
|
"model": "cross"
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"age": {
|
||||||
|
"0": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"textures": {
|
||||||
|
"cross": "tutorial:blocks/corn/7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `model` specified in the `defaults` section is Minecraft's `cross` model which is just the same texture rendered twice. You can see what this model looks like by looking at the various flowers in vanilla.
|
||||||
|
|
||||||
|
The `age` property is the age of the crop. All the objects inside the `age` object are for one value of the property. In our case, `age` can have a value 0 through 7 so we'll need separate JSON objects for each of those. For each value of age, we'll have a different texture that is specified in the `textures` object with the name `cross`.
|
||||||
|
|
||||||
|
### Corn Seed
|
||||||
|
Now let's make our corn seed item. Create a new class called `ItemCornSeed` that extends `ItemSeeds` and implements `ItemModelProvider`.
|
||||||
|
|
||||||
|
In our constructor, we'll need to pass a couple of things to the `ItemSeeds` constructor, `ModBlocks.cropCorn` and `Blocks.FARMLAND`. The first parameter of the `ItemSeeds` constructor is the crop block and the second is the soil block. Since we implemented `ItemModelProvider`, we'll need to provide an implementation for `registerItemModel` which will just use our `registerItemRenderer` proxy method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.init.Blocks;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemSeeds;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.block.ModBlocks;
|
||||||
|
|
||||||
|
public class ItemCornSeed extends ItemSeeds implements ItemModelProvider {
|
||||||
|
|
||||||
|
public ItemCornSeed() {
|
||||||
|
super(ModBlocks.cropCorn, Blocks.FARMLAND);
|
||||||
|
setUnlocalizedName("corn_seed");
|
||||||
|
setRegistryName("corn_seed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(item, 0, "corn_seed");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's register our corn seed in `ModItems`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static ItemCornSeed cornSeed;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
cornSeed = register(new ItemCornSeed());
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, we'll create a simple JSON model for the corn seed. First you'll want to download the texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/corn_seed.png) and save it to `src/main/resources/assets/tutorial/textures/items/cornSeed.png`. Now create a JSON file in the `models/item` folder called `cornSeed.json`. This model with be fairly similar to our copper ingot model, it will just have a parent of `item/generated` and a layer 0 texture of `tutorial:items/corn_seed`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/corn_seed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Corn Item
|
||||||
|
For now, our corn item is going to be a simple instance of our `ItemBase` class which means you won't be able to eat it (yet!). Let's add our corn item to our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static ItemBase corn;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
corn = register(new ItemBase("corn").setCreativeTab(CreativeTabs.FOOD));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's also make a simple model for our corn item. Download the texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/tutorial/textures/items/corn.png) and save it as `corn.png` in the `textures/items` folder. Now let's create a `corn.json` file for our model. This model will also be very simple, with a parent of `item/generated` and a layer 0 texture of `tutorial:items/corn`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/corn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
Now let's quickly add localization for new items:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.corn.name=Corn
|
||||||
|
item.corn_seed.name=Corn Seed
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finished
|
||||||
|
Now, you should be able to launch game from inside the IDE and see our corn seed in the materials creative tab, plant it, grow it with bone meal, and break it to get corn and more seeds.
|
||||||
|
|
||||||
|
![Corn Screenshot](http://i.imgur.com/1G1k8Sh.png)
|
|
@ -0,0 +1,439 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Dynamic Tile Entity Rendering"
|
||||||
|
metadata.date = "2017-03-30 17:03:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that our Pedestal is capable of storing an item and has a GUI with which players can interact, let's add some rendering code so the stored item actually renders in the world.
|
||||||
|
|
||||||
|
## Updating the `TileEntity`
|
||||||
|
The first thing we'll need to do is make some modifications to our `TileEntityPedestal` to accomodate the new rendering code.
|
||||||
|
|
||||||
|
First off, we'll add a `long` field called `lastChangeTime` to our TE. This field will store the world time, in ticks, of the last time the TE's inventory was modified. This number will be used in our rendering code to make sure that, when the pedestal's item is bobbing up and down (similar to item entities), they're not all synchronized.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
|
||||||
|
private ItemStackHandler inventory = new ItemStackHandler(1);
|
||||||
|
public long lastChangetime;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also add this to the `writeToNBT` and `readFromNBT` methods so that it persists between launches:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||||
|
compound.setTag("inventory", inventory.serializeNBT());
|
||||||
|
compound.setLong("lastChangeTime", lastChangeTime);
|
||||||
|
return super.writeToNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFromNBT(NBTTagCompound compound) {
|
||||||
|
inventory.deserializeNBT(compound.getCompoundTag("inventory"));
|
||||||
|
lastChangeTime = compound.getLong("lastChangeTime");
|
||||||
|
super.readFromNBT(compound);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll make our `inventory` field public and change our `ItemStackHandler` instance to override the `onContentsChanged` method on instantiation. We need this because, when our TE's inventory is updated on the server, we'll need to notify the client of the change so that it renders the correct item.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
|
||||||
|
public ItemStackHandler inventory = new ItemStackHandler(1) {
|
||||||
|
@Override
|
||||||
|
protected void onContentsChanged(int slot) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
lastChangeTime = world.getTotalWorldTime();
|
||||||
|
TutorialMod.network.sendToAllAround(new PacketUpdatedPedestal(TileEntityPedestal.this), new NetworkRegistry.TargetPoint(world.provider.getDimension(), pos.getX(), pos.getY(), pos.getZ(), 64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `onContentsChanged` method is called by Forge's `ItemStackHandler` every time what's stored in a slot changes. We surround our code in a `!world.isRemote` check because we don't want this code, which sends a packet, to be executed on the client and the server, but just the server.
|
||||||
|
|
||||||
|
This code updates the `lastChangeTime` field with the current world time and sends a `PacketUpdatePedestal` (which we'll create in the next section) to every player within 64 meters of our block's position.
|
||||||
|
|
||||||
|
Nextly, we'll override the `onLoad` method. This method will (on the client side) send a packet to the server requesting an update, which will inform the client of what's stored in the pedestal and the last time it was modified. We specifically request a packet when a client loads the TE because the TE is only saved on the server, and the client isn't aware of what data is stored in it, so we specifically request an update from the server.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public void onLoad() {
|
||||||
|
if (world.isRemote) {
|
||||||
|
TutorialMod.network.sendToServer(new PacketRequestUpdatePedestal(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `world.isRemote` check makes sure that the packet is only sent when `onLoad` is being called on the client side and `sendToServer`, as the name suggests, sends the packet to the server. The packet itself, `PacketRequestUpdatePedestal`, will be created in the next section.
|
||||||
|
|
||||||
|
Lastly for the tile entity, we'll override the `getRenderBoundingBox` method. This method returns an Axis Aligned Bounding Box (AABB) which is used by Minecraft to check if our tile entity should be rendered. If the AABB is in view on the player, it will be rendered, otherwise it won't. Because of the way our tile entity will render the item (floating above the pedestal base), we need to use a larger box than normal, so that if the base is out of view but the item isn't, the item is still rendered.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public AxisAlignedBB getRenderBoundingBox() {
|
||||||
|
return new AxisAlignedBB(getPos(), getPos().add(1, 2, 1));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we simply return a new box with the first position being at the block's position and the second position being at the opposite corner that's two above the base of the block.
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
This is the first feature in our mod for which we'll need networking and packets to transmit information between the client and server.
|
||||||
|
|
||||||
|
For networking in our mod, we'll use Forge's SimpleImpl system which is split into three parts:
|
||||||
|
|
||||||
|
1. A channel (a `SimpleNetworkWrapper` instance) which is unique to our mod and is managed by Forge.
|
||||||
|
2. An `IMessage` implementation. This handles serializing/deserializing for transmission over the network.
|
||||||
|
3. An `IMessageHandler` implementation which is used to run code when the packet is received.
|
||||||
|
|
||||||
|
Firstly, we'll need to setup our `SimpleNetworkWrapper` instance. Let's add a field to our main mod class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static SimpleNetworkWrapper wrapper;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then we'll set it up in our `preInit` method:
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
network = NetworkRegistry.INSTANCE.newSimpleChannel(modId);
|
||||||
|
network.registerMessage(new PacketUpdatePedestal.Handler(), PacketUpdatePedestal.class, 0, Side.CLIENT);
|
||||||
|
network.registerMessage(new PacketRequestUpdatePedestal.Handler(), PacketRequestUpdatePedestal.class, 1, Side.SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this code, we:
|
||||||
|
|
||||||
|
1. Call `newSimpleChannel` with our mod ID to obtain a `SimpleNetworkWrapper` instance specific to our mod.
|
||||||
|
2. Call `registerMessage` on our channel to register our two packets. For each packet, we pass in the `IMessageHandler` instance, the class of the `IMessage` implementation, the ID (unique to our channel) of the packet, and the `Side` on which it's received.
|
||||||
|
|
||||||
|
The two packets we register are: the `PacketUpdatePedestal`, which is sent from the server to the client and updates the item stored in the pedestal on the client side, and the `PacketRequestUpdatedPedestal` , which is sent from the client to the server to request an update from the server. The `PacketUpdatePedestal` is used whenever the item changes on the server to notify the client. The `PacketRequestUpdatePedestal` is sent to the server when the client first joins to get the stored stack (because the data is only saved on the server, not the client).
|
||||||
|
|
||||||
|
Let's start with the `PacketUpdatePedestal` class. We'll create it in a new `network` package and we'll make it implement Forge's `IMessage` interface.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.network;
|
||||||
|
|
||||||
|
import net.minecraftforge.fml.network.simpleimpl.IMessage;
|
||||||
|
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first thing we'll need to do is add a couple fields to the packet. The fields we'll need are: `BlockPos pos` for the tile entity's position, `ItemStack stack` for the stack that's stored in the pedestal, and `long lastChangeTime` for the last time the pedestal's constant has changed.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
|
||||||
|
private BlockPos pos;
|
||||||
|
private ItemStack stack;
|
||||||
|
private long lastChangeTime;
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll add a couple constructors. The first constructor will take parameters for all three fields and assign them as expected. The second one will be a convince constructor that takes a parameter for the `TileEntityPedestal` and calls the first constructor will all the values determined from the TE. The last constructor will take no parameters and won't initialize any of the fields. This is necessary because Forge's SimpleImpl will call it via reflection and then call the `fromBytes` method which will initialize the fields.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public PacketUpdatePedestal(BlockPos pos, ItemStack stack, long lastChangeTime) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.stack = stack;
|
||||||
|
this.lastChangeTime = lastChangeTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketUpdatePedestal(TileEntityPedestal te) {
|
||||||
|
this(te.getPos(), te.inventory.getStackInSlot(0), te.lastChangeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketUpdatePedestal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll implement the `toBytes` and `fromBytes` methods which respectively serialize to a `ByteBuf` for transmission over the network and deserialize it back from a `ByteBuf`. One key thing about these two methods is that because they're serializing/deserializing from a `ByteBuf`, which is just a sequence of bytes, we need to do everything in the same order in both methods.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toBytes(ByteBuf buf) {
|
||||||
|
buf.writeLong(pos.toLong());
|
||||||
|
ByteBufUtils.writeItemStack(buf, stack);
|
||||||
|
buf.writeLong(lastChangeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromBytes(ByteBuf buf) {
|
||||||
|
pos = BlockPos.fromLong(buf.readLong());
|
||||||
|
stack = ByteBufUtils.readItemStack(buf);
|
||||||
|
lastChangeTime = buf.readLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First, we use `BlockPos`' `toLong` and `fromLong` to serialize that, then we use the helper methods in `ByteBufUtils` to serialize the stack, and lastly we write the `lastChangeTime`. Note that we're reading/writing things in the same order in both these methods. This is very important, and if something's out of order, we'll end up getting the wrong data when our packet is received.
|
||||||
|
|
||||||
|
Lastly, we'll add the handler. Let's create a static inner `Handler` class in our `PacketUpdatePedestal` class that implements `IMessageHandler`. This interface takes two generic type paramters: the type of the packet that the handler's handling and the type of the packet that the handler responds with. The first type will obviously be `PacketUpdatePedestal` and, because we don't want to respond with a packet, the return packet type will just be `IMessage` and we'll return `null` from the handler method.
|
||||||
|
|
||||||
|
What we're going to do in the handler's `onMessage` method is get the tile entity from the world and update its inventory and `lastChangeTime`. Unfortunately, there's a caveat to this so it's a bit more complicated. With Netty (the library Minecraft and Forge use for networking), packets are handled on a different thread that's not the main thread. Because we're going to be interacting with and modifying the world, we can't just do it from a different thread because it could potentially cause a `ConcurrentModificationException` to be thrown. To deal with this, we'll call the `Minecraft.addScheduledTask` method which executes the given `Runnable` on the main thread as soon as possible, so in this runnable, we _can_ interact with the world.
|
||||||
|
|
||||||
|
In the runnable, we simply get the tile entity from the client world (`Minecraft.getMinecraft().world`) and modify its inventory and set its `lastChangeTime` field.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketUpdatePedestal implements IMessage {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public static class Handler implements IMessageHandler<PacketUpdatePedestal, IMessage> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IMessage onMessage(PacketUpdatePedestal message, MessageContext ctx) {
|
||||||
|
Minecraft.getMinecraft().addScheduledTask(() -> {
|
||||||
|
TileEntityPedestal te = (TileEntityPedestal)Minecraft.getMinecraft().world.getTileEntity(message.pos);
|
||||||
|
te.inventory.setStackInSlot(0, message.stack);
|
||||||
|
te.lastChangeTime = message.lastChangeTime;
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly for networking, we'll add one more packet: `PacketRequestUpdatePedestal`. This packet is sent from the client to the server when the client loads the tile entity and needs to get its data from the server. This packet will be fairly similar to the previous one, so I won't go over it in as much detail.
|
||||||
|
|
||||||
|
This packet has a position and a dimension ID. (This one needs a dimension ID unlike the previous one because this will be received on the server which has multiple worlds, and we need a way to determine which one to use, whereas the previous packet was received on the client which only ever has one world.) These are serialized/deserialized as you'd expect.
|
||||||
|
|
||||||
|
The handler class, however, has a slight difference. Unlike the `PacketUpdatePedestal`, this packet has a response packet. So for the generic types we'll use `PacketRequestUpdatePedestal` and `PacketUpdatePedestal`. In the `onMessage` method, we'll call `FMLCommonHandler.instance().getMinecraftServerInstance()` to obtain the instance of `MinecraftServer`, which stores all the worlds. On that instance we'll call `worldServerForDimension` with the dimension from the packet to obtain the `World` instance. We then get the tile entity and return a new `PacketUpdatePedestal` from it which is sent back to the client.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class PacketRequestUpdatePedestal implements IMessage {
|
||||||
|
|
||||||
|
private BlockPos pos;
|
||||||
|
private int dimension;
|
||||||
|
|
||||||
|
public PacketRequestUpdatePedestal(BlockPos pos, int dimension) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.dimension = dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketRequestUpdatePedestal(TileEntityPedestal te) {
|
||||||
|
this(te.getPos(), te.getWorld().provider.getDimension());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketRequestUpdatePedestal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toBytes(ByteBuf buf) {
|
||||||
|
buf.writeLong(pos.toLong());
|
||||||
|
buf.writeInt(dimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fromBytes(ByteBuf buf) {
|
||||||
|
pos = BlockPos.fromLong(buf.readLong());
|
||||||
|
dimension = buf.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Handler implements IMessageHandler<PacketRequestUpdatePedestal, PacketUpdatePedestal> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PacketUpdatePedestal onMessage(PacketRequestUpdatePedestal message, MessageContext ctx) {
|
||||||
|
World world = FMLCommonHandler.instance().getMinecraftServerInstance().worldServerForDimension(message.dimension);
|
||||||
|
TileEntityPedestal te = (TileEntityPedestal)world.getTileEntity(message.pos);
|
||||||
|
if (te != null) {
|
||||||
|
return new PacketUpdatePedestal(te);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** See the [official Forge documentation](http://mcforge.readthedocs.io/en/latest/networking/simpleimpl/) for more information about the SimpleImpl networking system.
|
||||||
|
|
||||||
|
## `TileEntitySpecialRenderer`
|
||||||
|
|
||||||
|
Now that we've updated the tile entity and finished all the networking code, we can finally write the renderer itself.
|
||||||
|
|
||||||
|
Let's create a class called `TESRPedestal` in our `block.pedestal` package that extends `TileEntitySpecialRenderer`. The generic type paramter is the type of our tile entity, so we'll use `TileEntityPedestal`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
|
||||||
|
|
||||||
|
public class TESRPedestal extends TileEntitySpecialRenderer<TileEntityPedestal> {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll overide the `renderTileEntityAt` method. First, we'll get the stored stack from the tile entity, and then, if the stack isn't empty, setup the GL state, render the stack, and reset the GL state.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TESRPedestal extends TileEntitySpecialRenderer<TileEntityPedestal> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderTileEntityAt(TileEntityPedestal te, double x, double y, double z, float partialTicks, int destroyStage) {
|
||||||
|
ItemStack stack = te.inventory.getStackInSlot(0);
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
GlStateManager.enableRescaleNormal();
|
||||||
|
GlStateManager.alphaFunc(GL11.GL_GREATER, 0.1f);
|
||||||
|
GlStateManager.enableBlend();
|
||||||
|
RenderHelper.enableStandardItemLighting();
|
||||||
|
GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, 1, 0);
|
||||||
|
GlStateManager.pushMatrix();
|
||||||
|
double offset = Math.sin((te.getWorld().getTotalWorldTime() - te.lastChangeTime + partialTicks) / 8) / 4.0;
|
||||||
|
GlStateManager.translate(x + 0.5, y + 1.25 + offset, z + 0.5);
|
||||||
|
GlStateManager.rotate((te.getWorld().getTotalWorldTime() + partialTicks) * 4, 0, 1, 0);
|
||||||
|
|
||||||
|
IBakedModel model = Minecraft.getMinecraft().getRenderItem().getItemModelWithOverrides(stack, te.getWorld(), null);
|
||||||
|
model = ForgeHooksClient.handleCameraTransforms(model, ItemCameraTransforms.TransformType.GROUND, false);
|
||||||
|
|
||||||
|
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE);
|
||||||
|
Minecraft.getMinecraft().getRenderItem().renderItem(stack, model);
|
||||||
|
|
||||||
|
GlStateManager.popMatrix();
|
||||||
|
GlStateManager.disableRescaleNormal();
|
||||||
|
GlStateManager.disableBlend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After setting up the GL state (lighting, blending, etc.), we perform the translation (`GlStateManager.translate`) and rotation (`GlStateManager.rotate`).
|
||||||
|
|
||||||
|
There are two parts to the translation: we translate it to `x + 0.5`, `y + 1.25`, and `z + 0.5` which is above the center of our block, on top of the pedestal. The second part is the part that changes: the `offset` in the `y` value. The offset is the height of the item for any given frame. We recalculate this each time because we want it ot be animating bouncing up and down. We calculate this by:
|
||||||
|
|
||||||
|
1. Taking the time (in ticks) since the pedestal was modified by subtracting the `lastChangeTime` from the current total world time.
|
||||||
|
2. Adding the partial ticks. (The partial ticks is a fractional value representing the amount of time that's passed between the last full tick and now. We use this because otherwise the animation would be jittery because there are fewer ticks per second than frames per second.)
|
||||||
|
3. Dividing that by 8 to slow the movement down.
|
||||||
|
4. Taking the sine of that to produce a value that's oscillating back and forth.
|
||||||
|
5. Dividing that by 4 to compress the sine wave vertically so the item doesn't move up and down as much.
|
||||||
|
|
||||||
|
Nextly, we perform the rotation. For this, we take the total world time and add `partialTicks` to it, and multiply it by 4. Unlike for the translation, we don't use a sine wave because we want the item to rotate at a fixed speed. With a sine wave, the rate of rotation would change and it would look rather weird. The last three parameters to the `GlStateManager.rotate` is the vector about which it will be rotated. Because we want to rotate it horizontally we use the vector along the Y-axis: (0, 1, 0).
|
||||||
|
|
||||||
|
Now, that the GL state is all setup the way we want it, we can actually render the model. We call `getItemModelWithOverrides` on the `RenderItem` instance obtained from `Minecraft.getMinecraft()` with parameters for the `ItemStack` to be rendered, the `World` it'll be rendered in, and `null` for the entity parameter to indicate that there is no entity. This gives the `IBakedModel` instance for the `ItemStack`.
|
||||||
|
|
||||||
|
`IBakedModel` is asort of "compiled" representation of a model. It has all of the data from the JSON model (or another source) compressed down into a list of `BakedQuad`s that can be passed directly to OpenGL to be rendered.
|
||||||
|
|
||||||
|
Now that we've got the `IBakedModel` instance, we call `ForgeHooksClient.handleCameraTransforms` with some parameters: the model that it should handle the transformations for, the type of transformations that should be applied (in this case, `TransformType.GROUND` because on the ground is the closest to what we want because we're rendering it in the world), and `false` for the last parameter because we are not rendering the item in the left hand.
|
||||||
|
|
||||||
|
The `handleCameraTransforms` method handles everything necessary for one of Forge's features: `IPerspectiveAwareModel`. This is an extension of the `IBakedModel` interface which allows the model to be overridden depending on how it's being rendered and provide custom transformations from code. If the model we've gotten from `getItemModelWithOverriddes` is an `IPerspectiveAwareModel`, Forge will call the correct method to get the transformation for the given type (in this case, `GROUND`) and apply those transformations to the current GL state.
|
||||||
|
|
||||||
|
Now that we've got the model, we call `bindTexture` on the `TextureManager` instance obtained from `Minecraft.getMinecraft().getTextureManager()` with the ID of the texture we want to bind. In this case, because we want to be bind the main texture map which contains all of the textures that are used in the models, we use the ID `TextureMap.LOCATION_BLOCKS_TEXTURE`. This field name is a bit of a misnomer because it's not just for blocks, it stores the textures for items as well.
|
||||||
|
|
||||||
|
With the texture map boudn, we can finally render the item itself. We call `renderItem` with the `ItemStack` we're rendering and the `IBakedModel` to render on the `RenderItem` instance.
|
||||||
|
|
||||||
|
Once we've finished rendering, we reset the GL state back to what it was before our TESR started rendering.
|
||||||
|
|
||||||
|
Lastly, we'll need to register our TESR. Let's create a new method in our proxiescalled `registerRenderers`. In our `CommonProxy` this method won't do anything because we only want to register our renderers on the client side. In our `ClientProxy` we'll bind our TESR to our tile entity so that it gets rendered.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class CommonProxy {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public void registerRenderers() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ClientProxy extends CommonProxy {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerRenderers() {
|
||||||
|
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityPedestal.class, new TESRPedestal());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We call `bindTileEntitySpecialRenderer` to bind a new instance of our `TESRPedestal` to the `TileEntityPedestal` class. This way, for every instance of the `TileEntityPedestal` class that's in the world, the `renderTileEntityAt` method of our TESR will be called.
|
||||||
|
|
||||||
|
Lastly, we'll update our main mod class to call `registerRenderers` on our proxy:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
proxy.registerRenderers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now that we've finally got all the code done, we can launch Minecraft and take a look at how our Pedestals render:
|
||||||
|
|
||||||
|
![Items in Pedestals Rendering In-World](https://fat.gfycat.com/MenacingRipeCod.gif)
|
|
@ -0,0 +1,76 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Food"
|
||||||
|
metadata.date = "2016-08-12 18:04:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
We've already made a Corn item for our [crop](/tutorials/forge-modding-1102/crops/), however, we were unable to eat the corn (defeating its very purpose). Let's make our Corn behave as actual food.
|
||||||
|
|
||||||
|
First we'll need to create an `ItemCorn` class that will be our new corn item, instead of just using `ItemOre`. Our class will extend `ItemFood` so it inherits all of the vanilla food-handling logic. We'll also want our class to implement `ItemModelProvider` and `ItemOreDict` so it retains the functionality from the existing corn item.
|
||||||
|
|
||||||
|
The `ItemFood` constructor takes 3 parameters:
|
||||||
|
|
||||||
|
1. The amount of hunger restored by this food.
|
||||||
|
2. The saturation given by this food.
|
||||||
|
3. If this food is edible by wolves.
|
||||||
|
|
||||||
|
We'll pass in `3`, `0.6f`, and `false` for the hunger, saturation, and wolf food values, the same values as the Carrot. Also in the constructor, we'll call `setUnlocalizedName` and `setRegistryName` with the same value as we used for the original corn item (`corn`). We'll also call `setCreativeTab` with our custom creative tab.
|
||||||
|
|
||||||
|
We'll also need to override the `registerItemModel` and `initOreDict` methods from the interfaces we implemented.
|
||||||
|
|
||||||
|
In `registerItemModel`, we'll use our proxy `registerItemRenderer` method to register an item model for our corn item. We'll use `corn` as the model name, the same as our original item.
|
||||||
|
|
||||||
|
We'll also override `initOreDict` and call the `OreDictionary.registerOre` method with `cropCorn` as the ore name, the same as our original item.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemFood;
|
||||||
|
import net.minecraftforge.oredict.OreDictionary;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemCorn extends ItemFood implements ItemModelProvider, ItemOreDict {
|
||||||
|
|
||||||
|
public ItemCorn() {
|
||||||
|
super(3, 0.6f, false);
|
||||||
|
setUnlocalizedName("corn");
|
||||||
|
setRegistryName("corn");
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, "corn");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initOreDict() {
|
||||||
|
OreDictionary.registerOre("cropCorn", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `ModItems` class, we'll also need to change the `corn` field.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemCorn corn;
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
corn = register(new ItemCorn());
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We simply need to change the `corn` field to by of item `ItemCorn` and the registration call to instantiate `ItemCorn` instead of `ItemOre`.
|
||||||
|
|
||||||
|
Now we've got an edible corn item!
|
||||||
|
|
||||||
|
![Edible Corn](http://i.imgur.com/aT5BZ5x.png)
|
|
@ -0,0 +1,206 @@
|
||||||
|
```
|
||||||
|
metadata.title = "JSON Block Models"
|
||||||
|
metadata.date = "2016-08-08 13:58:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
We're going to add a new block that has a custom JSON model (that is, one defined completely by us, not one of Mojang's).
|
||||||
|
|
||||||
|
The first thing we'll need to do is create a block class. We need to create a new class instead of just using the `BlockBase` class because w'll need to override a couple of methods to have the model render properly. Our `BlockPedestal` class will extend our `BlockBase` class so we can use the code we've already written for item model registration.
|
||||||
|
|
||||||
|
The two methods we'll be override are `isOpaqueCube` and `isFullCube`. In both of these methods, we'll want to return false from both of these methods in order to change some of the default Minecraft behavior.
|
||||||
|
|
||||||
|
`isOpaqueCube` is used to determine if this block should cull faces of the adjacent block. Since our block doesn't take up the entirety of the 1m^3 cube, we'll want to return `false` so the faces of adjacent blocks can be seen behind our block.
|
||||||
|
|
||||||
|
`isFullCube` is used to determine if light should pass through the block. Once again, we'll want to return `false` because our block is less than 1m^3 so we'll want light to propgate through it.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.block.state.IBlockState;
|
||||||
|
|
||||||
|
public class BlockPedestal extends BlockBase {
|
||||||
|
|
||||||
|
public BlockPedestal() {
|
||||||
|
super(Material.ROCK, "pedestal");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public boolean isOpaqueCube(IBlockState state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public boolean isFullCube(IBlockState state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to register our pedestal block in our `ModBlocks` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
pedestal = register(new BlockPedestal());
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to add a localization for the pedestal block!
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Blocks
|
||||||
|
# ...
|
||||||
|
tile.pedestal.name=Pedestal
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we'll need to create a blockstate file at `assets/tutorial/blockstates/pedestal.json` that tells Forge which model to use for the normal variant and the inventory variant.
|
||||||
|
|
||||||
|
**Note:** See [Basic Forge Blockstates](/tutorials/forge-modding-1102/basic-forge-blockstates/) for more information about the Forge blockstate format.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"variants": {
|
||||||
|
"normal": {
|
||||||
|
"model": "tutorial:pedestal"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"model": "tutorial:pedestal",
|
||||||
|
"transform": "forge:default-block"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our blockstate file does a couple of things.
|
||||||
|
|
||||||
|
1. It instructs Forge to use the model at `assets/tutorial/models/block/pedestal.json` for both the `normal` and `inventory` variants.
|
||||||
|
2. It uses the `forge:default-block` transformation for the inventory variant. This makes the block appear at the proper angle in the inventory and in the hand.
|
||||||
|
|
||||||
|
Now we need to create the Pedestal model itself. The model will be located at `assets/tutorial/models/block/pedestal.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"textures": {
|
||||||
|
"pedestal": "blocks/stonebrick",
|
||||||
|
"particle": "blocks/stonebrick"
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"from": [3, 0, 3],
|
||||||
|
"to": [13, 11, 13],
|
||||||
|
"faces": {
|
||||||
|
"down": {
|
||||||
|
"uv": [0, 0, 10, 10],
|
||||||
|
"texture": "#pedestal",
|
||||||
|
"cullface": "down"
|
||||||
|
},
|
||||||
|
"north": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"uv": [0, 0, 10, 11],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": [2, 11, 2],
|
||||||
|
"to": [14, 12, 14],
|
||||||
|
"faces": {
|
||||||
|
"down": {
|
||||||
|
"uv": [0, 0, 12, 12],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"north": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"uv": [0, 0, 12, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": [1, 12, 1],
|
||||||
|
"to": [15, 13, 15],
|
||||||
|
"faces": {
|
||||||
|
"down": {
|
||||||
|
"uv": [0, 0, 14, 14],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"uv": [0, 0, 14, 14],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"north": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"south": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"west": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
},
|
||||||
|
"east": {
|
||||||
|
"uv": [0, 0, 14, 1],
|
||||||
|
"texture": "#pedestal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The model has two primary parts, the `textures` and the `elements`.
|
||||||
|
|
||||||
|
The `textures` section contains a map of texture name to the location of the texture itself. We define the `pedestal` texture as `blocks/stonebrick` and then reference it using `#pedestal` in the face `texture` attribute. (The `particle` texture is used by Minecraft to generate the block breaking particle.)
|
||||||
|
|
||||||
|
The `elements` section is an array of the elements in the model.
|
||||||
|
|
||||||
|
Each element has 3 properties:
|
||||||
|
|
||||||
|
1. `from`: This is the bottom/left/backmost point of the element.
|
||||||
|
2. `to`: This is the top/right/frontmost point of the elemnt. With the `from` property, this is used to determine the size of the element.
|
||||||
|
3. `faces`: This is an object containg a map of directions to faces. All the faces are optional.
|
||||||
|
|
||||||
|
Each face has several properties:
|
||||||
|
|
||||||
|
1. `texture`: This is the texture to use for the face. This can be a reference to a predefined texture (e.g. `#pedestal`) or a direct reference (e.g. `blocks/stonebrick`).
|
||||||
|
2. `uv`: This is an array of 4 integer elements representing the minimum U, mimumin V, maximum U, and maximum V (in that order).
|
||||||
|
3. `cullface`: This is optional. If specified, this face will be culled if there is a solid block against the specified face of the block.
|
||||||
|
|
||||||
|
![Finished Pedestal Model](http://i.imgur.com/Axt5iiE.png)
|
|
@ -0,0 +1,36 @@
|
||||||
|
```
|
||||||
|
metadata.title = "JSON Item Models"
|
||||||
|
metadata.date = "2016-05-07 16:32:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Models for items and blocks are created using Mojang's fairly simple JSON format. We're going to create a simple item model for our copper ingot.
|
||||||
|
|
||||||
|
Before we do this, we'll need to add our copper ingot texture. Download the copper ingot texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/ingot_copper.png) and save it to `src/main/resources/assets/tutorial/textures/items/ingot_copper.png` so we can use it from our model.
|
||||||
|
|
||||||
|
|
||||||
|
Now we'll create a simple JSON model and save it to `src/main/resources/assets/tutorial/models/item/ingot_copper.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/ingot_copper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's go over what each bit does:
|
||||||
|
|
||||||
|
- `parent`: specifies which model to use as the parent. We're using `item/generated` which is a code model that is part of Minecraft that generates that nice 3D look that items have.
|
||||||
|
- `textures`: specifies all the textures to use for this model.
|
||||||
|
- `layer0`: Our model only has one texture so we only need to specify one layer.
|
||||||
|
- `tutorial:items/ingotCopper`: There are two important parts of this bit.
|
||||||
|
- `tutorial` specifies that the texture is part of the `tutorial` domain.
|
||||||
|
- `items/ingotCopper` specifies what path the texture is at.
|
||||||
|
- These are all combined to get the entire path to the texture `assets/tutorial/textures/items/ingot_copper.png`
|
||||||
|
|
||||||
|
Now our item has a nice texture and nice model in-game!
|
||||||
|
|
||||||
|
![Copper Ingot Model/Texture Screenshot](http://i.imgur.com/cup7xwW.png)
|
|
@ -0,0 +1,29 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Localization"
|
||||||
|
metadata.date = "2016-05-08 09:15:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you'll recalled, we used the `setUnlocalizedName` method in both our `BlockBase` and `ItemBase` classes. The name that we passed into that method is what Minecraft uses when localizing the name of our block or item for the currently active language.
|
||||||
|
|
||||||
|
In these tutorials, we are only going to add English localizations however you can easily add more localizations by following the same pattern.
|
||||||
|
|
||||||
|
Language files are located at `src/main/resources/assets/tutorial/lang/IDENTIFIER.lang` where `IDENTIFIER` is the locale code of the language. Let's create a localization file with the identifier `en_US` (see [here](http://minecraft.gamepedia.com/Language) for more locale codes).
|
||||||
|
|
||||||
|
Language files are written in a simple `key=value` format with one entry per line. The `value` is obviously the translated name, this obviously differs for every language file. The `key` is the key that Minecraft uses when translating things. This is slightly different for blocks and items. For blocks the key is `tile.UNLOCALIZED.name`. For items the key is `item.UNLOCALIZED.name`. Where `UNLOCALIZED` is what we passed into `setUnlocalizedName`.
|
||||||
|
|
||||||
|
**Note:** Lines starting with `#` are comments and won't be parsed.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
item.ingot_copper.name=Copper Ingot
|
||||||
|
|
||||||
|
# Blocks
|
||||||
|
tile.ore_copper.name=Copper Ore
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, both our Copper Ore and Copper Ingot have properly localized names!
|
||||||
|
|
||||||
|
![Copper Ore Screenshot](http://i.imgur.com/f6T09kI.png)
|
||||||
|
![Copper Ingot Screenshot](http://i.imgur.com/oafpj5q.png)
|
|
@ -0,0 +1,50 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Main Mod Class"
|
||||||
|
metadata.date = "2016-05-07 15:27:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Every mod has a main mod class that Forge loads and uses as a starting point when it runs your mod. Before getting started, you'll want to delete all the existing code that comes in the MDK by deleting the `com.example.examplemod` package. For this tutorial, I'll be putting all of the code in the `net.shadowfacts.tutorial` package, so you'll need to create that in your IDE. Next, create a class called `TutorialMod`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial;
|
||||||
|
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
|
||||||
|
import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
|
||||||
|
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
|
||||||
|
|
||||||
|
@Mod(modid = TutorialMod.modId, name = TutorialMod.name, version = TutorialMod.version)
|
||||||
|
public class TutorialMod {
|
||||||
|
|
||||||
|
public static final String modId = "tutorial";
|
||||||
|
public static final String name = "Tutorial Mod";
|
||||||
|
public static final String version = "1.0.0";
|
||||||
|
|
||||||
|
@Mod.Instance(modId)
|
||||||
|
public static TutorialMod instance;
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
System.out.println(name + " is loading!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void init(FMLInitializationEvent event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void postInit(FMLPostInitializationEvent event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if we run the Minecraft Client through IDEA, `Tutorial Mod is loading!` should be printed out in the console. Now that we've got some code that's actually running, let's take a look at what it does.
|
||||||
|
|
||||||
|
- `@Mod(...)` (L8): This marks our `TutorialMod` class as a main mod class so that Forge will load it.
|
||||||
|
- `@Mod.Instance(modId)` (L15-L16): The `@Mod.Instance` annotation marks this field so that Forge will inject the instance of our mod that is used into it. This will become more important later when we're working with GUIs.
|
||||||
|
- `@Mod.EventHandler` methods (L15, L20, L25): This annotation marks our `preInit`, `init`, and `postInit` methods to be called by Forge. Forge determines which method to call for which lifecycle event by checking the parameter of the method, so these methods can be named anything you want.
|
|
@ -0,0 +1,188 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Ore Dictionary"
|
||||||
|
metadata.date = "2016-08-08 11:28:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Forge's Ore Dictionary system provides an API that modders can use to mark items/blocks as equivalent to one another. This was originally created because multiple mods were all adding their own versions of the same ores and ingots (copper, tin, etc.). The way this system works is each `ItemStack` as a list of `String` ore names associated with it.
|
||||||
|
|
||||||
|
Let's create an `ItemOreDict` interface in the `item` package of our mod. This interface will be used to mark our items/blocks to be registered with the Ore Dictionary. This interface will have a single abstract method called `initOreDict` that performs the registration.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
public interface ItemOreDict {
|
||||||
|
|
||||||
|
void initOreDict();
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also create a `ItemOre` class that extends `ItemBase` and implements `ItemOreDict` to give us a nice fully implemented class for ore-dictionaried items.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraftforge.oredict.OreDictionary;
|
||||||
|
|
||||||
|
public class ItemOre extends ItemBase implements ItemOreDict {
|
||||||
|
|
||||||
|
private String oreName;
|
||||||
|
|
||||||
|
public ItemOre(String name, String oreName) {
|
||||||
|
super(name);
|
||||||
|
|
||||||
|
this.oreName = oreName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initOreDict() {
|
||||||
|
OreDictionary.registerOre(oreName, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This class simply takes a second `String` parameter in its constructor that is the ore-dictionary name and then uses that in the `initOreDict` method.
|
||||||
|
|
||||||
|
We'll do something similar for our `BlockOre` class, that is, have it implement `ItemOreDict` and `initOreDict` and have a second parameter for the ore dictionary name.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraftforge.oredict.OreDictionary;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemOreDict;
|
||||||
|
|
||||||
|
public class BlockOre extends BlockBase implements ItemOreDict {
|
||||||
|
|
||||||
|
private String oreName;
|
||||||
|
|
||||||
|
public BlockOre(String name, String oreName) {
|
||||||
|
super(Material.ROCK, name);
|
||||||
|
|
||||||
|
this.oreName = oreName;
|
||||||
|
|
||||||
|
setHardness(3f);
|
||||||
|
setResistance(5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initOreDict() {
|
||||||
|
OreDictionary.registerOre(oreName, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockOre setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll need to make some changes to our `ModItems` and `ModBlocks` classes so they call the `initOreDict` method after the item/block is registered with the `GameRegistry`.
|
||||||
|
|
||||||
|
We'll first check if the item implements our `ItemOreDict` interface (because not all our items will use the Ore Dictionary) and if so, call the `initOreDict` method on it.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private static <T extends Item> T register(T item) {
|
||||||
|
GameRegistry.register(item);
|
||||||
|
|
||||||
|
if (item instanceof ItemModelProvider) {
|
||||||
|
((ItemModelProvider)item).registerItemModel(item);
|
||||||
|
}
|
||||||
|
if (item instanceof ItemOreDict) {
|
||||||
|
((ItemOreDict)item).initOreDict();
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll do this similarly in the `ModBlocks` class excpet we'll check `instanceof ItemOreDict` and call `initOreDict` on both the block itself and the associated `ItemBlock`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block, ItemBlock itemBlock) {
|
||||||
|
GameRegistry.register(block);
|
||||||
|
if (itemBlock != null) {
|
||||||
|
GameRegistry.register(itemBlock);
|
||||||
|
|
||||||
|
if (block instanceof ItemModelProvider) {
|
||||||
|
((ItemModelProvider)block).registerItemModel(itemBlock);
|
||||||
|
}
|
||||||
|
if (block instanceof ItemOreDict) {
|
||||||
|
((ItemOreDict)block).initOreDict();
|
||||||
|
}
|
||||||
|
if (itemBlock instanceof ItemOreDict) {
|
||||||
|
((ItemOreDict)itemBlock).initOreDict();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got all our base classes setup, we're going to modify some of our items and blocks to given the ore dictionary names!
|
||||||
|
|
||||||
|
The only block that will have an ore dictionary name is the Copper Ore block. Following with the conventions for ore dictionary names (if you look in the `OreDictionary` class, you can get a general idea for what these conventions are), our Copper Ore block will have an ore dictionary name of `oreCopper`.
|
||||||
|
|
||||||
|
We'll simply change our registration call for the Copper Ore block to have a second parameter that is also `"oreCopper"`, telling the `BlockOre` class to use `oreCopper` as the ore dictionary name for that block.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
oreCopper = register(new BlockOre("ore_copper", "oreCopper"));
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll now change both our Copper Ingot and Corn items to have ore dictionary names `ingotCopper` and `cropCorn` respectively. All this requires is changing the `ItemBase` instantiations to `ItemOre` instantiations and passing in the desired ore dictionary name as the second constructor parameter.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static void init() {
|
||||||
|
ingotCopper = register(new ItemOre("ingot_copper", "ingotCopper"));
|
||||||
|
corn = register(new ItemOre("corn", "cropCorn"));
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recipes
|
||||||
|
Now that we've got ore dictionary names for some of our items and blocks, let's add recipes that utilize them. Forge adds the `ShapedOreRecipe` and `ShapelessOreRecipe` classes that are identical to the vanilla shaped and shapless recipes, except instead of just accepting an item/block/stack for the input, they can also accept a string of an ore dictionary name that will match anything with that given name.
|
||||||
|
|
||||||
|
These recipes need to be instantiated manually and registered using `GameRegistry.addRecipe` unlike normal shaped/shapeless recipes which have convienience methods in `GameRegistry`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModRecipes {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
GameRegistry.addRecipe(new ShapedOreRecipe(Items.BUCKET, "I I", " I ", 'I', "ingotCopper"));
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This recipe is the same as the vanilla bucket recipe, except it matches any item with the `ingotCopper` ore dictionary name instead of just iron ingots.
|
||||||
|
|
||||||
|
![Shaped Ore Recipe](http://i.imgur.com/OICDDTJ.png)
|
|
@ -0,0 +1,61 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Overview"
|
||||||
|
metadata.date = "2016-05-06 10:00:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### About
|
||||||
|
This series of tutorials teaches modding [Minecraft](https://minecraft.net) version 1.11.2 using [Forge](http://minecraftforge.net).
|
||||||
|
|
||||||
|
**This tutorial series does not teach Java. You should already know Java before you try to mod Minecraft.**
|
||||||
|
|
||||||
|
### Tutorials
|
||||||
|
- [Setting up the Development Environment](/tutorials/forge-modding-1112/workspace-setup/)
|
||||||
|
- [Main Mod Class](/tutorials/forge-modding-1112/main-mod-class/)
|
||||||
|
- [Proxy System](/tutorials/forge-modding-1112/proxy-system/)
|
||||||
|
- [Basic Items](/tutorials/forge-modding-1112/basic-items/)
|
||||||
|
- [JSON Item Models](/tutorials/forge-modding-1112/json-item-models/)
|
||||||
|
- [Basic Blocks](/tutorials/forge-modding-1112/basic-blocks/)
|
||||||
|
- [Basic Forge Blockstates](/tutorials/forge-modding-1112/basic-forge-blockstates/)
|
||||||
|
- [Localization](/tutorials/forge-modding-1112/localization/)
|
||||||
|
- [Crops](/tutorials/forge-modding-1112/crops/)
|
||||||
|
- [Creative Tabs](/tutorials/forge-modding-1112/creative-tabs/)
|
||||||
|
- [Advanced Creative Tabs](/tutorials/forge-modding-1112/advanced-creative-tabs/)
|
||||||
|
- [Crafting/Smelting Recipes](/tutorials/forge-modding-1112/crafting-smelting-recipes/)
|
||||||
|
- [Ore Dictionary](/tutorials/forge-modding-1112/ore-dictionary/)
|
||||||
|
- [JSON Block Models](/tutorials/forge-modding-1112/json-block-models/)
|
||||||
|
- [Food](/tutorials/forge-modding-1112/food/)
|
||||||
|
- [Tools](/tutorials/forge-modding-1112/tools/)
|
||||||
|
- [Armor](/tutorials/forge-modding-1112/armor/)
|
||||||
|
- [World Generation: Ore](/tutorials/forge-modding-1112/world-generation-ore/)
|
||||||
|
- _Interlude:_ [Updating to 1.11](/tutorials/forge-modding-1112/updating-to-1112/)
|
||||||
|
- World Generation: Tree
|
||||||
|
- World Generation: Structure
|
||||||
|
- mcmod.info
|
||||||
|
- [Tile Entities](/tutorials/forge-modding-1112/tile-entities/)
|
||||||
|
- [Tile Entities with Inventory](/tutorials/forge-modding-1112/tile-entities-inventory/)
|
||||||
|
- [Tile Entities with Inventory GUI](/tutorials/forge-modding-1112/tile-entities-inventory-gui/)
|
||||||
|
- [Dynamic Tile Entity Rendering](/tutorials/forge-modding-1112/dynamic-tile-entity-rendering/)
|
||||||
|
- Advanced GUIs with Widgets
|
||||||
|
- Energy API - RF - Items
|
||||||
|
- Energy API - RF - Blocks
|
||||||
|
- Energy API - RF - GUI Widget
|
||||||
|
- Energy API - Tesla - Items
|
||||||
|
- Energy API - Tesla - Blocks
|
||||||
|
- Energy API - Tesla - GUI Widget
|
||||||
|
- Energy API - Forge Energy - Items
|
||||||
|
- Energy API - Forge Energy - Blocks
|
||||||
|
- Energy API - Forge Energy - GUI Widget
|
||||||
|
- Configuration
|
||||||
|
- Packets and Packet Handlers
|
||||||
|
- Event Handling
|
||||||
|
- Keybindings
|
||||||
|
- Commands
|
||||||
|
|
||||||
|
### Other Resources
|
||||||
|
- [Forge Forum](http://minecraftforge.net/)
|
||||||
|
- [Forge Docs](https://mcforge.readthedocs.io/en/latest/)
|
||||||
|
- #minecraftforge ([esper](https://esper.net)) IRC
|
||||||
|
- [TheGreyGhost's Blog](http://greyminecraftcoder.blogspot.com.au/p/list-of-topics.html)
|
||||||
|
- [MinecraftByExample](https://github.com/TheGreyGhost/MinecraftByExample)
|
|
@ -0,0 +1,20 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Proxy System"
|
||||||
|
metadata.date = "2016-05-07 15:41:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Minecraft (and therefore Forge) are split up between the client and the server, so certain things can only be done on the client. Because some classes only exist on the client, we'll be using Forge's proxy system to access those classes without having to worry about crashes on dedicated servers.
|
||||||
|
|
||||||
|
We'll need to create a pair of classes to act as the proxy. One will contain code common to both sides, and the other will contain client-specific code. Create two new classes, `CommonProxy` and `ClientProxy` (I usually use a `proxy` package to keep this contained from the rest of the code) and have `ClientProxy` extend `CommonProxy`. You'll see specifically how we'll be using this proxy structure when we get to custom items and blocks.
|
||||||
|
|
||||||
|
In order to have Forge load the correct proxy class for the correct side, we'll need to use the `@SidedProxy` annotation. Add this (replacing my package with yours) to your main mod class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SidedProxy(serverSide = "net.shadowfacts.tutorial.proxy.CommonProxy", clientSide = "net.shadowfacts.tutorial.proxy.ClientProxy")
|
||||||
|
public static CommonProxy proxy;
|
||||||
|
```
|
||||||
|
|
||||||
|
At runtime, Forge will detect which side our mod is running on and inject the correct proxy into our `proxy` field using reflection.
|
|
@ -0,0 +1,327 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tile Entities with Inventory GUI"
|
||||||
|
metadata.date = "2017-03-29 18:58:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Now that we've got the inventory for our tile entity working, let's add a GUI so people can easily see what's inside of it.
|
||||||
|
|
||||||
|
Of course, in Minecraft, GUIs only exist on the client-side. This poses a problem because inventory's can only be interacted with from the server-side. In order to handle this, we use something called a Container which bridges the gap between the client and the server. There are two identical instances of the `Container` subclass that exist on both the client and the server. Whenever a change is made on the server, the change is automatically sent to the client, and vice versa: whenever a change is made on the client, it's automatically sent to the server.
|
||||||
|
|
||||||
|
## Container
|
||||||
|
|
||||||
|
First off, we'll create our container class. We'll create a new class called `ContainerPedestal` in the `block.pedestal` package of our mod that extends Minecraft's `Container` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.inventory.*;
|
||||||
|
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will cause an error because we need to implement the abstract method `canInteractWith` from the `Container` class. This method determines if a given player can open the container. For our pedestal, we don't have any special restrictions so we'll just return true regardless of the player.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canInteractWith(EntityPlayer player) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll override the `transferStackInSlot` method from `Container`. This method handle's when a player tries to quick-transfer a stack by shift-clicking it. The implementation of this method is copied from [my library mod](https://github.com/shadowfacts/ShadowMC/blob/1.11/src/main/java/net/shadowfacts/shadowmc/inventory/ContainerBase.java) which is designed to work with any container regardless of the number of slots. This differs from the vanilla implementations of this method are dependent on the number of slots being a specific number. This method is fairly complicated and not that important so I won't go over super in-detail here. The gist of it is that it tries to add the stack that's been shift-clicked into the opposite inventory leaving anything that can't be transferred in the slot that was shift-clicked.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack transferStackInSlot(EntityPlayer player, int index) {
|
||||||
|
ItemStack itemstack = ItemStack.EMPTY;
|
||||||
|
Slot slot = inventorySlots.get(index);
|
||||||
|
|
||||||
|
if (slot != null && slot.getHasStack()) {
|
||||||
|
ItemStack itemstack1 = slot.getStack();
|
||||||
|
itemstack = itemstack1.copy();
|
||||||
|
|
||||||
|
int containerSlots = inventorySlots.size() - player.inventory.mainInventory.size();
|
||||||
|
|
||||||
|
if (index < containerSlots) {
|
||||||
|
if (!this.mergeItemStack(itemstack1, containerSlots, inventorySlots.size(), true)) {
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
} else if (!this.mergeItemStack(itemstack1, 0, containerSlots, false)) {
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemstack1.getCount() == 0) {
|
||||||
|
slot.putStack(ItemStack.EMPTY);
|
||||||
|
} else {
|
||||||
|
slot.onSlotChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemstack1.getCount() == itemstack.getCount()) {
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.onTake(player, itemstack1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemstack;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll add the most important part of the container, our constructor. In the constructor, we'll add all the slots for the player inventory and the slot for the pedestal's inventory. The container stores a `List` of `Slot` objects, each of which has a position on the GUI to render at and represents one inventory slot (either from the player's inventory or anything else). Minecraft's built in `Slot` class expects an `IInventory` but obviously, our pedestal doesn't use `IInventory`, it uses Forge's `IItemHandler` capability. In order to have containers still work with `IItemHandler`s, Forge provides a `SlotItemHandler` which takes an `IItemHandler` and creates a dummy `IInventory` object and overrides all the necessary methods to use the `IItemHandler`. We'll have a single one of these slots for our pedestal's inventory. We'll also have 36 normal slots which are for the player's inventory. We'll use some for loops add in all 36 of the slots at the correct positions so we don't have to type them all out.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ContainerPedestal extends Container {
|
||||||
|
|
||||||
|
public ContainerPedestal(InventoryPlayer playerInv, final TileEntityPedestal pedestal) {
|
||||||
|
IItemHandler inventory = pedestal.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.NORTH);
|
||||||
|
addSlotToContainer(new SlotItemHandler(inventory, 0, 80, 35) {
|
||||||
|
@Override
|
||||||
|
public void onSlotChanged() {
|
||||||
|
pedestal.markDirty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
for (int j = 0; j < 9; j++) {
|
||||||
|
addSlotToContainer(new Slot(playerInv, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int k = 0; k < 9; k++) {
|
||||||
|
addSlotToContainer(new Slot(playerInv, k, 8 + k * 18, 142));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One important thing to note is how in our `SlotItemHandler` instance, we override the `onSlotChanged` method to call `markDirty` on our pedestal. This makes it so that when the contents of the slot changes, the tile entity will be marked as dirty so Minecraft knows it needs to be saved to disk. If we were using a normal `Slot` instead of a `SlotItemHandler`, this wouldn't be necessary because `IInventory` has a `markDirty` method that the slot can call. However, because we're using Forge's `IItemHandler` which obeys separation of concerns, no equivalent method exists, meaning we need to handle it ourselves.
|
||||||
|
|
||||||
|
## GUI
|
||||||
|
|
||||||
|
Now that we've finished our container class, we need to make the client-side GUI itself. We'll create a new class called `GuiPedestal` that extends `GuiContainer` in our `block.pedestal` package.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.GuiContainer;
|
||||||
|
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll have a constructor that takes a `Container` and a `InventoryPlayer`. It will pass the container to the super-constructor so that `GuiContainer` can render our slots and handle interaction with them. It will also save the `InventoryPlayer` to a field for later.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ..
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
private InventoryPlayer playerInv;
|
||||||
|
|
||||||
|
public GuiPedestal(Container container, InventoryPlayer playerInv) {
|
||||||
|
super(container);
|
||||||
|
this.playerInv = playerInv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight%}
|
||||||
|
|
||||||
|
Before we can move on to the next method, we'll add a `private static final` to store the `ResourceLocation` for the background texture of the GUI.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
private static final ResourceLocation BG_TEXTURE = new ResourceLocation(TutorialMod.modId, "textures/gui/pedestal.png");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for the pedestal GUI [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/519b5e5265c6203997d9f378ec5eaccbc33a6870/src/main/resources/assets/tutorial/textures/gui/pedestal.png). You'll want to save it to `src/main/resources/assets/tutorial/textures/gui/pedestal.png`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
In our `GuiPedestal` class, we'll need to override two methods: `drawGuiContainerBackgroundLayer` and `drawGuiContainerForegroundLayer`. These two methods respectively handle rendering the background (the stuff that renders _behind_ the slots) and rendering the foreground (the stuff that renders _in front of_ the background and the slots).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Firstly, the background method:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class GuiPedestal extends GuiContainer {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) {
|
||||||
|
GlStateManager.color(1, 1, 1, 1);
|
||||||
|
mc.getTextureManager().bindTexture(BG_TEXTURE);
|
||||||
|
int x = (width - xSize) / 2;
|
||||||
|
int y = (height - ySize) / 2;
|
||||||
|
drawTexturedModalRect(x, y, 0, 0, xSize, ySize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In our `drawGuiContainerBackgroundLayer` method, we:
|
||||||
|
|
||||||
|
1. Call `GlStateManager.color(1, 1, 1, 1)`. This resets the GL color to solid white, instead of potentially something else. If we don't reset it, and the color isn't already white, our texture would be tinted with that color.
|
||||||
|
2. Call `bindTexture(BG_TEXTURE)`. This binds the background texture that we've specified in our `BG_TEXTURE` field to Minecraft's rendering engine, so that when we render a rectangle with a texture on it, the correct texture is used.
|
||||||
|
3. Calculate the X and Y positions to draw our texture at. We want our texture to be centered on screen, so we take half the width and height of the screen and subtract half the x-size and y-size of our GUI from it, giving us the position of the upper left hand corner of the GUI, which is the position the texture should be drawn at.
|
||||||
|
4. Call `drawTexturedModalRect`. This actually draws the texture. We pass in:
|
||||||
|
1. The `x` and `y` positions.
|
||||||
|
2. The point (0, 0) for the UV position. This is where on the texture rendering should start from. Because in our image, the GUI is at the top left corner, we use (0, 0).
|
||||||
|
3. The x-size and the y-size for the dimensions of the drawn texture.
|
||||||
|
|
||||||
|
Lastly for our GUI, we override the `drawGuiContainerForegroundLayer` method:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class GuiPedestal {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) {
|
||||||
|
String name = I18n.format(ModBlocks.pedestal.getUnlocalizedName() + ".name");
|
||||||
|
fontRenderer.drawString(name, xSize / 2 - fontRenderer.getStringWidth(name) / 2, 6, 0x404040);
|
||||||
|
fontRenderer.drawString(playerInv.getDisplayName().getUnformattedText(), 8, ySize - 94, 0x404040);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this method, we:
|
||||||
|
|
||||||
|
1. Call `I18n.format` with our pedestal block's unlocalized name. This converts the unlocalized name of our block (`tile.pedestal.name`) into the correct name for the current locale as specified in our localization files. For English, this will be `Pedestal`.
|
||||||
|
2. Draw the localized name of our block on the screen. We draw it at the top center of our GUI, so we subtract half of width of our localized name (as calculated by `getStringWidth`) from half of the x-size of the GUI, giving use the X position that will result in it being centered in the GUI. We also pass 6 as the Y coordinate, so 6 pixels from the top of the GUI, and `0x404040` as the color, in hexadecimal, for our string.
|
||||||
|
3. Draw the localized name of the player's inventory on the screen. We call `playerInv.getDisplayName()` which returns an `ITextComponent` and call `getUnformattedText()` on it to get the string to render. We draw it at X position 8, just offset from the left side of our GUI, and at the Y position which is 94 pixels (the height of the player's inventory in our GUI) above the bottom of our GUI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## GUI Handler
|
||||||
|
Now that we've got our container and GUI classes finished, we need to add a GUI handler. This class will have methods which are called by Forge that will be responsible for creating the correct instances of our GUI and container classes from some pieces of information.
|
||||||
|
|
||||||
|
We'll create a class called `ModGuiHandler` that implements the `IGuiHandler` interface in our root mod package.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial;
|
||||||
|
|
||||||
|
import net.minecraftforge.fml.common.network.IGuiHandler;
|
||||||
|
|
||||||
|
public class ModGuiHandler implements IGuiHandler {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First off, we'll add a constant field to our GUI handler for the Pedestal's GUI ID. Forge uses integer IDs to differentiate between which GUI should be opened, and since this is our first GUI, its ID will be `0`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModGuiHandler implements IGuiHandler {
|
||||||
|
public static final int PEDESTAL = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll implement the `getServerGuiElement` method. This method returns the appropriate instance (or `null`, if there is none) for the given ID, player, world, and position.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModGuiHandler implements IGuiHandler {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Container getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
|
||||||
|
switch (ID) {
|
||||||
|
case PEDESTAL:
|
||||||
|
return new ContainerPedestal(player.inventory, (TileEntityPedestal)world.getTileEntity(new BlockPos(x, y, z)));
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** We've changed the return type of our `getServerGuiElement` method to `Container` from `Object`. Forge uses `Object` because client-only classes aren't present on servers, so there can't be any references to them in the signatures of methods that will exist on both sides. Forge also uses this logic for the container method, but because `Container` is present on both sides, we can change the return type to `Container` from `Object` without issue.
|
||||||
|
|
||||||
|
In this method, we switch over the GUI ID, and if it's the pedestal's ID, return a new `ContainerPedestal` instance using the player's inventory, and the `TileEntityPedestal` that's in the world. Otherwise, if the ID doesn't match any of the one's we've added (this should never happen, but it's necessary just to satisfy the Java compiler), we return `null`.
|
||||||
|
|
||||||
|
Nextly, we add the `getClientGuiElement` method which returns the appropriate `GuiScreen` instance given the same data as the `getServerGuiElement`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
|
||||||
|
switch (ID) {
|
||||||
|
case PEDESTAL:
|
||||||
|
return new GuiPedestal(getServerGuiElement(ID, player, world, x, y, z), player.inventory);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly to the `getServerGuiElement` method, we switch on the ID, and if it's the pedestal's, we return a new instance of `GuiPedestal` with a new container intance and the player's inventory.
|
||||||
|
|
||||||
|
Finally, we need to register our GUI handler.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
@Mod.EventHandler
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
NetworkRegistry.INSTANCE.registerGuiHandler(this, new ModGuiHandler());
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This tells Forge that our GUI handler instance corresponds to our mod instance, so it knows which GUI handler to use when we actually open our GUI.
|
||||||
|
|
||||||
|
## Updating the Block
|
||||||
|
Lastly, in order to open the GUI, we need to modify our Block's `onBlockActivated` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
// ...
|
||||||
|
@Override
|
||||||
|
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
// ...
|
||||||
|
if (!player.isSneaking()) {
|
||||||
|
// ...
|
||||||
|
} else {
|
||||||
|
player.openGui(TutorialMod.instance, ModGuiHandler.PEDESTAL, world, pos.getX(), pos.getY(), pos.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll remove the code that prints messages to chat and replace it with a call to `player.openGui` with our mod instance, the Pedestal's GUI ID, the world, and the positions which get passed into our GUI handler to open the GUI.
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now that we've finished, we can launch Minecraft, and once we shift right-click on the pedestal block, we can see and interact with our GUI:
|
||||||
|
|
||||||
|
![Pedestal GUI](http://i.imgur.com/0ajo2b2.png)
|
|
@ -0,0 +1,239 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tile Entities with Inventory"
|
||||||
|
metadata.date = "2016-11-27 14:25:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've learned the basics of making tile entities, let's make a more complicated one that has an inventory.
|
||||||
|
|
||||||
|
**Note:** If you haven't already completed the [tile entities tutorial](/tutorials/forge-modding-111/tile-entities/), you'll want to do that so you'll have the foundations that this tutorial builds on.
|
||||||
|
|
||||||
|
## The Block
|
||||||
|
|
||||||
|
Firstly, we'll move the `BlockPedestal` from the `block` package to the `block.pedestal` package. Next, we'll change `BlockPedestal` so it extends `BlockTileEntity` instead of `BlockBase`. We'll also add a generic type parameter of `TileEntityPedestal`, which will be the tile entity class for our pedestal. Next, we'll need to implement the abstract methods provided by `BlockTileEntity` (`getTileEntityClass` and `createTileEntity`):
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<TileEntityPedestal> getTileEntityClass() {
|
||||||
|
return TileEntityPedestal.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public TileEntityPedestal createTileEntity(World world, IBlockState state) {
|
||||||
|
return new TileEntityPedestal();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From the `getTileEntityClass` method, we'll return `TileEntityPedestal.class` (this will cause errors because we haven't creeated the tile entity class yet) and from the `createTileEntity` method, we'll return a new instance of the `TileEntityPedestal` class.
|
||||||
|
|
||||||
|
Next, we'll add the `onBlockActivated` method which will handle our block being right-clicked. The logic for this method will be something like this:
|
||||||
|
|
||||||
|
1. Check that we're running on the server (see the [Sides section](/tutorials/forge-modding-111/tile-entities/#sides) of the previous tutorial).
|
||||||
|
1. Retrieve the `TileEntity` and the `IItemHandler` instance.
|
||||||
|
2. If the player is sneaking:
|
||||||
|
1. If the player's hand is empty:
|
||||||
|
1. Take what's in the pedestal's `IItemHandler` and put it in the player's hand.
|
||||||
|
2. Otherwise:
|
||||||
|
1. Take what's in the player's hand and attempt to insert it into the pedestal
|
||||||
|
3. Mark the tile entity as dirty so Minecraft knows it needs to be saved to disk.
|
||||||
|
3. Otherwise:
|
||||||
|
1. Retrieve the `ItemStack` currently in the pedestal
|
||||||
|
2. If there is a stack (i.e. it `!stack.isEmpty()`)
|
||||||
|
1. Send a chat message to the player with count and name of the item.
|
||||||
|
3. Otherwise
|
||||||
|
1. Send a chat message to the player telling them that the pedestal's empty
|
||||||
|
2. Return `true`
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
TileEntityPedestal tile = getTileEntity(world, pos);
|
||||||
|
IItemHandler itemHandler = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, side);
|
||||||
|
if (!player.isSneaking()) {
|
||||||
|
if (heldItem.isEmpty()) {
|
||||||
|
player.setHeldItem(hand, itemHandler.extractItem(0, 64, false));
|
||||||
|
} else {
|
||||||
|
player.setHeldItem(hand, itemHandler.insertItem(0, heldItem, false));
|
||||||
|
}
|
||||||
|
tile.markDirty();
|
||||||
|
} else {
|
||||||
|
ItemStack stack = itemHandler.getStackInSlot(0);
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
String localized = TutorialMod.proxy.localize(stack.getUnlocalizedName() + ".name");
|
||||||
|
player.addChatMessage(new TextComponentString(stack.getCount() + "x " + localized));
|
||||||
|
} else {
|
||||||
|
player.addChatMessage(new TextComponentString("Empty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `IItemHandler` and capability stuff might look a bit confusing, but that's ok, it will be explained in more detail later on. For now, suffice it to say that the `IItemHandler` is the object that stores the pedestal's inventory.
|
||||||
|
|
||||||
|
Before we can continue, we'll also need to add a new method to our proxy class. This method will take an unlocalized name (e.g. `item.diamond.name`) and translate it into the correct version (e.g. `Diamond`). This needs to be a method in our proxy class because there are two different ways of localizing things depending if you're on the client or the server. If you're on the server, you need to use `net.minecraft.util.text.translation.I18n` whereas if you're on the client, you need to use `net.minecraft.client.resources.I18n`. In our `CommonProxy` class, we'll add the server-side version of this:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
import net.minecraft.util.text.translation.I18n;
|
||||||
|
|
||||||
|
public class CommonProxy {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public String localize(String unlocalized, Object... args) {
|
||||||
|
return I18n.translateToLocalFormatted(unlocalized, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the `ClientProxy`, we'll add the client-side version of this:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
import net.minecraft.client.resources.I18n;
|
||||||
|
|
||||||
|
public class ClientProxy extends CommonProxy {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String localize(String unlocalized, Object... args) {
|
||||||
|
return I18n.format(unlocalized, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The very last thing we'll need to add to our block class is the `breakBlock` method. This method is called when our block is destroyed in the world, and we'll use it to drop the contents of the pedestal's inventory.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void breakBlock(World world, BlockPos pos, IBlockState state) {
|
||||||
|
TileEntityPedestal tile = getTileEntity(world, pos);
|
||||||
|
IItemHandler itemHandler = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.NORTH);
|
||||||
|
ItemStack stack = itemHandler.getStackInSlot(0);
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
EntityItem item = new EntityItem(world, pos.getX(), pos.getY(), pos.getZ(), stack);
|
||||||
|
world.spawnEntityInWorld(item);
|
||||||
|
}
|
||||||
|
super.breakBlock(world, pos, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the break block method, we'll:
|
||||||
|
|
||||||
|
1. Get the tile entity instance, the `IItemHandler` instance, and the `ItemStack` stored in the inventory.
|
||||||
|
2. If there is a stack (i.e. `!stack.isEmpty()`):
|
||||||
|
1. Create a new `EntityItem` instance at the correct position with the stack
|
||||||
|
2. Spawn the entity in the world so the item is dropped
|
||||||
|
3. Call the `super.breakBlock` method to remove our block and tile entity from the world.
|
||||||
|
|
||||||
|
## The Tile Entity
|
||||||
|
|
||||||
|
Like in the previous tutorial, the tile entity class itself will be fairly simple. This is possible because of Forge's `IItemHandler` capability and its `ItemStackHandler` class which handles all the logic for storing items, reading/writing them to/from NBT, and inserting/extracting items.
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
Forge provides a simple Entity Component System called capabilities. Capabilities allow mod developers to easily add/use functionality without having to implement lots of interfaces or perform lots of `instanceof` checks and casts. In this tutorial we'll use the Forge-provided `IItemHandler` capability which is a replacement for Vanilla's `IInventory` and `ISidedInventory`. We'll be using the `ItemStackHandler` implementation of the `IItemHandler` interface which is provided by Forge. By overriding the `hasCapability` and `getCapability` methods of our tile entity, we can "regster" the capabilty object and make it accessible to everyone else.
|
||||||
|
|
||||||
|
The `IItemHandler` interface provides a couple methods that we can use for interacting with the inventory:
|
||||||
|
|
||||||
|
1. `insertItem`: This method takes 3 parameters: `int slot`, `ItemStack stack`, and `boolean simulate` and returns an `ItemStack`.
|
||||||
|
1. `int slot`: The index of the slot in the inventory that we want to insert into.
|
||||||
|
2. `ItemStack stack`: The stack that we are attempting to insert.
|
||||||
|
3. `boolean simulate`: If true, no modification of the `IItemHandler`'s internal inventory will be performed. This is useful if you want to test if an interaction can be performed.
|
||||||
|
4. `ItemStack` return: The remainder of the stack that could not be inserted. If the stack was fully inserted, this will be `ItemStack.EMPTY`.
|
||||||
|
2. `extractItem`: This method takes 3 parameters: `int slot`, `int amount`, `boolean simulate` and returns an `ItemStack`.
|
||||||
|
1. `int slot`: The index of the slot in the inventory that we want to extract from.
|
||||||
|
2. `int amount`: The amount of items we want to extract from the slot.
|
||||||
|
3. `boolean simulate`: If true, no modification of the `IItemHandler`'s internal inventory will be performed. This is useful if you want to test if an interaction can be performed.
|
||||||
|
4. `ItemStack` return: The stack that was extracted from the inventory.
|
||||||
|
|
||||||
|
**Note:** If you want to know more about capabilities, you can checkout the [official Forge documentation](http://mcforge.readthedocs.io/en/latest/datastorage/capabilities/) on the subject.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.pedestal;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.NBTTagCompound;
|
||||||
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
import net.minecraft.util.EnumFacing;
|
||||||
|
import net.minecraftforge.common.capabilities.Capability;
|
||||||
|
import net.minecraftforge.items.CapabilityItemHandler;
|
||||||
|
import net.minecraftforge.items.ItemStackHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class TileEntityPedestal extends TileEntity {
|
||||||
|
|
||||||
|
private ItemStackHandler inventory = new ItemStackHandler(1);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||||
|
compound.setTag("inventory", inventory.serializeNBT());
|
||||||
|
return super.writeToNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFromNBT(NBTTagCompound compound) {
|
||||||
|
inventory.deserializeNBT(compound.getCompoundTag("inventory"));
|
||||||
|
super.readFromNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasCapability(Capability<?> capability, @Nullable EnumFacing facing) {
|
||||||
|
return capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY || super.hasCapability(capability, facing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <T> T getCapability(Capability<T> capability, @Nullable EnumFacing facing) {
|
||||||
|
return capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY ? (T)inventory : super.getCapability(capability, facing);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In our tile entity, we'll have a `private ItemStackHandler inventory` field which is initialized to a `new ItemStackHandler(1)`. The first parameter of the `ItemStackHandler` constructor is the number of slots it should have. In our case, this is 1 because the pedestal can only hold 1 stack at a time.
|
||||||
|
|
||||||
|
`ItemStackHandler` also provides `serializeNBT` and `deserializeNBT` methods making it very easy to save our inventory. In the `writeToNBT` method, we'll call `inventory.serializeNBT()` to create an `NBTTagCompound` that represents the inventory and set that to the key `inventory` on the root compound. Similarly, in the `readFromNBT` method, we'll retrieve the tag compound that has the key `inventory` and pass it to `inventory.deserializeNBT` so that the items that were saved to NBT are loaded back into our `ItemStackHandler` object.
|
||||||
|
|
||||||
|
Lastly, we'll override the `hasCapability` and `getCapability` methods. In `hasCapability` we'll return if the capability being tested is the `IItemHandler` capability instance, or if it's provided by the super method*. Likewise, in the `getCapability` method, we'll check if the capability being requested is the `IItemHandler` capability and if so, return our `inventory`, and otherwise, delegate to the super method\*.
|
||||||
|
|
||||||
|
*: We delegate back to the super method because Forge provides an `AttachCapabilitiesEvent` which allows other mods to add capabilites to tile entities and other objects that they don't own.
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now we can launch Minecraft and see how our pedestal can now store an item in its inventory:
|
||||||
|
|
||||||
|
![Pedestal Gif](https://zippy.gfycat.com/DescriptiveWhoppingBangeltiger.gif)
|
|
@ -0,0 +1,266 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tile Entities"
|
||||||
|
metadata.date = "2016-11-27 12:32:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
In Minecraft, the `Block` class is used to represent a _type_ of block, not a single block in the world. The `Block` instance has the properties for every single instance of your block that exists. If we want to have data that is unique to an instance of a block in the world, we need to use a `TileEntity`.
|
||||||
|
|
||||||
|
A common myth that exists in the world of modding is that tile entities are bad, especially for performance. **This is not true.** Tile entities can be bad for performance if they're implemented poorly, just like anything else, but they are not bad just by virtue of existing.
|
||||||
|
|
||||||
|
There are two varieties of tile entities: _ticking_ and _non-ticking_. Ticking tile entities, as the name implies, are updated (or ticked) every single game tick (usually 20 times per second). Tick tile entities are the more performance intensive kind because they're updated so frequently and as such, need to be written carefully. Non-ticking tile entities on the other hand don't tick at all, they exist simply for storing data. In this tutorial we'll be making a non-ticking tile entity. Ticking tile entities we'll get to later.
|
||||||
|
|
||||||
|
## Helper Stuff
|
||||||
|
|
||||||
|
Before we create the tile entity, we'll add some more code to our mod that will make it easier to add more tile entities in the future.
|
||||||
|
|
||||||
|
Firstly, we'll create a `BlockTileEntity` class in our `block` package.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.block.state.IBlockState;
|
||||||
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.IBlockAccess;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public abstract class BlockTileEntity<TE extends TileEntity> extends BlockBase {
|
||||||
|
|
||||||
|
public BlockTileEntity(Material material, String name) {
|
||||||
|
super(material, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Class<TE> getTileEntityClass();
|
||||||
|
|
||||||
|
public TE getTileEntity(IBlockAccess world, BlockPos pos) {
|
||||||
|
return (TE)world.getTileEntity(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTileEntity(IBlockState state) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public abstract TE createTileEntity(World world, IBlockState state);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `BockTileEntity` class wil provide a couple of things:
|
||||||
|
|
||||||
|
1. It will extend `BlockBase` so we still have access to all our existing helpers.
|
||||||
|
2. It will have a generic parameter `TE` which will be the type of our tile entity class. This will be used to create a simple helper method to reduce the numebr of casts necessary to obtain the instance of our tile entity for a specific position in the world and to ensure that the `TileEntity` we create is of the correct type for our block instance.
|
||||||
|
3. It will override the `hasTileEntity(IBlockState)` method from Minecraft's `Block` class to return `true`. This will tell Minecraft that our block has a tile entity associated with it that needs to be created.
|
||||||
|
4. It will have two abstract methods:
|
||||||
|
1. `getTileEntityClass`: From here, we'll return the `Class` that our tile entity is so it can automatically be registered when our block is registered.
|
||||||
|
2. `createTileEntity`: This is a more specific version of the `Block` class' `createTileEntity`. This method will be called by Minecraft whenever it needs to create a new instance of our tile entity, like when our block has been placed.
|
||||||
|
|
||||||
|
Nextly, we'll add another check to the `register` method in our `ModBlocks` class. This will check if the block being registered is an instance of our `BlockTileEntity` class and if so, register the tile entity class that's associated with the block to the name of the block. (This tile entity registration is necessary so that Minecraft knows which class to create for a tile entity that's been saved to and reloaded from the disk.)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private static <T extends Block> T register(T block, ItemBlock itemBlock) {
|
||||||
|
GameRegistry.register(block);
|
||||||
|
// ...
|
||||||
|
|
||||||
|
if (block instanceof BlockTileEntity) {
|
||||||
|
GameRegistry.registerTileEntity(((BlockTileEntity<?>)block).getTileEntityClass(), block.getRegistryName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Block
|
||||||
|
|
||||||
|
Now that we've got all the necessary helpers out of the way it's time to create the block.
|
||||||
|
|
||||||
|
We'll create a new class called `BlockCounter` in the `block.counter` package of our mod. This class will be block class that extends `BlockTileEntity`. (This class will have some errors because we haven't created the tile entity class itself yet.)
|
||||||
|
|
||||||
|
```java
|
||||||
|
|
||||||
|
package net.shadowfacts.tutorial.block.counter;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.block.state.IBlockState;
|
||||||
|
import net.minecraft.entity.player.EntityPlayer;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.util.EnumFacing;
|
||||||
|
import net.minecraft.util.EnumHand;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.util.text.TextComponentString;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import net.shadowfacts.tutorial.block.BlockTileEntity;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class BlockCounter extends BlockTileEntity<TileEntityCounter> {
|
||||||
|
|
||||||
|
public BlockCounter() {
|
||||||
|
super(Material.ROCK, "counter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {
|
||||||
|
if (!world.isRemote) {
|
||||||
|
TileEntityCounter tile = getTileEntity(world, pos);
|
||||||
|
if (side == EnumFacing.DOWN) {
|
||||||
|
tile.decrementCount();
|
||||||
|
} else if (side == EnumFacing.UP) {
|
||||||
|
tile.incrementCount();
|
||||||
|
}
|
||||||
|
player.addChatMessage(new TextComponentString("Count: " + tile.getCount()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<TileEntityCounter> getTileEntityClass() {
|
||||||
|
return TileEntityCounter.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public TileEntityCounter createTileEntity(World world, IBlockState state) {
|
||||||
|
return new TileEntityCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our block class will extend `BlockTileEntity` and have a generic parameter of `TileEntityCounter` because that's the type of tile entity that belongs to this block.
|
||||||
|
|
||||||
|
In the constructor, we'll simply call the super constructor with the material `ROCK` and the name `"counter"`.
|
||||||
|
|
||||||
|
In the `getTileEntityClass` method, we'll return `TileEntityCounter.class` (this wil cause an error, but don't worry, we haven't created this class yet). This will allow our `ModBlocks` class to automatically register our `TileEntityCounter.class` to the name `tutorial:counter`.
|
||||||
|
|
||||||
|
In the `createTileEntity` class, we'll simply return a new instance of our `TileEntityCounter ` class.
|
||||||
|
|
||||||
|
Lastly, and most importantly, in the `onBlockActivated` method, which is called when our block is right-clicked, we'll do a number of things:
|
||||||
|
|
||||||
|
1. Check that we're operating on the server[*](#sides).
|
||||||
|
1. Retrieve the `TileEntityCounter` instance.
|
||||||
|
2. If the player hit the bottom side:
|
||||||
|
1. Decrement the counter.
|
||||||
|
3. Or if the player hit the top side:
|
||||||
|
1. Increment the counter.
|
||||||
|
4. Send a chat message to the player with the current value of the counter.
|
||||||
|
2. Return true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2 id="sides">Sides</h2>
|
||||||
|
|
||||||
|
As I mentioned above, before we modify the counter, we check that we're on the server. We need to do this because the Minecraft client and the server are completely separated and some methods are called on both.
|
||||||
|
|
||||||
|
In a multiplayer scenario, there are multiple clients connect to one server. In this case, the distinction between client and server is fairly obvious, but in a single player scenario, things get more complicated. In a multiplayer scenario, the servger that everybody's connecting to is referred to as the **physical server** and all of the individual clients are the **physical clients**.
|
||||||
|
|
||||||
|
In a single player world, the client and the server are still decoupled, even though they are running on the same computer (and even in the same JVM, just on different threads). In singleplayer, the client connects to a local, private server that functions very similarly to a physical server. In this case, the server thread is referred to as the **logical server** and the client thread as the **logical client** because both logical sides are running on the same physical side.
|
||||||
|
|
||||||
|
The `World.isRemote` field is used to check which logical side we're operating on (be it logical or physical). The field is `true` for the physical client in a multiplayer scenario and for the logical client in a single-player scenario. The reverse is also true. The field is `false` for the physical server in a multiplayer scenario and for the logical server in the single-player scenario. So by checking `!world.isRemote`, we ensure that the code inside the `if` statement will only be run on the server (be it logical or physical).
|
||||||
|
|
||||||
|
If you want to know more about sides in Minecraft and how they work, you can see [here](http://mcforge.readthedocs.io/en/latest/concepts/sides/) for the official Forge documentation.
|
||||||
|
|
||||||
|
## The `TileEntity`
|
||||||
|
|
||||||
|
Now that our block is finished, we can finally create the tile entity itself.
|
||||||
|
|
||||||
|
We'll create a new class called `TileEntityCounter` which will also reside in the `block.counter` package of our mod (this is my preferred package structure, however, many people also prefer to have all the tile entity classes reside in a separete `tileentity` package and the block classes reside in the `block` package).
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block.counter;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.NBTTagCompound;
|
||||||
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
|
||||||
|
public class TileEntityCounter extends TileEntity {
|
||||||
|
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||||
|
compound.setInteger("count", count);
|
||||||
|
return super.writeToNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFromNBT(NBTTagCompound compound) {
|
||||||
|
count = compound.getInteger("count");
|
||||||
|
super.readFromNBT(compound);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementCount() {
|
||||||
|
count++;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decrementCount() {
|
||||||
|
count--;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `TileEntityCounter` class is fairly simple. It will:
|
||||||
|
|
||||||
|
- Extend Minecraft's `TileEntity` class so Minecraft knows what to do with it.
|
||||||
|
- Have a private `int count` field which will store the value of the counter.
|
||||||
|
- Override the `writeToNBT` and `readFromNBT` methods so Minecraft is able to properly save and load it from the disk.
|
||||||
|
- Provide `getCount`, `incrementCount`, and `decrementCount` methods for accessing and modifying the value of the field.
|
||||||
|
|
||||||
|
Additionally, in the `incrementCount` and `decrementCount` methods, we call the `markDirty` method from the Vanilla `TilEntity` class. This method call tells Mincraft that our TE has changed since it was last saved to disk and therefore must be re-saved.
|
||||||
|
|
||||||
|
### The NBT (Named Binary Tag) Format
|
||||||
|
|
||||||
|
NBT is a format for storing all types of data into a key/value tree structure that can easily be serialized to bytes and saved to the disk. You can read more about the internal structure of the NBT format [here](http://wiki.vg/NBT). You can look at the `NBTTagCompound` class in Minecraft to see all the types of things that can be stored. Vanilla code is also a good example of how to store more complex things in NBT.
|
||||||
|
|
||||||
|
In this case, we'll store our `count` integer field with the `count` key in the `NBTTagCompound` in the `writeToNBT` method and read it back from the tag compound in the `readFromNBT` method.
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
Lastly, we'll need to add our counter to our `ModBlocks` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
public static BlockCounter counter;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
counter = register(new BlockCounter());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finished
|
||||||
|
|
||||||
|
Now that we've got everything done, we can run Minecraft, grab one of our counters from our creative tab, place it, and see how the counter changes when the top and bottom of the block are right-clicked.
|
||||||
|
|
||||||
|
![Click on the top](http://i.imgur.com/zD1x2m0.png)
|
||||||
|
|
||||||
|
![Click on the bottom](http://i.imgur.com/UCqVJSI.png)
|
|
@ -0,0 +1,412 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Tools"
|
||||||
|
metadata.date = "2016-08-14 15:04:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's make some copper tools!
|
||||||
|
|
||||||
|
First we'll need to create a tool material for our new tools to use. We'll use Forge's `EnumHelper` class to add a value to the Minecraft `Item.ToolMaterial` enum.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static final Item.ToolMaterial copperToolMaterial = EnumHelper.addToolMaterial("COPPER", 2, 500, 6, 2, 14);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each tool is going to be quite similar, so feel free to skip ahead to the one you want.
|
||||||
|
|
||||||
|
- [Sword](#sword)
|
||||||
|
- [Pickaxe](#pickaxe)
|
||||||
|
- [Axe](#axe)
|
||||||
|
- [Shovel](#shovel)
|
||||||
|
- [Hoe](#hoe)
|
||||||
|
|
||||||
|
## Sword
|
||||||
|
First we'll create an `ItemSword` class in the `item.tool` package inside our mod package. This class will extend the vanilla `ItemSword` class and implement our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In the constructor, we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the name for use in item model registration
|
||||||
|
|
||||||
|
We'll also override `registerItemModel` and use the stored `name` to register our item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemSword extends net.minecraft.item.ItemSword implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemSword(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper sword to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemSword copperSword;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperSword = register(new ItemSword(TutorialMod.copperToolMaterial, "copper_sword"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also create our JSON item model at `assets/tutorial/models/item/copper_sword.json`. Unlike our other item models, the parent for the model will be `item/handheld` instead of `item/generated`. `item/handheld` provides the transformations used by handheld items, such as tools.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_sword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also need the texture, which you can download [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_sword.png).
|
||||||
|
|
||||||
|
And lastly, we'll add a localization entry for the sword.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copper_sword.name=Copper Sword
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Sword](http://i.imgur.com/ye5yMy4.png)
|
||||||
|
|
||||||
|
## Pickaxe
|
||||||
|
Let's create an `ItemPickaxe` class in the `item.tool` package of our mod. This class will extend the vanilla `ItemPickaxe` and implement our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In our `ItemPickaxe` constructor we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material
|
||||||
|
- Set the unlocalized name and registry names
|
||||||
|
- Store the name for use in the item model registration
|
||||||
|
|
||||||
|
We'll also override `registerItemModel` and use the stored `name` field to register our item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemPickaxe extends net.minecraft.item.ItemPickaxe implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemPickaxe(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper pickaxe to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemPickaxe copperPickaxe;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperPickaxe = register(new ItemPickaxe(TutorialMod.copperToolMaterial, "copper_pickaxe"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll create a JSON model for our item at `assets/tutorial/models/item/copper_pickaxe.json`. This model will have a parent of `item/handheld` instead of `item/generated` so it inherits the transformations for handheld models.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_pickaxe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for the copper pickaxe [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_pickaxe.png).
|
||||||
|
|
||||||
|
Lastly, we'll need a localization entry for the pick.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copper_pickaxe.name=Copper Pickaxe
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Pickaxe](http://i.imgur.com/FsbvVur.png)
|
||||||
|
|
||||||
|
## Axe
|
||||||
|
First off, we'll need an `ItemAxe` class that extends the vanilla `ItemAxe` class and implements our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
If you look at the vanilla `ItemAxe` class, you'll notice that it has two constructors. One of them takes only a `ToolMaterial` whereas the other takes a `ToolMaterial` and two `float`s. Only vanilla `ToolMaterial`s will work with the `ToolMaterial` only constructor, any modded materials will cause an `ArrayIndexOutOfBoundsException` because of the hardcoded values in the `float` arrays in the `ItemAxe` class. Forge provides the secondary constructor that accepts the two `float`s as well, allowing modders to add axes with their own tool materials.
|
||||||
|
|
||||||
|
In the `ItemPickaxe` constructor, we will:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material and the damage and attack speeds used by the vanilla iron axe.
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the name for use in the item model registration
|
||||||
|
|
||||||
|
Additionally, we'll override `registerItemModel` and use the stored `name` to register our model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemAxe extends net.minecraft.item.ItemAxe implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemAxe(ToolMaterial material, String name) {
|
||||||
|
super(material, 8f, -3.1f);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper axe to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemAxe copperAxe;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperAxe = register(new ItemAxe(TutorialMod.copperToolMaterial, "copper_axe"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, we'll need a JSON item model. We'll create it at `assets/tutorials/models/item/copper_axe.json`.
|
||||||
|
|
||||||
|
Our model will have a parent of `item/handheld` instead of `item/generated` so it has the same transformations used by other hand-held items.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_axe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for the copper axe [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/items/copper_axe.png).
|
||||||
|
|
||||||
|
Lastly, we'll need a localization entry for our axe.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copper_axe.name=Copper Axe
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Axe](http://i.imgur.com/5E3vjTo.png)
|
||||||
|
|
||||||
|
## Shovel
|
||||||
|
Firstly we'll create an `ItemShovel` class that extends the vanilla `ItemSpade` class and implements our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In the `ItemShovel` constructor, we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material used
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the name to be used for item model registration
|
||||||
|
|
||||||
|
We'll also need to implement `registerItemModel` and register a item model for our shovel.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemSpade;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemShovel extends ItemSpade implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemShovel(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our copper shovel to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemShovel copperShovel;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperShovel = register(new ItemShovel(TutorialMod.copperToolMaterial, "copper_shovel"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll create a JSON item model for our shovel at `assets/tutorial/models/item/copper_shovel.json`. This model will have a parent of `item/handheld`, unlike our previous item models, so it inherits the transformations used by handheld items.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_shovel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the texture for our shovel [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/tutorial/textures/items/copper_shovel.png).
|
||||||
|
|
||||||
|
We'll also need a localization entry for our shovel.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copper_shovel.name=Copper Shovel
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Shovel](http://i.imgur.com/l1VMi6L.png)
|
||||||
|
|
||||||
|
## Hoe
|
||||||
|
Let's create an `ItemHoe` class that extends the vanilla `ItemHoe` class and implements our `ItemModelProvider` interface.
|
||||||
|
|
||||||
|
In the `ItemHoe` constructor, we'll:
|
||||||
|
|
||||||
|
- Call the `super` constructor with the tool material
|
||||||
|
- Set the unlocalized and registry names
|
||||||
|
- Store the item name to be used for item model registration
|
||||||
|
|
||||||
|
We'll also need to implement `registerItemModel` and register an item model for our hoe.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item.tool;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ItemModelProvider;
|
||||||
|
|
||||||
|
public class ItemHoe extends net.minecraft.item.ItemHoe implements ItemModelProvider {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemHoe(ToolMaterial material, String name) {
|
||||||
|
super(material);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll add our hoe to our `ModItems` class simply by adding a field and initializing it using our `register` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemSword copperHoe;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// ...
|
||||||
|
copperHoe = register(new ItemSword(TutorialMod.copperToolMaterial, "copper_hoe"));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll create a JSON item model for our hoe at `assets/tutorial/models/item/copper_hoe.json`. This model will have a parent of `item/handheld` instead of `item/generated` so it inherits the transformations used by vanilla handheld items.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/handheld",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_hoe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper hoe texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/tutorial/textures/items/copper_hoe.png).
|
||||||
|
|
||||||
|
Lastly, we'll need a localization entry for our hoe.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Items
|
||||||
|
# ...
|
||||||
|
item.copper_hoe.name=Copper Hoe
|
||||||
|
```
|
||||||
|
|
||||||
|
![Copper Hoe](http://i.imgur.com/8PZ3MdD.png)
|
|
@ -0,0 +1,32 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Updating to 1.11.2"
|
||||||
|
metadata.date = "2016-11-27 09:48:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're updating from 1.10.2 to 1.11.2, there are a couple of things you need to be aware of:
|
||||||
|
|
||||||
|
## `ItemStack`s are _never_ `null`
|
||||||
|
Starting in 1.11, `ItemStack`s can _never, ever_ be `null`. Instead of checking for `null`-ness, you use the `ItemStack.isEmpty` method to make sure the stack contains something.
|
||||||
|
|
||||||
|
## `ItemStack` private fields
|
||||||
|
In 1.11, the `ItemStack.stackSize` field was made private. Instead of directly modifying this field like before, there are getter, setter, and mutator methods available.
|
||||||
|
|
||||||
|
- `getCount`: equivalent to simply retrieving the field.
|
||||||
|
- `setCount`: equivalent to setting the field
|
||||||
|
- `grow`: equivalent to increasing the field.
|
||||||
|
`stack.grow(1)` is equivalent to `stack.stackSize++`.
|
||||||
|
- `shrink`: equivalent to decreasing the field.
|
||||||
|
`stack.shrink(1)` is equivalent to `stack.stackSize--`.
|
||||||
|
|
||||||
|
**Note:** The default Forge MDK comes with older MCP mappings in which the new `ItemStack` methods aren't named. You'll need to change the mappings version from `snapshot_20161111` to the latest (`snapshot_20170330` as of this post).
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
In 1.11.2, Minecraft enforces all resource file names being completely lowercase. This practically enforces `snake_case` instead of `camelCase`. You'll need to rename all your resource files (blockstates, models, textures, etc.) and change all the corresponding uses of their old names in code.
|
||||||
|
|
||||||
|
## Other
|
||||||
|
Aside from those major changes, there are a couple of minor things:
|
||||||
|
|
||||||
|
- `CreativeTabs.getTabIconItem` returns an `ItemStack`, not an `Item` now.
|
||||||
|
- Various mapping changes throughout the MC codebase. You can search on [here](https://github.com/ModCoderPack/MCPBot-Issues/issues) to see mapping changes in 1.11.2.
|
|
@ -0,0 +1,50 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Setting up the Development Environment"
|
||||||
|
metadata.date = "2016-05-06 11:16:00 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Java
|
||||||
|
This series does not cover learning Java or installing the JDK. You should have the Java 8 JDK installed already.
|
||||||
|
|
||||||
|
### IntelliJ IDEA
|
||||||
|
I will be using [IntelliJ IDEA](https://jetbrains.com/idea/) throughout this series as it is my IDE of choice. You can download the free community version of IDEA [here](https://www.jetbrains.com/idea/). It is possible to use [Eclipse](https://www.eclipse.org/) if you prefer.
|
||||||
|
|
||||||
|
### Forge MDK
|
||||||
|
From the [Forge files site](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.11.2.html), download the latest MDK for 1.10.2. (Click the button with the floppy disk icon labeled `MDK` on the left.) After download, unzip the MDK to a new folder wherever you like. After unzipping the MDK, we can delete a number of extraneous files that are part of the MDK. You can delete every file in the folder thats not one of these:
|
||||||
|
|
||||||
|
- `src/`
|
||||||
|
- `build.gradle`
|
||||||
|
- `gradle/`
|
||||||
|
- `gradlew`
|
||||||
|
- `gradlew.bat`
|
||||||
|
|
||||||
|
### Gradle
|
||||||
|
Before we setup Forge and IDEA, we need to configure Gradle (the build system Forge mods use) to have more RAM available, otherwise we will not be able to decompile and deobfuscate Minecraft. Open the file at `~/.gradle/gradle.properties` (where `~` is your user directory) and create it if it does not exist. Add this to the file to instruct Gradle to use at most 3 gigabytes of memory:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
org.gradle.jvmargs=-Xmx3G
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll need to make a couple of additions to the `build.gradle` file that is part of the Forge MDK. This will configure IDEA and Gradle to use Java 8 to compile our project, allowing us to use the [shiny new Java 8 features](http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html).
|
||||||
|
|
||||||
|
```properties
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forge
|
||||||
|
Now, to setup Forge and create the IDEA configurations we will need, run this command. (Replace `idea` with `eclipse` if you are using Eclipse and remove the leading `./` if you are using Windows)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew setupDecompWorkspace idea
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This may take a while to run, depending on the speed of your computer.
|
||||||
|
|
||||||
|
Now, if everything ran sucessfully, you should have a file that has the `.ipr` extension in your mod folder. Launch IDEA and after doing so, click the Import Project button and open the `.ipr` file in your mod folder and wait a moment for IDEA to reconfigure itself for the project.
|
||||||
|
|
||||||
|
**Note:** If you have not launched IDEA before, you may need to go through some first time setup options beforehand.
|
||||||
|
|
||||||
|
Now that you've got IDEA setup, check out [how to setup the main mod class](/tutorials/forge-modding-1102/main-mod-class/).
|
|
@ -0,0 +1,146 @@
|
||||||
|
```
|
||||||
|
metadata.title = "World Generation: Ore"
|
||||||
|
metadata.date = "2016-10-10 11:28:42 -0400"
|
||||||
|
metadata.series = "forge-modding-1112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.11.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
We've got our copper ore block, but it doesn't generate in the world so it's not very useful to players. Let's fix that.
|
||||||
|
|
||||||
|
The first thing we'll need to do is create a class called `ModWorldGeneration` in the `world` sub-package in our mod. This class will implement Forge's `IWorldGenerator` interface which is used to hook into Minecraft's world generation.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.world;
|
||||||
|
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import net.minecraft.world.chunk.IChunkGenerator;
|
||||||
|
import net.minecraft.world.chunk.IChunkProvider;
|
||||||
|
import net.minecraftforge.fml.common.IWorldGenerator;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `generate` method is called by Forge for every chunk that's generated and is our entry point into MC's world generation. Inside the `generate` method, we'll check if `world.provider.getDimension() == 0` because we only want our ore to generate in the overworld. If that's true, we'll call a separate method called `generateOverworld` that takes the same parameters as `generate`. In this method we'll have our generation code that's specific to the overworld.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
if (world.provider.getDimension() == 0) { // the overworld
|
||||||
|
generateOverworld(random, chunkX, chunkZ, world, chunkGenerator, chunkProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateOverworld(Random random, int chunkX, int chunkY, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we write the code that will actually add our Copper Ore into the world, let's write a little helper method to make our lives a bit easier.
|
||||||
|
|
||||||
|
This method will take a couple of things:
|
||||||
|
|
||||||
|
1. The `IBlockState` to generate.
|
||||||
|
2. The `World` to generate in.
|
||||||
|
3. The `Random` to use for generation.
|
||||||
|
4. The X and Z positions to generate the block at.
|
||||||
|
5. The minimum and maximum Y positions for which the ore can be generated.
|
||||||
|
6. The size of each ore vein.
|
||||||
|
7. The number of veins per chunk.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private void generateOre(IBlockState ore, World world, Random random, int x, int z, int minY, int maxY, int size, int chances) {
|
||||||
|
int deltaY = maxY - minY;
|
||||||
|
|
||||||
|
for (int i = 0; i < chances; i++) {
|
||||||
|
BlockPos pos = new BlockPos(x + random.nextInt(16), minY + random.nextInt(deltaY), z + random.nextInt(16));
|
||||||
|
|
||||||
|
WorldGenMinable generator = new WorldGenMinable(ore, size);
|
||||||
|
generator.generate(world, random, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This method does a couple of things:
|
||||||
|
|
||||||
|
1. Calculate the difference between the maximum Y and minimum Y values.
|
||||||
|
2. Create a `BlockPos` with X, minimum Y, and Z values passed into the method and offset by:
|
||||||
|
1. A random number from 0 to 15
|
||||||
|
2. A random number from 0 to the difference between the min and max Y values (so that the ore is generated somewhere in between)
|
||||||
|
3. A random number from 0 to 15
|
||||||
|
3. Creates a new `WorldGenMinable` instance.
|
||||||
|
4. Calls the `generate` method on it to generate our ore in the world.
|
||||||
|
5. Repeats steps 2 through 4 `chances` times.
|
||||||
|
|
||||||
|
This results in our ore being generated `chanes` times per chunk with each chance having a different position inside the chunk and in between the specificed Y values.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModWorldGen implements IWorldGenerator {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
private void generateOverworld(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider) {
|
||||||
|
generateOre(ModBlocks.oreCopper.getDefaultState(), world, random, chunkX * 16, chunkZ * 16, 16, 64, 4 + random.nextInt(4), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateOre(IBlockState ore, World world, Random random, int x, int z, int minY, int maxY, int size, int chances) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We call the `generateOre` method with:
|
||||||
|
|
||||||
|
1. The block state we want to generate (the default block state of our copper ore block).
|
||||||
|
2. The world we want generate in (the `World` we've been passed).
|
||||||
|
3. The random we want to use to generate (the `Random` we've been passed).
|
||||||
|
4. The X position we want to generate at (the `chunkX` value multipled by 16, because chunks are 16x16).
|
||||||
|
5. The Z position we want to generate at (the `chunkZ` value multiplied by 16, because chunks are 16x16).
|
||||||
|
6. The minimum Y position we want to generate at (16).
|
||||||
|
7. The maximum Y position we want to generate at (64).
|
||||||
|
8. The size of the vein to generate (a random number from 4 to 7).
|
||||||
|
9. The number of times per chunk to generate (6).
|
||||||
|
|
||||||
|
Lastly, in the `preInit` of our main mod class, we'll need to register our world generator.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public void preInit(FMLPreInitializationEvent event) {
|
||||||
|
// ...
|
||||||
|
GameRegistry.registerWorldGenerator(new ModWorldGen(), 3);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `int` parameter of `GameRegistry.registerWorldGenerator` is the weight of our mod's world generator. This usually doens't matter, however, if you're experiencing issues with other mods interfering with your world generation, you may want to change this.
|
||||||
|
|
||||||
|
Now, if you create a new world and search around for a bit, you'll bet able to find a deposit of our Copper Ore!
|
||||||
|
|
||||||
|
You may want to play around with the vein size and chances settings until you achieve the desired concentration of ore per chunk.
|
||||||
|
|
||||||
|
![Copper Ore generating in the world](http://i.imgur.com/jfeYvi0.png)
|
|
@ -0,0 +1,62 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Advanced Creative Tabs"
|
||||||
|
metadata.date = "2016-06-15 11:42:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Searchable Tab
|
||||||
|
Let's make our creative tab searchable, just like the Search Items tab.
|
||||||
|
|
||||||
|
There are two main parts to this:
|
||||||
|
1. Returning `true` from the `hasSearchBar` method of our creative tab class.
|
||||||
|
2. Setting the texture name for the background image of our creative tab, so the search bar appears.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.client;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class TutorialTab extends CreativeTabs {
|
||||||
|
|
||||||
|
public TutorialTab() {
|
||||||
|
super(TutorialMod.modId);
|
||||||
|
setBackgroundImageName("item_search.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack getTabIconItem() {
|
||||||
|
return new ItemStack(ModItems.ingotCopper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasSearchBar() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we are returning `true` from `hasSearchBar` so Minecraft will allow us to type in our tab and filter the visible items.
|
||||||
|
|
||||||
|
We're also calling `setBackgroundImageName` with `"item_search.png"`. Minecraft will use this string to find the texture to use for the background. It will look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_BACKGROUND_NAME` where `BACKGROUND_NAME` is what you passed into `setBackgroundImageName`. `tag_item_search.png` is provided by Minecraft, so we don't need to do anything else.
|
||||||
|
|
||||||
|
![Searchable Creative Tab](http://i.imgur.com/C34Nh4R.png)
|
||||||
|
|
||||||
|
## Custom Background
|
||||||
|
As explained above, we can use custom backgrounds for our creative tabs.
|
||||||
|
|
||||||
|
> Minecraft will use this string to find the texture to use for the background. It will look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_BACKGROUND_NAME` where `BACKGROUND_NAME` is what you passed into `setBackgroundImageName`.
|
||||||
|
|
||||||
|
By passing a different string into `setBackgroundImageName` and adding the texture into the correct folder of our `src/main/resources` folder, we can use a custom background.
|
||||||
|
|
||||||
|
In our constructor, let's call `setBackgroundImageName` with `"tutorialmod.png"`. This will tell Minecraft to look for the texture at `assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png`
|
||||||
|
|
||||||
|
Download [this](https://raw.githubusercontent.com/shadowfacts/TutorialMod/master/src/main/resources/assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png) texture and save it to `src/main/resources/assets/minecraft/textures/gui/container/creative_inventory/tab_tutorialmod.png` in your mod folder. Make sure it's under `assets/minecraft`, not `assets/tutorial` otherwise the texture won't be where MC's expecting it.
|
||||||
|
|
||||||
|
That's it! When you open up the creative tab, you should now see our nice custom texture!
|
||||||
|
|
||||||
|
![Creative Tab with Custom Background](http://i.imgur.com/pP2W6h0.png)
|
|
@ -0,0 +1,238 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Armor"
|
||||||
|
metadata.date = "2016-09-17 16:43:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we can create armor, we'll need to create an armor material to use for our copper armor.
|
||||||
|
|
||||||
|
We'll add a new field to our main mod class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
public static final ItemArmor.ArmorMaterial copperArmorMaterial = EnumHelper.addArmorMaterial("COPPER", modId + ":copper", 15, new int[]{2, 5, 6, 2}, 9, SoundEvents.ITEM_ARMOR_EQUIP_IRON, 0.0F);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnumHelper.addArmorMaterial` takes a number of parameters:
|
||||||
|
- `"COPPER"`: The name of the new enum value, this is completely capitalized, following the enum naming convention.
|
||||||
|
- `modId + ":copper"`: This is the texture that will be used for our armor. We prefix it with our mod ID to use our mod's domain instead of the default `minecraft` domain.
|
||||||
|
- `15`: The maximum damage factor.
|
||||||
|
- `new int[]{2, 5, 6, 2}`: The damage reduction factors for each armor piece.
|
||||||
|
- `9`: The enchantibility of the armor.
|
||||||
|
- `SoundEvents.ITEM_ARMOR_EQUIP_IRON`: The sound event that is played when the armor is equipped.
|
||||||
|
- `0.0F`: The toughness of the armor.
|
||||||
|
|
||||||
|
Next we'll need the textures for the armor material that are used to render the on-player overlay.
|
||||||
|
Download the layer 1 texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.12/src/main/resources/assets/tutorial/textures/models/armor/copper_layer_1.png) and save it to `src/main/resources/assets/tutorial/textures/models/armor/copper_layer_1.png`. Download the layer 2 texture [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.12/src/main/resources/assets/tutorial/textures/models/armor/copper_layer_2.png) and save it to `src/main/resources/assets/tutorial/textures/models/armor/copper_layer_2.png`.
|
||||||
|
|
||||||
|
## Armor Item Base Class
|
||||||
|
Before we can begin creating armor items, we'll need to create a base class that implements our `ItemModelProvider` interface so it can be used with our registration helper method.
|
||||||
|
|
||||||
|
We'll create a class called `ItemArmor` in our `item` package that extends the Vanilla `ItemArmor` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.inventory.EntityEquipmentSlot;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemArmor extends net.minecraft.item.ItemArmor {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public ItemArmor(ArmorMaterial material, EntityEquipmentSlot slot, String name) {
|
||||||
|
super(material, 0, slot);
|
||||||
|
setRegistryName(name);
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel(Item item) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Helmet
|
||||||
|
Firstly, we'll create a field for our copper helmet item and register it and its item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperHelmet = new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.HEAD, "copper_helmet");
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Item> registry) {
|
||||||
|
registry.registerAll(
|
||||||
|
// ...
|
||||||
|
copperHelemet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
// ...
|
||||||
|
copperHelmet.registerItemModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_helmet.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_helmet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.12/src/main/resources/assets/tutorial/textures/items/copper_helmet.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_helmet.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_helmet.name=Copper Helmet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Chestplate
|
||||||
|
First, we'll create a field for our copper chestplate item and register it and its item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperChestplate = new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.CHEST, "copper_chestplate");
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Item> registry) {
|
||||||
|
registry.registerAll(
|
||||||
|
// ...
|
||||||
|
copperChestplate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
// ...
|
||||||
|
copperChestplate.registerItemModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_chestplate.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_chestplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.12/src/main/resources/assets/tutorial/textures/items/copper_chestplate.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_chestplate.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_chestplate.name=Copper Chestplate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Leggings
|
||||||
|
First, we'll create a field for our copper leggings item and register it and its item model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperLeggings = new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.LEGS, "copper_leggings");
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Item> registry) {
|
||||||
|
registry.registerAll(
|
||||||
|
// ...
|
||||||
|
copperLeggings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
// ...
|
||||||
|
copperLeggings.registerItemModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_leggings.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_leggings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper helmet texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.12/src/main/resources/assets/tutorial/textures/items/copper_leggings.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_leggings.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the helmet.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_leggings.name=Copper Leggings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copper Boots
|
||||||
|
First, we'll create a field for our copper boots item and register it in our `ModItems.init` method.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ModItems {
|
||||||
|
// ...
|
||||||
|
public static ItemArmor copperBoots = new ItemArmor(TutorialMod.copperArmorMaterial, EntityEquipmentSlot.FEET, "copper_boots");
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Item> registry) {
|
||||||
|
registry.registerAll(
|
||||||
|
// ...
|
||||||
|
copperBoots
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
// ...
|
||||||
|
copperBoots.registerItemModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we'll need to create a JSON model for our item. The file will be at `src/main/resources/assets/tutorial/models/item/copper_boots.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parent": "item/generated",
|
||||||
|
"textures": {
|
||||||
|
"layer0": "tutorial:items/copper_boots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can download the copper boots texture from [here](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.12/src/main/resources/assets/tutorial/textures/items/copper_boots.png) and save it to `src/main/resources/assets/tutorial/textures/items/copper_boots.png`.
|
||||||
|
|
||||||
|
Lastly, we'll need to add a localization entry for the boots.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ...
|
||||||
|
item.copper_boots.name=Copper Boots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Done!
|
||||||
|
Now, when we run the game, we can obtain our copper armor from the Combat creative tab, and when we equip it, we can see the player overlay being rendered and the armor value being show on the HUD:
|
||||||
|
|
||||||
|
![copper armor screenshot](https://i.imgur.com/Vv8Qzne.png)
|
|
@ -0,0 +1,192 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Basic Blocks"
|
||||||
|
metadata.date = "2016-05-07 21:45:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
For our first block, we are going to make a Copper Ore to go along with our Copper Ingot.
|
||||||
|
|
||||||
|
### Base Block
|
||||||
|
We're going to do something similar to what we did for [Basic Items](/tutorials/forge-modding-111/basic-items/), create a base class for all of our blocks to extend to make our life a bit easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemBlock;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class BlockBase extends Block {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public BlockBase(Material material, String name) {
|
||||||
|
super(material);
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
setRegistryName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel(Item itemBlock) {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(itemBlock, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Item createItemBlock() {
|
||||||
|
return new ItemBlock(this).setRegistryName(getRegistryName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockBase setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is almost exactly the same as our `ItemBase` class except it extends `Block` instead of `Item`. It sets the unlocalized and registry names, has a method to register the item model, and has an overriden version of `Block#setCreativeTab` that returns a `BlockBase`.
|
||||||
|
|
||||||
|
The one additional method that `BlockBase` has (`createItemBlock`) is added to make dealing with `ItemBlock`s a bit easier. The `ItemBlock` for a given block is what is used as the inventory form of a given block. In the game, when you have a piece of Cobblestone in your inventory, you don't actually have the Cobblestone block in your inventory, you have the Cobblestone _`ItemBlock`_ in your inventory. We'll be using this method when we register our `ItemBlock`s, just to make dealing with them a bit easier.
|
||||||
|
|
||||||
|
We'll also create a `BlockOre` class which extends `BlockBase` to make adding ore's a little easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.material.Material;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
|
||||||
|
public class BlockOre extends BlockBase {
|
||||||
|
|
||||||
|
public BlockOre(String name) {
|
||||||
|
super(Material.ROCK, name);
|
||||||
|
|
||||||
|
setHardness(3f);
|
||||||
|
setResistance(5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockOre setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ModBlocks`
|
||||||
|
|
||||||
|
Now let's create a `ModBlocks` class similar to `ModItems` to assist us when registering blocks.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.block;
|
||||||
|
|
||||||
|
import net.minecraft.block.Block;
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraft.item.ItemBlock;
|
||||||
|
import net.minecraftforge.registries.IForgeRegistry;
|
||||||
|
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Block> registry) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerItemBlocks(IForgeRegistry<Item> registry) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll need to add a new event handler method to our `RegistrationHandler` to call `ModBlocks.register`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventBusSubscriber
|
||||||
|
public static class RegistrationHandler {
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerBlocks(RegistryEvent.Register<Block> event) {
|
||||||
|
ModBlocks.register(event.getRegistry());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also need to update the two previously-created `RegistrationHandler` methods to handle registering our `ItemBlock`s and our block models.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventBusSubscriber
|
||||||
|
public static class RegistrationHandler {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerItems(RegistryEvent.Register<Item> event) {
|
||||||
|
ModItems.register(event.getRegistry());
|
||||||
|
ModBlocks.registerItemBlocks(event.getRegistry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerModels(ModelRegistryEvent event) {
|
||||||
|
ModItems.registerModels();
|
||||||
|
ModBlocks.registerModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Copper Ore
|
||||||
|
|
||||||
|
Now, because we have our `BlockBase` and `ModBlocks` classes in place, we can quickly add a new block:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class ModBlocks {
|
||||||
|
|
||||||
|
public static BlockOre oreCopper = new BlockOre("ore_copper").setCreativeTab(CreativeTabs.MATERIALS);
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Block> registry) {
|
||||||
|
registery.registerAll(
|
||||||
|
oreCopper
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerItemBlocks(IForgeRegistry<Item> registry) {
|
||||||
|
registry.registerAll(
|
||||||
|
oreCopper.createItemBlock()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
oreCopper.registerItemModel(Item.getItemFromBlock(oreCopper));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Create a new `BlockOre` with the name `oreCopper` and set the creative tab to the Materials tab.
|
||||||
|
2. Registers the block itself with the block registry.
|
||||||
|
3. Registers the `ItemBlock` with the item registry.
|
||||||
|
4. Registers the item model.
|
||||||
|
|
||||||
|
|
||||||
|
Now, in the game, we can see our (untextured) copper ore block!
|
||||||
|
|
||||||
|
![Copper Ore Screenshot](http://i.imgur.com/uWdmyA5.png)
|
||||||
|
|
||||||
|
Next, we'll look at how to make a simple model for our copper ore block.
|
|
@ -0,0 +1,37 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Forge Blockstates"
|
||||||
|
metadata.date = "2016-05-07 21:45:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got our copper ore block, let's add a simple blockstate to give it a texture. This will go in a file at `src/main/resources/assets/tutorial/blockstates/ore_copper.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"forge_marker": 1,
|
||||||
|
"defaults": {
|
||||||
|
"textures": {
|
||||||
|
"all": "tutorial:blocks/ore_copper"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"normal": {
|
||||||
|
"model": "cube_all"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"model": "cube_all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `forge_marker` (L2): This tells Forge to use its custom blockstate parser instead of Minecraft's which isn't as good. (See [here](https://mcforge.readthedocs.io/en/latest/blockstates/forgeBlockstates/) for more info about Forge's blockstate format)
|
||||||
|
- `defaults` (L3-L7): Defaults are things to apply for all variants, a feature added by Forge's blockstate format.
|
||||||
|
- `textures` (L4-L6): This specifies which textures to use for the `cube_all` model. This uses the same texture format as explained in the [JSON Item Models](https://shadowfacts.net/tutorials/forge-modding-1102/json-item-models/) tutorial.
|
||||||
|
- `variants` (L8-L15): Inside of this block are where all of our individual variants go. Because we don't have any custom block properties, we have the `normal` variant which is the normal, in-world variant. The `inventory` variant is used when rendering our item in inventory and in the player's hand.
|
||||||
|
- `"model": "cube_all"` (L10 & L13): This uses the `cube_all` model for both variants. This is a simple model included in Minecraft which uses the same `#all` texture for every side of the block. We can't include this in the `defaults` block because Forge expects there to be at least one thing in each variant block.
|
||||||
|
|
||||||
|
Now, we just need to download the [copper ore texture](https://raw.githubusercontent.com/shadowfacts/TutorialMod/1.11/src/main/resources/assets/tutorial/textures/blocks/ore_copper.png) to `src/main/resources/assets/tutorial/textures/blocks/ore_copper.png` and we're all set!
|
||||||
|
|
||||||
|
![Textured Copper Ore Screenshot](http://i.imgur.com/wJ1iJUg.png)
|
|
@ -0,0 +1,189 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Basic Items"
|
||||||
|
metadata.date = "2016-05-07 16:32:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we've got the basic structure of our mod set up, we can create our first item. This item will be fairly simple, just a copper ingot.
|
||||||
|
|
||||||
|
### Base Item
|
||||||
|
|
||||||
|
Before we actually begin creating items, we'll want to create a base class just to make things easier.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
|
||||||
|
public class ItemBase extends Item {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public ItemBase(String name) {
|
||||||
|
this.name = name;
|
||||||
|
setUnlocalizedName(name);
|
||||||
|
setRegistryName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerItemModel() {
|
||||||
|
TutorialMod.proxy.registerItemRenderer(this, 0, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemBase setCreativeTab(CreativeTabs tab) {
|
||||||
|
super.setCreativeTab(tab);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our `ItemBase` class will make it simpler to add basic items quickly. `ItemBase` primarily has a convenience constructor that sets both the unlocalized and the registry names.
|
||||||
|
|
||||||
|
- The unlocalized name is used for translating the name of the item into the currently active language.
|
||||||
|
- The registry name is used when registering our item with Forge and should _never, ever change_.
|
||||||
|
|
||||||
|
The `setCreativeTab` method is an overriden version that returns `ItemBase` instead of `Item` so we can use it in our `register` method without casting, as you'll see later.
|
||||||
|
|
||||||
|
You will have an error because we haven't created the `registerItemRenderer` method yet, so let's do that now. In the `CommonProxy` class add a new method called `registerItemRenderer` that accepts an `Item`, an `int`, and a `String`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public void registerItemRenderer(Item item, int meta, String id) {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll leave this method empty, because it's in the common proxy so it can't access any client-only code, but it still needs to be here becuase `TutorialMod.proxy` is of type `CommonProxy` so any client-only methods still need to have an empty stub in the `CommonProxy`.
|
||||||
|
|
||||||
|
To our `ClientProxy` we'll add the actual implementation of `registerItemRenderer`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void registerItemRenderer(Item item, int meta, String id) {
|
||||||
|
ModelLoader.setCustomModelResourceLocation(item, meta, new ModelResourceLocation(TutorialMod.modId + ":" + id, "inventory"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This method calls `ModelLoader.setCustomModelResourceLocation` which will tell Minecraft which item model to use for our item.
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
|
||||||
|
Nextly, we'll need to update our main mod class to actually register our items and models. In the main class, will create a static inner class called `RegistrationHandler` to, as the name implies, handle the registration. This class will have the `@Mod.EventBusSubscriber` annotation which signals Forge that it needs to be subscribed to the main event bus.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Mod.EventBusSubscriber
|
||||||
|
public static class RegistrationHandler {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The event bus is Forge's system for allowing mods to register (or subscribe) handler methods to be run when specific events happen. Forge provides numerous events for all sorts of things in the game, however the ones we're currently concerned with are the registry events. (Registries are Forge's method of keeping track of all the objects of various types in the game, both vanilla Minecraft and modded ones.)
|
||||||
|
|
||||||
|
One of the events Forge provides is the `RegistryEvent.Register<T>` event, which is fired at the appropriate time to register objects of type `T` with Forge. Since we're working with items, we'll be using the `RegisteryEvent.Register<Item>` event.
|
||||||
|
|
||||||
|
To subscribe to this event, we'll create a `static` method called `registerItems` with a return type of `void` and single parameter of type `RegistryEvent.Register<Item>`. The method will be annotated with `@SubscribeEvent` to indicate to Forge that this method handles an event.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public class TutorialMod {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Mod.EventBusSubscriber
|
||||||
|
public static class RegistrationHandler {
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerItems(RegistryEvent.Register<Item> event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the event is fired, we'll want to register our items. Instead of registering every single item in our main mod class, we'll leave that to the `ModItems` class which we'll build in the next section. For now, what's important is that we'll have a `register(IForgeRegistry<Item>)` method in the `ModItems` class which we can call during the registry event.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventBusSubscriber
|
||||||
|
public static class RegistrationHandler {
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerItems(RegistryEvent.Register<Item> event) {
|
||||||
|
ModItems.register(event.getRegistry());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nextly, we'll add another event handler for the `ModelRegistryEvent` which is fired at the appropriate time for models to be registered. In this event handler, we'll have a call to `ModItems.registerModels` which will handle the model registration.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mod.EventBusSubscriber
|
||||||
|
public static class RegistrationHandler {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerItems(ModelRegistryEvent event) {
|
||||||
|
ModItems.registerModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ModItems`
|
||||||
|
|
||||||
|
Create a class called `ModItems`. This class will contain the instances of all of our items. In Minecraft, items are singletons so we'll only ever have on instance, and a reference to this instance will be kept in our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.item;
|
||||||
|
|
||||||
|
import net.minecraft.item.Item;
|
||||||
|
import net.minecraftforge.registries.IForgeRegistry;
|
||||||
|
|
||||||
|
public class ModItems {
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Item> registry) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Right now the `register` and `registerModels` methods are empty, but this is where we'll register our items and their models.
|
||||||
|
|
||||||
|
### Copper Ingot
|
||||||
|
|
||||||
|
Now to create our actual item, the copper ingot. Because we've created the `ItemBase` helper class, we won't need to create any more classes. We'll simply add a field for our new item and create/register/set it in the `init` method of our `ModItems` class.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static ItemBase ingotCopper = new ItemBase("ingot_copper").setCreativeTab(CreativeTabs.MATERIALS);
|
||||||
|
|
||||||
|
public static void register(IForgeRegistry<Item> registry) {
|
||||||
|
registry.registerAll(
|
||||||
|
ingotCopper
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerModels() {
|
||||||
|
ingotCopper.registerItemModel();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Create a new `ItemBase` with the name `ingot_copper`
|
||||||
|
2. Set the creative tab to the Materials tab.
|
||||||
|
3. Register our item with the `GameRegistry`.
|
||||||
|
|
||||||
|
Now, if you load up the game and go into the Materials creative tab, you shoulds see our new copper ingot item (albeit without a model)! Next time we'll learn how to make basic JSON models and add a model to our copper ingot!
|
||||||
|
|
||||||
|
![Copper Ingot Item Screenshot](http://i.imgur.com/6uHudqH.png)
|
|
@ -0,0 +1,78 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Crafting Recipes"
|
||||||
|
metadata.date = "2016-06-30 10:49:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Crafting Recipes
|
||||||
|
There are two kinds of crafting recipes: shaped recipes and shapeless recipes.
|
||||||
|
|
||||||
|
In a shapeless recipe, the ingredients can be placed in any arrangement on the crafting grid. An example of a shapeless recipe is the [Pumpkin Pie recipe](http://minecraft.gamepedia.com/Pumpkin_Pie#Crafting).
|
||||||
|
|
||||||
|
Shaped recipes require their ingredients to be placed in a specific arrangement. An example of a shaped recipe is the [Cake recipe](http://minecraft.gamepedia.com/Cake#Crafting).
|
||||||
|
|
||||||
|
## Shapeless Recipe
|
||||||
|
Our shapeless recipe is going to be a simple recipe that lets people craft 1 corn into 1 corn seed. To add this, we'll create a JSON file in the `recipes` subfolder of our mod assets folder called `corn_seed.json`. (The full path from the project root should be `src/main/resources/assets/tutorial/recipes/corn_seed.json`.)
|
||||||
|
|
||||||
|
Inside the root object of the file, we'll have a couple of things: the recipe type, the recipe's ingredients, and the recipe's output. The type will be `minecraft:crafting_shapeless`, meaning it's a crafting recipe and it's of the shapeless variety. The sole ingredient will be one of our Corn items, and the output will be one of our Corn Seeds.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "minecraft:crafting_shapeless",
|
||||||
|
"ingredients": [
|
||||||
|
{
|
||||||
|
"item": "tutorial:corn"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"item": "tutorial:corn_seed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each object in the `ingredients` array and the `results` object represent items. The `item` key in the object should have a value that is the registry name of our item, including our mod ID. For the Corn item, this is `tutorial:corn` and for the Corn Seed this is `tutorial:corn_seed`.
|
||||||
|
|
||||||
|
![Shapeless Recipe](http://i.imgur.com/tFZdyK3.png)
|
||||||
|
|
||||||
|
## Shaped Recipe
|
||||||
|
Our shaped recipe is going to be an additional recipe for Rabbit Stew that accepts corn instead of carrots. We'll create another JSON file in the same folder as before, this time called `rabbit_stew.json`.
|
||||||
|
|
||||||
|
In this case, the type will be `minecraft:crafting_shaped` and the result will be the `minecraft:rabbit_stew` item, but the ingredients part of the recipe will be a bit different.
|
||||||
|
|
||||||
|
In order to avoid very repetitive code, shaped recipe inputs are defined by a pattern and a key. The pattern is an array of up to three strings, each of which can be up to three characters long. Each string in the array represents the row in the crafting grid corresponding to its index in the array, and each character in the string corresponds to the slot at the string's row and at the column corresponding to its index in the string. The key is an object mapping each character in the pattern to an input ingredient. (The space character is already defined by Minecraft to mean an empty slot.)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "minecraft:crafting_shaped",
|
||||||
|
"pattern": [
|
||||||
|
" R ",
|
||||||
|
"CPM",
|
||||||
|
" B "
|
||||||
|
],
|
||||||
|
"key": {
|
||||||
|
"R": {
|
||||||
|
"item": "minecraft:cooked_rabbit"
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"item": "tutorial:corn"
|
||||||
|
},
|
||||||
|
"P": {
|
||||||
|
"item": "minecraft:baked_potato"
|
||||||
|
},
|
||||||
|
"M": {
|
||||||
|
"item": "minecraft:brown_mushroom"
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"item": "minecraft:bowl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"item": "minecraft:rabbit_stew"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our finished recipe looks like this:
|
||||||
|
|
||||||
|
![Shaped Recipe](http://i.imgur.com/KaatGDN.png)
|
|
@ -0,0 +1,94 @@
|
||||||
|
```
|
||||||
|
metadata.title = "Creative Tabs"
|
||||||
|
metadata.date = "2016-06-14 16:26:00 -0400"
|
||||||
|
metadata.series = "forge-modding-112"
|
||||||
|
metadata.seriesName = "Forge Mods for 1.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
In this tutorial, we are going to create a custom creative tab that players can use to access all of our items when in creative mode.
|
||||||
|
|
||||||
|
## Creative Tab
|
||||||
|
First off, let's create our creative tab class. Create a class called `TutorialTab` that extends `CreativeTabs`. It will need a couple things:
|
||||||
|
|
||||||
|
1. A no-args constructor that calls the super constructor with the correct label.
|
||||||
|
2. An overriden `getTabIconItem` which returns the item to render as the icon.
|
||||||
|
|
||||||
|
The `String` passed into the super constructor is the label. The label is used to determine the localization key for the tab. For the label, we are going to pass in `TutorialMod.modId` so Minecraft uses our mod's ID to determine the localization key.
|
||||||
|
|
||||||
|
The item stack we return from the `getTabIconItem` will be rendered on the tab in the creative inventory GUI. We'll use `ModItems.ingotCopper` as the icon so our creative tab has a nice distinctive icon.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.shadowfacts.tutorial.client;
|
||||||
|
|
||||||
|
import net.minecraft.creativetab.CreativeTabs;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.shadowfacts.tutorial.TutorialMod;
|
||||||
|
import net.shadowfacts.tutorial.item.ModItems;
|
||||||
|
|
||||||
|
public class TutorialTab extends CreativeTabs {
|
||||||
|
|
||||||
|
public TutorialTab() {
|
||||||
|
super(TutorialMod.modId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack getTabIconItem() {
|
||||||
|
return new ItemStack(ModItems.ingotCopper);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's add a field to our `TutorialMod` class that stores the instance of our creative tab.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// ...
|
||||||
|
public static final TutorialTab creativeTab = new TutorialTab();
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating Everything Else
|
||||||
|
Now that we've got our creative tab all setup, let's change all of our items and blocks to use it.
|
||||||
|
|
||||||
|
Let's add a line to the end of our `BlockBase` and `ItemBase` constructors that calls `setCreativeTab` with our creative tab.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public BlockBase(Material material, String name) {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ItemBase(String name) {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll also need to add this line to the `BlockCropCorn` and `ItemCornSeed` classes because they don't extend our base item/block classes.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public BlockCropCorn() {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ItemCornSeed() {
|
||||||
|
// ...
|
||||||
|
setCreativeTab(TutorialMod.creativeTab);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, we'll need to update our `ModBlocks` and `ModItems` classes so we're no longer setting the creative tabs to other tabs.
|
||||||
|
|
||||||
|
Remove the `setCreativeTab` call from the end of the BlockOre constructor on the line where we register/create the copper ore block.
|
||||||
|
|
||||||
|
Remove the `setCreativeTab` calls from the copper ingot and corn items in `ModItems`.
|
||||||
|
|
||||||
|
## All Done!
|
||||||
|
Now when we start the game and open the creative inventory, we should be able to see our creative tab on the second page.
|
||||||
|
|
||||||
|
![our creative tab in action](http://i.imgur.com/JfEhwvu.png)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue