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::map<int, std::string> &misc_get_entity_names();
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);
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
- 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, the first "0" is removed, so the return value is just the 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
- [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
- 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 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.
- [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)
@ -83,12 +83,16 @@ This includes:
- Gets the absolute position of the player, aka, without passing it through `OffsetPosTranslator`
- [x] `player.setAbsPos(x: int, y: int, z: int)`
- 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
It does not implement (due to MCPI limitations):
- Per-entity events
- Per-player events besides `player.events.chat.posts` for the main player
- `events.chat.posts`'s id field is always set to 0
Due to MCPI limitations:
- `events.chat.posts`'s id field is set to 0 for messages that did not come from the main player
- `events.block.hits` (and more specialized calls), only work in creative or creator mode
Egdecases:
- `world.removeEntities`/`player.removeEntities`/`entity.removeEntities` will not remove players when given 0 or -1 for type.
@ -99,6 +103,7 @@ Egdecases:
Extras:
- Compatibility mode, this is enabled by default, and can be changed with `reborn.disableCompatMode` or `reborn.enableCompatMode`.
- `events.clear`
## How does it work?

View File

@ -67,7 +67,7 @@ static std::string getEntityData(CommandServer *commandserver, Entity *entity) {
// type
std::to_string(entity->getEntityTypeId()) + "," +
// name
misc_get_entity_name(entity) + "," +
misc_get_entity_name_upper(entity) + "," +
// x
std::to_string(x) + "," +
// y
@ -87,7 +87,9 @@ static float distance_between(Entity *e1, Entity *e2) {
struct ProjectileHitEvent {
int x, y, z;
std::string owner = "";
int owner_id = 0;
int targetId = 0;
bool invalid = false;
std::string toString(CommandServer *commandserver) {
float nx = x, ny = y, nz = z;
@ -96,13 +98,14 @@ struct ProjectileHitEvent {
+ std::to_string((int) ny) + ","
+ std::to_string((int) nz) + ","
+ (compat_mode ? "1," : "")
+ owner + "," + std::to_string(targetId);
+ (compat_mode ? owner : std::to_string(owner_id)) + "," + std::to_string(targetId);
}
};
struct ChatEvent {
std::string message = "";
bool from_player = false;
int from_player = 0;
bool invalid = false;
std::string toString() {
return message;
@ -118,12 +121,8 @@ struct CircularQueue {
void push(T event) {
buf[at] = event;
at++;
at %= SIZE;
if (at == start) {
start++;
start %= SIZE;
}
advance_counter(at);
if (at == start) advance_counter(start);
}
T pop() {
@ -131,14 +130,104 @@ struct CircularQueue {
ERR("Over popped projectile hit events!");
}
T ret = buf[start];
start++;
start %= EVENT_SIZE;
advance_counter(start);
return ret;
}
void advance_counter(int &i) {
i += 1;
i %= EVENT_SIZE;
}
};
CircularQueue<ProjectileHitEvent> hitEvents{};
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";
std::unordered_map<int, int> modern_entity_id_mapping = {
{93, 10}, /* Chicken */
@ -192,7 +281,7 @@ std::string CommandServer_parse_injection(CommandServer_parse_t old, CommandServ
if (entity == NULL) {
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") {
int 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 = "";
while (chatEvents.at != chatEvents.start) {
ChatEvent ce = chatEvents.pop();
if (!ce.invalid) {
std::string message = ce.toString();
if (compat_mode) std::replace(message.begin(), message.end(), '|', '\\');
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();
return ret + "\n";
} else if (cmd == "player.events.chat.posts") {
std::string ret = "";
while (chatEvents.at != chatEvents.start) {
ChatEvent ce = chatEvents.pop();
if (ce.from_player) {
std::string message = ce.toString();
if (compat_mode) std::replace(message.begin(), message.end(), '|', '\\');
else message = misc_base64_encode(message);
ret += (compat_mode ? "0," : "") + message + "|";
}
}
if (ret.size() > 1) ret.pop_back();
return ret + "\n";
return getChatPosts(commandserver->minecraft->player->id);
} else if (cmd == "entity.events.chat.posts") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return fail;
return getChatPosts(id);
} else if (cmd == "player.events.block.hits") {
return getBlockHits(commandserver, client, commandserver->minecraft->player->id);
} else if (cmd == "entity.events.block.hits") {
int id;
int ret = sscanf(args.c_str(), "%d", &id);
if (ret != 1) return fail;
return getBlockHits(commandserver, client, id);
} else if (cmd == "events.projectile.hits") {
std::string result = "";
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();
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) {
float 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 + "|";
}
if (result.size() > 1) result.pop_back();
for (auto &c : result) c = toupper(c);
return result + "\n";
} else if (cmd == "player.setAbsPos" && commandserver->minecraft->player) {
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) + ","
+ std::to_string(commandserver->minecraft->player->y) + ","
+ 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") {
compat_mode = false;
} else if (cmd == "reborn.enableCompatMode") {
@ -486,6 +602,7 @@ static HitResult *Arrow_tick_HitResult_constructor_injection(HitResult *self, En
.y = (int) target->y,
.z = (int) target->z,
.owner = misc_get_player_username((Player *) shooter),
.owner_id = shooter->id,
.targetId = target->id
});
}
@ -505,6 +622,7 @@ static void Arrow_tick_injection(Arrow_tick_t old, Arrow *self) {
.y = self->hit_y,
.z = self->hit_z,
.owner = misc_get_player_username((Player *) shooter),
.owner_id = shooter->id,
.targetId = 0
});
}
@ -524,6 +642,7 @@ static void Throwable_tick_Throwable_onHit_injection(Throwable *self, HitResult
.y = (int) res->exact.y,
.z = (int) res->exact.z,
.owner = misc_get_player_username((Player *) thrower),
.owner_id = thrower->id,
.targetId = res->entity->id
};
} else {
@ -533,6 +652,7 @@ static void Throwable_tick_Throwable_onHit_injection(Throwable *self, HitResult
.y = res->y,
.z = res->z,
.owner = misc_get_player_username((Player *) thrower),
.owner_id = thrower->id,
.targetId = 0
};
}
@ -546,8 +666,8 @@ static void Gui_addMessage_injection(Gui_addMessage_t original, Gui *gui, const
original(gui, text);
} else {
chatEvents.push(ChatEvent{
text,
chat_is_sending()
text.substr(gui->minecraft->player->username.size() + 3),
chat_is_sending() ? gui->minecraft->player->id : 0
});
recursing = true;
original(gui, text);

View File

@ -15,7 +15,7 @@ std::string chat_send_api_command(const Minecraft *minecraft, const std::string
ConnectedClient client;
client.sock = -1;
client.str = "";
client.time = 0;
client.lastBlockHitPoll = 0;
CommandServer *command_server = minecraft->command_server;
if (command_server != nullptr) {
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
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) {
// SurvivalMode, 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) {
if (id < 0x40) {
return (Entity *) MobFactory::CreateMob(id, level);

View File

@ -8,6 +8,11 @@ set(SRC
src/game/Minecraft.def
src/game/mode/GameMode.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/NinecraftApp.def
src/game/GameRenderer.def

View File

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

View File

@ -12,7 +12,7 @@ method bool isCreativeMode() = 0x17270;
method void releaseMouse() = 0x15d30;
method void grabMouse() = 0x15d10;
method void leaveGame(bool save_remote_level) = 0x15ea0;
method uchar *getCreator() = 0x17538;
method ICreator *getCreator() = 0x17538;
method LevelStorageSource *getLevelSource() = 0x16e84;
method void handleMouseDown(int param_1, bool can_destroy) = 0x1584c;
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;
size 0x1c;
vtable 0x102d00;
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;