Per-player and per-entity events

So much for "MCPI limitations"!
This commit is contained in:
Bigjango13 2024-10-28 11:06:43 -07:00
parent 2ea637581d
commit 245cc4dea5
15 changed files with 216 additions and 41 deletions

View File

@ -30,6 +30,7 @@ void misc_run_on_swap_buffers(const std::function<void()> &function);
std::string misc_get_player_username(Player *player); std::string misc_get_player_username(Player *player);
std::map<int, std::string> &misc_get_entity_names(); std::map<int, std::string> &misc_get_entity_names();
std::string misc_get_entity_name(Entity *entity); std::string misc_get_entity_name(Entity *entity);
std::string misc_get_entity_name_upper(Entity *entity);
Entity *misc_make_entity_from_id(Level *level, int id); Entity *misc_make_entity_from_id(Level *level, int id);
std::string misc_base64_encode(const std::string &data); std::string misc_base64_encode(const std::string &data);

View File

@ -19,14 +19,14 @@ This includes:
- Gets a list of chat messages, see the limitations section down below to see why it has a zero - Gets a list of chat messages, see the limitations section down below to see why it has a zero
- When compatibility mode is enabled, `|` in the chat messages is replaced with `\` - When compatibility mode is enabled, `|` in the chat messages is replaced with `\`
- When compatibility mode is disabled, `|` in the chat messages is replaced with `\|` - When compatibility mode is disabled, `|` in the chat messages is replaced with `\|`
- When compatibility mode is disabled, the first "0" is removed, so the return value is just the message
- [x] `player.events.chat.posts() -> {"0", message}[]` - [x] `player.events.chat.posts() -> {"0", message}[]`
- This is *exactly* the same as `events.chat.posts`, however it only returns chat messages sent by the player - This is *exactly* the same as `events.chat.posts`, however it only returns chat messages sent by the player
- [x] `events.projectile.hits() -> {x: int, y: int, z: int, "1", owner: string, target: int}` - [x] `events.projectile.hits() -> {x: int, y: int, z: int, "1", owner: string, target: int}` (and `player.events.projectile.hits()` and `entity.events.projectile.hits(id: int)`)
- Returns a list of projectile hit events, the list is cleared each time it is read by this call - 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 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 - When target id is 0, it means the projectile hit a block and (x, y, z) is the block's position
- When compatibility mode is disabled, the constant "1" is removed in the return value, leaving just the useful information - When compatibility mode is disabled, the constant "1" is removed in the return value, leaving just the useful information
- When compatibility mode is disabled, the owner's name is the owner's id instead, as players can have duplicate names
- 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. - 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)` - [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) - sets rotation as if the player was at 0, 0, 0 and looking towards (x, y, z)
@ -83,12 +83,16 @@ This includes:
- Gets the absolute position of the player, aka, without passing it through `OffsetPosTranslator` - Gets the absolute position of the player, aka, without passing it through `OffsetPosTranslator`
- [x] `player.setAbsPos(x: int, y: int, z: int)` - [x] `player.setAbsPos(x: int, y: int, z: int)`
- Sets the absolute position of the player, aka, without passing it through `OffsetPosTranslator` - Sets the absolute position of the player, aka, without passing it through `OffsetPosTranslator`
- [x] `player.events.block.hits()` and `entity.events.block.hits(id: int)`, both return `{x: int, y: int, z: int, face: int, hitter: int}`
- Same as `events.block.hits`, but for only the player or entity with the id. Only block hits in creative with an iron sword are counted
- [x] `player.events.clear()` and `entity.events.clear(id: int)`
- These clear projectile, block hit, and chat events for the player or entity
- [x] `events.clear` has been changed
- It will clear block hit events and chat events, and when compat mode is disabled; projectile events
Due to MCPI limitations:
It does not implement (due to MCPI limitations): - `events.chat.posts`'s id field is set to 0 for messages that did not come from the main player
- Per-entity events - `events.block.hits` (and more specialized calls), only work in creative or creator mode
- Per-player events besides `player.events.chat.posts` for the main player
- `events.chat.posts`'s id field is always set to 0
Egdecases: Egdecases:
- `world.removeEntities`/`player.removeEntities`/`entity.removeEntities` will not remove players when given 0 or -1 for type. - `world.removeEntities`/`player.removeEntities`/`entity.removeEntities` will not remove players when given 0 or -1 for type.
@ -99,6 +103,7 @@ Egdecases:
Extras: Extras:
- Compatibility mode, this is enabled by default, and can be changed with `reborn.disableCompatMode` or `reborn.enableCompatMode`. - Compatibility mode, this is enabled by default, and can be changed with `reborn.disableCompatMode` or `reborn.enableCompatMode`.
- `events.clear`
## How does it work? ## How does it work?

View File

@ -67,7 +67,7 @@ static std::string getEntityData(CommandServer *commandserver, Entity *entity) {
// type // type
std::to_string(entity->getEntityTypeId()) + "," + std::to_string(entity->getEntityTypeId()) + "," +
// name // name
misc_get_entity_name(entity) + "," + misc_get_entity_name_upper(entity) + "," +
// x // x
std::to_string(x) + "," + std::to_string(x) + "," +
// y // y
@ -87,7 +87,9 @@ static float distance_between(Entity *e1, Entity *e2) {
struct ProjectileHitEvent { struct ProjectileHitEvent {
int x, y, z; int x, y, z;
std::string owner = ""; std::string owner = "";
int owner_id = 0;
int targetId = 0; int targetId = 0;
bool invalid = false;
std::string toString(CommandServer *commandserver) { std::string toString(CommandServer *commandserver) {
float nx = x, ny = y, nz = z; float nx = x, ny = y, nz = z;
@ -96,13 +98,14 @@ struct ProjectileHitEvent {
+ std::to_string((int) ny) + "," + std::to_string((int) ny) + ","
+ std::to_string((int) nz) + "," + std::to_string((int) nz) + ","
+ (compat_mode ? "1," : "") + (compat_mode ? "1," : "")
+ owner + "," + std::to_string(targetId); + (compat_mode ? owner : std::to_string(owner_id)) + "," + std::to_string(targetId);
} }
}; };
struct ChatEvent { struct ChatEvent {
std::string message = ""; std::string message = "";
bool from_player = false; int from_player = 0;
bool invalid = false;
std::string toString() { std::string toString() {
return message; return message;
@ -118,12 +121,8 @@ struct CircularQueue {
void push(T event) { void push(T event) {
buf[at] = event; buf[at] = event;
at++; advance_counter(at);
at %= SIZE; if (at == start) advance_counter(start);
if (at == start) {
start++;
start %= SIZE;
}
} }
T pop() { T pop() {
@ -131,14 +130,104 @@ struct CircularQueue {
ERR("Over popped projectile hit events!"); ERR("Over popped projectile hit events!");
} }
T ret = buf[start]; T ret = buf[start];
start++; advance_counter(start);
start %= EVENT_SIZE;
return ret; return ret;
} }
void advance_counter(int &i) {
i += 1;
i %= EVENT_SIZE;
}
}; };
CircularQueue<ProjectileHitEvent> hitEvents{}; CircularQueue<ProjectileHitEvent> hitEvents{};
CircularQueue<ChatEvent> chatEvents{}; CircularQueue<ChatEvent> chatEvents{};
void clearEvents(CommandServer *commandserver, int id) {
// Clear block hit events
if (commandserver->minecraft->isCreativeMode()) {
EventList_TileEvent &tileEvents = commandserver->minecraft->getCreator()->getTileEvents();
for (int i = tileEvents.at + 1, j = 0; j < tileEvents.len; i++, j++) {
if (i == tileEvents.len) i = 0;
if (tileEvents.events[i].item.entityId == id)
// This will make the API ignore it
tileEvents.events[i].tick = -1;
}
}
// Clear chat events
int i = chatEvents.start;
while (chatEvents.at != i) {
if (chatEvents.buf[i].from_player == id) {
chatEvents.buf[i].invalid = true;
}
}
// Clear projectile events
i = hitEvents.start;
while (hitEvents.at != i) {
if (hitEvents.buf[i].owner_id == id) {
hitEvents.buf[i].invalid = true;
}
}
}
std::string getBlockHits(CommandServer *commandserver, ConnectedClient &client, int id) {
std::string ret = "";
if (commandserver->minecraft->isCreativeMode()) {
EventList_TileEvent &tileEvents = commandserver->minecraft->getCreator()->getTileEvents();
for (int i = tileEvents.at + 1, j = 0; j < tileEvents.len; i++, j++) {
if (i == tileEvents.len) i = 0;
if (tileEvents.events[i].item.entityId == id && tileEvents.events[i].tick > client.lastBlockHitPoll) {
tileEvents.events[i].tick = -1;
float x = tileEvents.events[i].item.x;
float y = tileEvents.events[i].item.y;
float z = tileEvents.events[i].item.z;
commandserver->pos_translator.to(x, y, z);
ret +=
std::to_string((int) x) + "," +
std::to_string((int) y) + "," +
std::to_string((int) z) + "," +
std::to_string(tileEvents.events[i].item.face) + "," +
std::to_string(tileEvents.events[i].item.entityId) + "|";
}
}
}
if (ret.size() > 1) ret.pop_back();
return ret + "\n";
}
std::string getChatPosts(int id) {
std::string ret = "";
int i = chatEvents.start;
while (chatEvents.at != i) {
ChatEvent ce = chatEvents.buf[i];
if (ce.from_player == id && !ce.invalid) {
chatEvents.buf[i].invalid = true;
std::string message = ce.toString();
if (compat_mode) std::replace(message.begin(), message.end(), '|', '\\');
else message = misc_base64_encode(message);
ret += std::to_string(ce.from_player) + "," + message + "|";
}
chatEvents.advance_counter(i);
}
if (ret.size() > 1) ret.pop_back();
return ret + "\n";
}
std::string getProjectileHits(CommandServer *commandserver, int id) {
std::string ret = "";
int i = hitEvents.start;
while (hitEvents.at != i) {
ProjectileHitEvent he = hitEvents.buf[i];
if (he.owner_id == id && !he.invalid) {
hitEvents.buf[i].invalid = true;
std::string message = he.toString(commandserver);
ret += message + "|";
}
hitEvents.advance_counter(i);
}
if (ret.size() > 1) ret.pop_back();
return ret + "\n";
}
static const std::string fail = "Fail\n"; static const std::string fail = "Fail\n";
std::unordered_map<int, int> modern_entity_id_mapping = { std::unordered_map<int, int> modern_entity_id_mapping = {
{93, 10}, /* Chicken */ {93, 10}, /* Chicken */
@ -192,7 +281,7 @@ std::string CommandServer_parse_injection(CommandServer_parse_t old, CommandServ
if (entity == NULL) { if (entity == NULL) {
WARN("Player (or Entity) [%i] not found in entity.getName.", id); WARN("Player (or Entity) [%i] not found in entity.getName.", id);
} }
return misc_get_entity_name(entity) + "\n"; return misc_get_entity_name_upper(entity) + "\n";
} else if (cmd == "world.getEntities") { } else if (cmd == "world.getEntities") {
int type; int type;
int ret = sscanf(args.c_str(), "%d", &type); int ret = sscanf(args.c_str(), "%d", &type);
@ -236,33 +325,45 @@ std::string CommandServer_parse_injection(CommandServer_parse_t old, CommandServ
std::string ret = ""; std::string ret = "";
while (chatEvents.at != chatEvents.start) { while (chatEvents.at != chatEvents.start) {
ChatEvent ce = chatEvents.pop(); ChatEvent ce = chatEvents.pop();
if (!ce.invalid) {
std::string message = ce.toString(); std::string message = ce.toString();
if (compat_mode) std::replace(message.begin(), message.end(), '|', '\\'); if (compat_mode) std::replace(message.begin(), message.end(), '|', '\\');
else message = misc_base64_encode(message); else message = misc_base64_encode(message);
ret += (compat_mode ? "0," : "") + message + "|"; ret += std::to_string(ce.from_player) + "," + message + "|";
}
} }
if (ret.size() > 1) ret.pop_back(); if (ret.size() > 1) ret.pop_back();
return ret + "\n"; return ret + "\n";
} else if (cmd == "player.events.chat.posts") { } else if (cmd == "player.events.chat.posts") {
std::string ret = ""; return getChatPosts(commandserver->minecraft->player->id);
while (chatEvents.at != chatEvents.start) { } else if (cmd == "entity.events.chat.posts") {
ChatEvent ce = chatEvents.pop(); int id;
if (ce.from_player) { int ret = sscanf(args.c_str(), "%d", &id);
std::string message = ce.toString(); if (ret != 1) return fail;
if (compat_mode) std::replace(message.begin(), message.end(), '|', '\\'); return getChatPosts(id);
else message = misc_base64_encode(message); } else if (cmd == "player.events.block.hits") {
ret += (compat_mode ? "0," : "") + message + "|"; return getBlockHits(commandserver, client, commandserver->minecraft->player->id);
} } else if (cmd == "entity.events.block.hits") {
} int id;
if (ret.size() > 1) ret.pop_back(); int ret = sscanf(args.c_str(), "%d", &id);
return ret + "\n"; if (ret != 1) return fail;
return getBlockHits(commandserver, client, id);
} else if (cmd == "events.projectile.hits") { } else if (cmd == "events.projectile.hits") {
std::string result = ""; std::string result = "";
while (hitEvents.at != hitEvents.start) { while (hitEvents.at != hitEvents.start) {
result += hitEvents.pop().toString(commandserver) + "|"; ProjectileHitEvent he = hitEvents.pop();
if (!he.invalid)
result += he.toString(commandserver) + "|";
} }
if (result.size() > 1) result.pop_back(); if (result.size() > 1) result.pop_back();
return result + "\n"; return result + "\n";
} else if (cmd == "player.events.projectile.hits") {
return getProjectileHits(commandserver, commandserver->minecraft->player->id);
} else if (cmd == "entity.events.projectile.hits") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return fail;
return getProjectileHits(commandserver, id);
} else if (cmd == "player.setDirection" && commandserver->minecraft->player) { } else if (cmd == "player.setDirection" && commandserver->minecraft->player) {
float x, y, z; float x, y, z;
int ret = sscanf(args.c_str(), "%f,%f,%f", &x, &y, &z); int ret = sscanf(args.c_str(), "%f,%f,%f", &x, &y, &z);
@ -452,6 +553,7 @@ std::string CommandServer_parse_injection(CommandServer_parse_t old, CommandServ
result += "," + i.second + "|"; result += "," + i.second + "|";
} }
if (result.size() > 1) result.pop_back(); if (result.size() > 1) result.pop_back();
for (auto &c : result) c = toupper(c);
return result + "\n"; return result + "\n";
} else if (cmd == "player.setAbsPos" && commandserver->minecraft->player) { } else if (cmd == "player.setAbsPos" && commandserver->minecraft->player) {
float x, y, z; float x, y, z;
@ -462,6 +564,20 @@ std::string CommandServer_parse_injection(CommandServer_parse_t old, CommandServ
return std::to_string(commandserver->minecraft->player->x) + "," return std::to_string(commandserver->minecraft->player->x) + ","
+ std::to_string(commandserver->minecraft->player->y) + "," + std::to_string(commandserver->minecraft->player->y) + ","
+ std::to_string(commandserver->minecraft->player->z) + "\n"; + std::to_string(commandserver->minecraft->player->z) + "\n";
} else if (cmd == "player.events.clear") {
clearEvents(commandserver, commandserver->minecraft->player->id);
} else if (cmd == "entity.events.clear") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return fail;
clearEvents(commandserver, id);
} else if (cmd == "events.clear") {
// Clear chat events
chatEvents.at = chatEvents.start;
// Clear projectile events
if (!compat_mode) hitEvents.at = hitEvents.start;
// Extended the original, so now call the original
return old(commandserver, client, command);
} else if (cmd == "reborn.disableCompatMode") { } else if (cmd == "reborn.disableCompatMode") {
compat_mode = false; compat_mode = false;
} else if (cmd == "reborn.enableCompatMode") { } else if (cmd == "reborn.enableCompatMode") {
@ -486,6 +602,7 @@ static HitResult *Arrow_tick_HitResult_constructor_injection(HitResult *self, En
.y = (int) target->y, .y = (int) target->y,
.z = (int) target->z, .z = (int) target->z,
.owner = misc_get_player_username((Player *) shooter), .owner = misc_get_player_username((Player *) shooter),
.owner_id = shooter->id,
.targetId = target->id .targetId = target->id
}); });
} }
@ -505,6 +622,7 @@ static void Arrow_tick_injection(Arrow_tick_t old, Arrow *self) {
.y = self->hit_y, .y = self->hit_y,
.z = self->hit_z, .z = self->hit_z,
.owner = misc_get_player_username((Player *) shooter), .owner = misc_get_player_username((Player *) shooter),
.owner_id = shooter->id,
.targetId = 0 .targetId = 0
}); });
} }
@ -524,6 +642,7 @@ static void Throwable_tick_Throwable_onHit_injection(Throwable *self, HitResult
.y = (int) res->exact.y, .y = (int) res->exact.y,
.z = (int) res->exact.z, .z = (int) res->exact.z,
.owner = misc_get_player_username((Player *) thrower), .owner = misc_get_player_username((Player *) thrower),
.owner_id = thrower->id,
.targetId = res->entity->id .targetId = res->entity->id
}; };
} else { } else {
@ -533,6 +652,7 @@ static void Throwable_tick_Throwable_onHit_injection(Throwable *self, HitResult
.y = res->y, .y = res->y,
.z = res->z, .z = res->z,
.owner = misc_get_player_username((Player *) thrower), .owner = misc_get_player_username((Player *) thrower),
.owner_id = thrower->id,
.targetId = 0 .targetId = 0
}; };
} }
@ -546,8 +666,8 @@ static void Gui_addMessage_injection(Gui_addMessage_t original, Gui *gui, const
original(gui, text); original(gui, text);
} else { } else {
chatEvents.push(ChatEvent{ chatEvents.push(ChatEvent{
text, text.substr(gui->minecraft->player->username.size() + 3),
chat_is_sending() chat_is_sending() ? gui->minecraft->player->id : 0
}); });
recursing = true; recursing = true;
original(gui, text); original(gui, text);

View File

@ -15,7 +15,7 @@ std::string chat_send_api_command(const Minecraft *minecraft, const std::string
ConnectedClient client; ConnectedClient client;
client.sock = -1; client.sock = -1;
client.str = ""; client.str = "";
client.time = 0; client.lastBlockHitPoll = 0;
CommandServer *command_server = minecraft->command_server; CommandServer *command_server = minecraft->command_server;
if (command_server != nullptr) { if (command_server != nullptr) {
return command_server->parse(client, str); return command_server->parse(client, str);

View File

@ -38,7 +38,7 @@ static void Minecraft_setIsCreativeMode_injection(Minecraft_setIsCreativeMode_t
} }
// Disable CreatorMode-Specific API Features (Polling Block Hits) In SurvivalMode, This Is Preferable To Crashing // Disable CreatorMode-Specific API Features (Polling Block Hits) In SurvivalMode, This Is Preferable To Crashing
static unsigned char *Minecraft_getCreator_injection(Minecraft_getCreator_t original, Minecraft *minecraft) { static ICreator *Minecraft_getCreator_injection(Minecraft_getCreator_t original, Minecraft *minecraft) {
if (is_survival) { if (is_survival) {
// SurvivalMode, Return nullptr // SurvivalMode, Return nullptr
return nullptr; return nullptr;

View File

@ -173,6 +173,14 @@ std::string misc_get_entity_name(Entity *entity) {
} }
} }
std::string misc_get_entity_name_upper(Entity *entity) {
std::string ret = misc_get_entity_name(entity);
if (entity != NULL && !entity->isPlayer()) {
for (auto &c : ret) c = toupper(c);
}
return ret;
}
Entity *misc_make_entity_from_id(Level *level, int id) { Entity *misc_make_entity_from_id(Level *level, int id) {
if (id < 0x40) { if (id < 0x40) {
return (Entity *) MobFactory::CreateMob(id, level); return (Entity *) MobFactory::CreateMob(id, level);

View File

@ -8,6 +8,11 @@ set(SRC
src/game/Minecraft.def src/game/Minecraft.def
src/game/mode/GameMode.def src/game/mode/GameMode.def
src/game/mode/CreatorMode.def src/game/mode/CreatorMode.def
src/game/mode/ICreator.def
src/game/mode/Creator.def
src/game/mode/EventList_Item.def
src/game/mode/EventList_TileEvent.def
src/game/mode/TileEvent.def
src/game/mode/SurvivalMode.def src/game/mode/SurvivalMode.def
src/game/NinecraftApp.def src/game/NinecraftApp.def
src/game/GameRenderer.def src/game/GameRenderer.def

View File

@ -2,6 +2,6 @@ size 0xc;
property uint sock = 0x0; property uint sock = 0x0;
property std::string str = 0x4; property std::string str = 0x4;
property int time = 0x8; property int lastBlockHitPoll = 0x8;
mark-as-simple; mark-as-simple;

View File

@ -12,7 +12,7 @@ method bool isCreativeMode() = 0x17270;
method void releaseMouse() = 0x15d30; method void releaseMouse() = 0x15d30;
method void grabMouse() = 0x15d10; method void grabMouse() = 0x15d10;
method void leaveGame(bool save_remote_level) = 0x15ea0; method void leaveGame(bool save_remote_level) = 0x15ea0;
method uchar *getCreator() = 0x17538; method ICreator *getCreator() = 0x17538;
method LevelStorageSource *getLevelSource() = 0x16e84; method LevelStorageSource *getLevelSource() = 0x16e84;
method void handleMouseDown(int param_1, bool can_destroy) = 0x1584c; method void handleMouseDown(int param_1, bool can_destroy) = 0x1584c;
method void handleBuildAction(uint *build_action_intention) = 0x15920; method void handleBuildAction(uint *build_action_intention) = 0x15920;

View File

@ -0,0 +1,7 @@
size 0x20;
extends ICreator;
vtable 0x1a0dc;
property EventList_TileEvent tileEvents = 0x4;
property int tick = 0x1c;

View File

@ -1,5 +1,8 @@
extends GameMode; extends GameMode;
size 0x1c; size 0x1c;
vtable 0x102d00;
constructor (Minecraft *minecraft) = 0x1a044; constructor (Minecraft *minecraft) = 0x1a044;
property Creator *creator = 0x4;

View File

@ -0,0 +1,6 @@
size 0x18;
property int tick = 0x0;
property TileEvent item = 0x4;
mark-as-simple;

View File

@ -0,0 +1,8 @@
size 0x18;
property int at = 0x0;
property int len = 0x4;
property int cap = 0x8;
property std::vector<EventList_Item> events = 0xc;
mark-as-simple;

View File

@ -0,0 +1,3 @@
vtable 0x102ca8;
virtual-method EventList_TileEvent &getTileEvents() = 0x8;

View File

@ -0,0 +1,9 @@
size 0x14;
property int entityId = 0x0;
property int x = 0x4;
property int y = 0x8;
property int z = 0xc;
property int face = 0x10;
mark-as-simple;