Inital RaspberryJuice API port

This commit is contained in:
Bigjango13 2024-09-24 18:16:40 -07:00
parent 00d193e732
commit 68a15236ef
13 changed files with 604 additions and 2 deletions

View File

@ -99,6 +99,8 @@ set(SRC
src/multidraw/storage.cpp
# classic-ui
src/classic-ui/classic-ui.cpp
# api
src/api/api.cpp
)
# Install Splashes
install(

View File

@ -31,4 +31,5 @@ void init_screenshot();
void init_f3();
void init_multidraw();
void init_classic_ui();
void init_api();
}

View File

@ -1,5 +1,7 @@
#pragma once
#pragma once
#include <symbols/minecraft.h>
#include "server_properties.h"
@ -16,4 +18,5 @@ struct ServerCommand {
extern "C" {
std::vector<ServerCommand> *server_get_commands(Minecraft *minecraft, ServerSideNetworkHandler *server_side_network_handler);
ServerProperties &get_server_properties();
std::string get_player_username(Player *player);
}

88
mods/src/api/README.md Normal file
View File

@ -0,0 +1,88 @@
# `api` Mod
This mod implements all of the RaspberryJuice extensions to the MCPI API, for the modding API, see [../misc/api.cpp]().
This includes:
- [x] `world.getBlocks(x0: int, y0: int, z0: int, x1: int, y1: int, z1: int) -> int[]`
- - Gets all the block ids between x0, y0, z0 and x1, y1, z1
- [x] `world.getPlayerId(name: str) -> int`
- - Gets the id of the first player who has the same name as `name`. "Fail" is returned on failure.
- [x] `entity.getName(id: int) -> str`
- - Gets the name of the entity of `id`, as MCPI does not have name tags, it will just be the entity type for non-players
- [x] `world.getEntities(type: int) -> {id: int, type: int, name: str, x: float, y: float, z: float}[]`
- - returns a list of entities
- [x] `world.removeEntity(id: int) -> int`
- - removes a single entity and returns 1 if successful
- [x] `world.removeEntities(type: int) -> int`
- - removes all entities of a type, and returns how many were removed
- [x] `events.chat.posts() -> {"0", message}[]`
- - gets a list of chat messages, see the limitations section down below to see why it has a zero
- - The messages are not cleared
- [x] `events.projectile.hits() -> {x: int, y: int, z: int, "1", owner: string, target: int}`
- - Returns a list of projectile hit events, the list is cleared each time it is read by this call
- - When target id is not 0, it means the projectile hit an entity and `x, y, z` is the entity's position
- - When target id is 0, it means the projectile hit a block and `x, y, z` is the block's position
- - Unlike RaspberryJuice, the last argument is the hit entity id, not the entity's name. This shouldn't break anything, and indeed should be more useful.
- [x] `player.setDirection(x: float, y: float, z: float)`
- - sets rotation as if the player was at 0, 0, 0 and looking towards x, y, z
- [x] `entity.setDirection(id: int, x: float, y: float, z: float)`
- - sets rotation as if the entity of `id` was at 0, 0, 0 and looking towards x, y, z
- [x] `player.getDirection() -> {x: float, y: float, z: float}`
- - gets the location the player would be looking at if they were at 0, 0, 0
- [x] `entity.getDirection() -> {x: float, y: float, z: float}`
- - gets the location the entity of `id` would be looking at if they were at 0, 0, 0
- [x] `player.setRotation(id: int)`
- - Sets the yaw of the player
- [x] `entity.setRotation(id: int)`
- - Sets the yaw of the entity of `id`
- [x] `player.getRotation`
- - Gets the yaw of the player
- [x] `entity.getRotation`
- - Gets the yaw of the entity of `id`
- [x] `player.getPitch`
- - Gets the pitch of the player
- [x] `entity.getPitch`
- - Gets the pitch of the entity of `id`
- [x] `player.setPitch(id: int)`
- - Sets the pitch of the player
- [x] `entity.setPitch(id: int)`
- - Sets the pitch of the entity of `id`
- [x] `player.getEntities(dist: int, type: int) -> {id: int, type: int, name: str, x: float, y: float, z: float}[]`
- - Gets all entities within `dist` of the player if the entity is of `type` (if `type` is -1, all entity types are included).
- - This can include the player.
- [x] `entity.getEntities(id: int, dist: int, type: int) -> {id: int, type: int, name: str, x: float, y: float, z: float}[]`
- - Gets all entities within `dist` of the entity of `id` if the entity is of `type` (if `type` is -1, all entity types are included).
- - This can include the entity.
- [x] `player.removeEntities(dist: int, type: int) -> int`
- - Removes all entities within `dist` of the player if the entity is of `type` (if `type` is -1, all entity types are included).
- - This will not include any players.
- - It returns the number of entities removed.
- [x] `entity.removeEntities(id: int, dist: int, type: int) -> int`
- - Removes all entities within `dist` of the entity of `id` if the entity is of `type` (if `type` is -1, all entity types are included).
- - This will not include any players, but may include the entity calling it if the entity is not a player.
- - It returns the number of entities removed.
- [x] `world.setSign(x: int, y: int, z: int, id: int, data: int, [l1: str], [l2: str], [l3: str], [l4: str])`
- - Sets a block of `id:data` at the specified point, if the block is a sign, it will attempt to set lines 1 through 4 of the sign to the given text
- - For the API, the lines must be below 100 characters, however when loading signs MCPI will cap it at 16 characters (this can be disabled by patching out the call at `0xd1e2c`).
- - The lines are optional
- - The wiki has a list of blocks: [https://mcpirevival.miraheze.org/wiki/Minecraft:_Pi_Edition_Complete_Block_List], sign is 63 and wall sign is 68
- [x] `world.spawnEntity(x: int, y: int, z: int, type: int) -> int`
- - Spawns an entity of `type` at the given position
- - Entities with a type of 0 cannot be spawned
- - The list of entity types can be found by running the command below, or on the wiki: [https://mcpirevival.miraheze.org/wiki/Minecraft:_Pi_Edition_Complete_Entity_List]
- [x] `world.getEntityTypes() -> {type: int, name: str}`
- - Returns a list of known entity types, if there are modded entities this list may be incorrect
It does not implement (due to MCPI limitations):
- Per-entity events
- Per-player events
- `player.getAbsPos`/`player.setAbsPos` as MCPI already does this
- `events.chat.posts`'s id field is always set to -1
- `events.chat.posts` only returns the last 30 messages, and does *not* clear them after each API call
Egdecases:
- `world.removeEntity` will remove a player if given a players id.
- `world.removeEntities`/`player.removeEntities`/`entity.removeEntities` will not remove players when given 0 or -1 for type.
- `entity.getName` will not get the name of non-player entities with a type id of 0 due to ambiguity.
- All Raspberry Juice commands/responses involving the player name are designed around the MCJE username restrictions, not the much looser MCPI restrictions. They may cause problems.

490
mods/src/api/api.cpp Normal file
View File

@ -0,0 +1,490 @@
#include <cmath>
#include <string>
#include <fstream>
#include <streambuf>
#include <algorithm>
#include <libreborn/libreborn.h>
#include <symbols/minecraft.h>
#include <mods/init/init.h>
#include <mods/server/server.h>
#include <mods/feature/feature.h>
static std::string getBlocks(CommandServer *commandserver, Vec3 start, Vec3 end) {
int startx = start.x, starty = start.y, startz = start.z;
int endx = end.x, endy = end.y, endz = end.z;
// Offset cords
commandserver->pos_translator.from(startx, starty, startz);
commandserver->pos_translator.from(endx, endy, endz);
if (endx < startx) std::swap(startx, endx);
if (endy < starty) std::swap(starty, endy);
if (endz < startz) std::swap(startz, endz);
// Get
std::string ret = "";
for (int x = startx; x <= endx; x++) {
for (int y = starty; y <= endy; y++) {
for (int z = startz; z <= endz; z++) {
ret += std::to_string(commandserver->minecraft->level->getTile(x, y, z)) + ",";
}
}
}
if (ret.size() > 1) ret.pop_back();
return ret + "\n";
}
static std::map<int, std::string> names = {
// Unsavables
{0, "Unknown"},
// Animals
{10, "Chicken"}, {11, "Cow"}, {12, "Pig"}, {13, "Sheep"},
// Hostiles
{32, "Zombie"}, {33, "Creeper"}, {34, "Skeleton"}, {35, "Spider"}, {36, "PigZombie"},
// Special 2, projectiles
{80, "Arrow"}, {81, "Snowball"}, {82, "ThrownEgg"},
// Special 1, misc
{64, "ItemEntity"}, {65, "PrimedTnt"}, {66, "FallingTile"}, {83, "Painting"}
};
void setEntityName(int type, std::string name) {
names[type] = name;
}
std::string getEntityName(Entity *entity) {
if (entity == NULL) {
return "";
} else if (entity->isPlayer()) {
return get_player_username((Player *) entity);
} else {
int type = entity->getEntityTypeId();
if (names.find(type) != names.end()) return names[type];
return std::to_string(entity->id);
}
}
void setDir(Entity *entity, float x, float y, float z) {
if (entity == NULL) return;
constexpr double _2PI = 2 * M_PI;
if (x == 0 && z == 0) {
entity->pitch = y > 0 ? -90 : 90;
return;
}
entity->yaw = fmod((float) atan2(-x, z), (float) _2PI) * (180.0f / M_PI);
float xz = sqrt(x * x + z * z);
entity->pitch = (float) atan(-y / xz) * (180.0f / M_PI);
}
Vec3 getDir(Entity *entity) {
float y = -sin(entity->pitch * (M_PI / 180));
float xz = cos(entity->pitch * (M_PI / 180));
float x = -xz * sin(entity->yaw * (M_PI / 180));
float z = xz * cos(entity->yaw * (M_PI / 180));
return Vec3{x, y, z};
}
std::string getEntityData(CommandServer *commandserver, Entity *entity) {
float x = entity->x, y = entity->y - entity->height_offset, z = entity->z;
commandserver->pos_translator.to(x, y, z);
return std::to_string(entity->id) + "," +
// type
std::to_string(entity->getEntityTypeId()) + "," +
// name
getEntityName(entity) + "," +
// x
std::to_string(x) + "," +
// y
std::to_string(y) + "," +
// z
std::to_string(z);
}
float edist(Entity *e1, Entity *e2) {
if (e1 == NULL || e2 == NULL) return 0;
float dx = e2->x - e1->x;
float dy = e2->y - e1->y;
float dz = e2->z - e1->z;
return sqrt(dx * dx + dy * dy + dz * dz);
}
struct ProjectileHitEvent {
int x, y, z;
std::string owner = "";
int targetId = 0;
std::string toString(CommandServer *commandserver) {
float nx = x, ny = y, nz = z;
commandserver->pos_translator.to(nx, ny, nz);
return std::to_string((int) nx) + ","
+ std::to_string((int) ny) + ","
+ std::to_string((int) nz) + ",1,"
+ owner + "," + std::to_string(targetId);
}
};
constexpr size_t EVENT_SIZE = 50;
static int event_at = 0, event_start = 0;
static ProjectileHitEvent hitEvents[EVENT_SIZE];
void addProjectile(ProjectileHitEvent event) {
hitEvents[event_at] = event;
event_at++;
event_at %= EVENT_SIZE;
if (event_at == event_start) {
event_start++;
event_start %= EVENT_SIZE;
}
}
ProjectileHitEvent popProjectile() {
if (event_start == event_at) {
ERR("Over popped projectile hit events!");
}
ProjectileHitEvent ret = hitEvents[event_start];
event_start++;
event_start %= EVENT_SIZE;
return ret;
}
std::string CommandServer_parse_injection(CommandServer_parse_t old, CommandServer *commandserver, ConnectedClient &client, const std::string &command) {
size_t arg_start = command.find("(");
if (arg_start == std::string::npos) return "Fail\n";
std::string cmd = command.substr(0, arg_start);
// rfind so ) in input doesn't break
size_t cmd_end = command.rfind(")");
if (cmd_end == std::string::npos) return "Fail\n";
std::string args = command.substr(arg_start + 1, cmd_end - arg_start - 1);
// And now the big if-else chain
if (cmd == "world.getBlocks") {
int x0, y0, z0, x1, y1, z1;
int ret = sscanf(args.c_str(), "%d,%d,%d,%d,%d,%d", &x0, &y0, &z0, &x1, &y1, &z1);
if (ret != 6) return "Fail\n";
// Get the blocks
return getBlocks(commandserver, Vec3{(float) x0, (float) y0, (float) z0}, Vec3{(float) x1, (float) y1, (float) z1});
} else if (cmd == "world.getPlayerId") {
for (Player *player : commandserver->minecraft->level->players) {
if (get_player_username(player) == args)
return std::to_string(player->id) + "\n";
}
INFO("Player [%s] not found.", args.c_str());
return "Fail\n";
} else if (cmd == "entity.getName") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return "\n";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Player (or Entity) [%i] not found in entity.getName.", id);
}
return getEntityName(entity) + "\n";
} else if (cmd == "world.getEntities") {
int type;
int ret = sscanf(args.c_str(), "%d", &type);
if (ret != 1) return "\n";
std::string result = "";
for (Entity *entity : commandserver->minecraft->level->entities) {
if (type == -1 || entity->getEntityTypeId() == type) {
// Get pos
// Put it together
result += getEntityData(commandserver, entity) + "|";
}
}
if (result.size() > 1) result.pop_back();
return result + "\n";
} else if (cmd == "world.removeEntity") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return "0\n";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity != NULL) {
// It may be a good idea to check if the entity is a player before removing
// but it doesn't crash, just lock the player in place until you rejoin the world
entity->remove();
return "1\n";
}
return "0\n";
} else if (cmd == "world.removeEntities") {
int type;
int ret = sscanf(args.c_str(), "%d", &type);
if (ret != 1) return "0\n";
int removed = 0;
for (Entity *entity : commandserver->minecraft->level->entities) {
if ((type == -1 || entity->getEntityTypeId() == type) && !entity->isPlayer()) {
entity->remove();
removed++;
}
}
return std::to_string(removed) + "\n";
} else if (cmd == "events.chat.posts") {
std::string ret = "";
for (GuiMessage gm : commandserver->minecraft->gui.messages) {
std::string message = gm.message;
std::replace(message.begin(), message.end(), '|', '\\');
INFO("%s %i", message.c_str(), gm.time);
ret += "0," + message + "|";
}
if (ret.size() > 1) ret.pop_back();
return ret + "\n";
} else if (cmd == "events.projectile.hits") {
std::string result = "";
while (event_at != event_start) {
result += popProjectile().toString(commandserver) + "|";
}
if (result.size() > 1) result.pop_back();
return result + "\n";
} else if (cmd == "player.setDirection") {
float x, y, z;
int ret = sscanf(args.c_str(), "%f,%f,%f", &x, &y, &z);
if (ret != 3) return "";
setDir((Entity *) commandserver->minecraft->player, x, y, z);
} else if (cmd == "entity.setDirection") {
int id;
float x, y, z;
int ret = sscanf(args.c_str(), "%d,%f,%f,%f", &id, &x, &y, &z);
if (ret != 4) return "";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Entity [%i] not found.", id);
} else {
setDir(entity, x, y, z);
}
} else if (cmd == "player.getDirection") {
Vec3 vec = getDir((Entity *) commandserver->minecraft->player);
return std::to_string(vec.x) + "," + std::to_string(vec.y) + "," + std::to_string(vec.z) + "\n";
} else if (cmd == "entity.getDirection") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return "";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Entity [%i] not found.", id);
return "Fail\n";
} else {
Vec3 vec = getDir(entity);
return std::to_string(vec.x) + "," + std::to_string(vec.y) + "," + std::to_string(vec.z) + "\n";
}
} else if (cmd == "player.setRotation") {
float yaw;
int ret = sscanf(args.c_str(), "%f", &yaw);
if (ret != 1) return "";
commandserver->minecraft->player->yaw = yaw;
} else if (cmd == "entity.setRotation") {
int id;
float yaw;
int ret = sscanf(args.c_str(), "%d,%f", &id, &yaw);
if (ret != 2) return "";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Entity [%i] not found.", id);
} else {
entity->yaw = yaw;
}
} else if (cmd == "player.setPitch") {
float pitch;
int ret = sscanf(args.c_str(), "%f", &pitch);
if (ret != 1) return "";
commandserver->minecraft->player->pitch = pitch;
} else if (cmd == "entity.setPitch") {
int id;
float pitch;
int ret = sscanf(args.c_str(), "%d,%f", &id, &pitch);
if (ret != 2) return "";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Entity [%i] not found.", id);
} else {
entity->pitch = pitch;
}
} else if (cmd == "player.getRotation") {
return std::to_string(commandserver->minecraft->player->yaw) + "\n";
} else if (cmd == "entity.getRotation") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return "";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Entity [%i] not found.", id);
return "Fail\n";
} else {
return std::to_string(entity->yaw) + "\n";
}
} else if (cmd == "player.getPitch") {
return std::to_string(commandserver->minecraft->player->pitch) + "\n";
} else if (cmd == "entity.getPitch") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return "";
Entity *entity = commandserver->minecraft->level->getEntity(id);
if (entity == NULL) {
INFO("Entity [%i] not found.", id);
return "Fail\n";
} else {
return std::to_string(entity->pitch) + "\n";
}
} else if (cmd == "player.getEntities" || cmd == "entity.getEntities") {
// Parse
int dist, type;
Entity *src = NULL;
if (cmd == "player.getEntities") {
int ret = sscanf(args.c_str(), "%d,%d", &dist, &type);
if (ret != 2) return "";
src = (Entity *) commandserver->minecraft->player;
} else {
int id = 0;
int ret = sscanf(args.c_str(), "%d,%d,%d", &id, &dist, &type);
if (ret != 3) return "";
src = commandserver->minecraft->level->getEntity(id);
if (src == NULL) {
INFO("Entity [%i] not found.", id);
return "Fail\n";
}
}
// Run
std::string result = "";
for (Entity *entity : commandserver->minecraft->level->entities) {
if ((type == -1 || entity->getEntityTypeId() == type) && edist(src, entity) < dist) {
result += getEntityData(commandserver, entity) + "|";
}
}
if (result.size() > 1) result.pop_back();
return result + "\n";
} else if (cmd == "player.removeEntities" || cmd == "entity.removeEntities") {
// Parse
int dist, type;
Entity *src = NULL;
if (cmd == "player.removeEntities") {
int ret = sscanf(args.c_str(), "%d,%d", &dist, &type);
if (ret != 2) return "";
src = (Entity *) commandserver->minecraft->player;
} else {
int id = 0;
int ret = sscanf(args.c_str(), "%d,%d,%d", &id, &dist, &type);
if (ret != 3) return "";
src = commandserver->minecraft->level->getEntity(id);
if (src == NULL) {
INFO("Entity [%i] not found.", id);
return "Fail\n";
}
}
// Run
int removed = 0;
for (Entity *entity : commandserver->minecraft->level->entities) {
if ((type == -1 || entity->getEntityTypeId() == type) && edist(src, entity) < dist && !entity->isPlayer()) {
entity->remove();
removed++;
}
}
return std::to_string(removed) + "\n";
} else if (cmd == "world.setSign") {
// Parse
int x, y, z, id, data;
char l1[100], l2[100], l3[100], l4[100];
int ret = sscanf(args.c_str(), "%d,%d,%d,%d,%d,%99[^,],%99[^,],%99[^,],%99s", &x, &y, &z, &id, &data, l1, l2, l3, l4);
if (ret < 5) return "Fail\n";
// Translate
commandserver->pos_translator.from(x, y, z);
// Set block
commandserver->minecraft->level->setTileAndData(x, y, z, id, data);
// Set sign data
if (ret == 3) return "";
SignTileEntity *sign = (SignTileEntity *) commandserver->minecraft->level->getTileEntity(x, y, z);
if (sign == NULL || sign->type != 4) return "";
if (ret > 5) sign->lines[0] = l1;
if (ret > 6) sign->lines[1] = l2;
if (ret > 7) sign->lines[2] = l3;
if (ret > 8) sign->lines[3] = l4;
} else if (cmd == "world.spawnEntity") {
// Parse
float x, y, z;
int id;
int ret = sscanf(args.c_str(), "%f,%f,%f,%d", &x, &y, &z, &id);
if (ret != 4) return "Fail\n";
// Translate
int ix = x, iy = y, iz = z;
int ix1 = x, iy1 = y, iz1 = z;
commandserver->pos_translator.from(ix, iy, iz);
x += ix - ix1;
y += iy - iy1;
z += iz - iz1;
// Spawn
Entity *entity = NULL;
if (id < 0x40) {
entity = (Entity *) MobFactory::CreateMob(id, commandserver->minecraft->level);
} else {
entity = EntityFactory::CreateEntity(id, commandserver->minecraft->level);
}
if (entity == NULL) return "Fail\n";
entity->moveTo(x, y, z, 0, 0);
commandserver->minecraft->level->addEntity(entity);
return std::to_string(entity->id) + "\n";
} else if (cmd == "world.getEntityTypes") {
std::string result = "";
for (auto &i : names) {
result += std::to_string(i.first) + "," + i.second + "|";
}
if (result.size() > 1) result.pop_back();
return result + "\n";
} else {
// Either invalid or a vanilla command, either way hand it off to the orignal handler
return old(commandserver, client, command);
}
// No return, it's different from empty! Empty is "\n". "" will result in nothing being sent back while "\n" will result in an empty string being sent back
return "";
}
// Arrow entity hit
static Entity *shooter = NULL;
HitResult *Arrow_tick_HitResult_constructor_injection(HitResult *self, Entity *target) {
// Orignal
self->type = 1;
self->entity = target;
self->unknown = false;
self->x = target->x;
self->y = target->y;
self->z = target->z;
// Add event
if (shooter && shooter->isPlayer()) {
addProjectile(ProjectileHitEvent{
.x = (int) self->x,
.y = (int) self->y,
.z = (int) self->z,
.owner = get_player_username((Player *) shooter),
.targetId = target->id
});
}
return self;
}
// Arrow block hit
void Arrow_tick_injection(Arrow_tick_t old, Arrow *self) {
int oldFlightTime = self->flight_time;
shooter = self->level->getEntity(self->getAuxData());
old(self);
if (self && !self->pending_removal && self->grounded && oldFlightTime != self->flight_time) {
if (shooter && shooter->isPlayer()) {
// Hit! Get the data
INFO("B_ADDED");
addProjectile(ProjectileHitEvent{
.x = self->hit_x,
.y = self->hit_y,
.z = self->hit_z,
.owner = get_player_username((Player *) shooter),
.targetId = 0
});
}
}
}
void init_api() {
overwrite_calls(CommandServer_parse, CommandServer_parse_injection);
overwrite_calls(Arrow_tick, Arrow_tick_injection);
overwrite_call((void *) 0x8b1e8, (void *) Arrow_tick_HitResult_constructor_injection);
}

View File

@ -38,6 +38,7 @@ __attribute__((constructor)) static void init() {
init_cake();
init_home();
init_override();
init_api();
if (!reborn_is_server()) {
init_benchmark();
}

View File

@ -112,7 +112,7 @@ static std::vector<Player *> get_players_in_level(Level *level) {
return level->players;
}
// Get Player's Username
static std::string get_player_username(Player *player) {
std::string get_player_username(Player *player) {
const std::string *username = &player->username;
char *safe_username_c = from_cp437(username->c_str());
std::string safe_username = safe_username_c;

View File

@ -131,6 +131,7 @@ set(SRC
src/gui/components/GuiComponent.def
src/gui/components/Button.def
src/gui/Gui.def
src/gui/GuiMessage.def
src/gui/components/IntRectangle.def
src/gui/components/RectangleArea.def
src/gui/components/ScrollingPane.def

View File

@ -1,3 +1,10 @@
extends Entity;
vtable 0x10db20;
property bool is_critical = 0xd8;
property int hit_x = 0xdc;
property int hit_y = 0xe0;
property int hit_z = 0xe4;
property bool grounded = 0xf0;
property int flight_time = 0xf8;

View File

@ -16,6 +16,7 @@ method void handleKeyPressed(int key) = 0x25a08;
method void renderHearts() = 0x2641c;
method void renderDebugInfo() = 0x26958;
property std::vector<GuiMessage> messages = 0x18;
property Minecraft *minecraft = 0x9f4;
property float selected_item_text_timer = 0x9fc;
property int flashing_slot = 0xa2c;

View File

@ -0,0 +1,6 @@
size 0x8;
property std::string message = 0x0;
property int time = 0x4;
mark-as-simple;

View File

@ -1 +1,3 @@
extends TileEntity;
property std::string lines[4] = 0x30;