Skip to content
MPGHThe Dark Arts
/
RegisterLog in
Forum
Community
What's NewLatest posts across the boardTrendingHottest threads right nowSubscribedThreads you follow
Discussion
GeneralIntroductionsEntertainmentDebate FortFlaming & Rage
Board
News & AnnouncementsMPGH TimesSuggestions & HelpGiveaways
More Sections
Art & Graphic DesignProgrammingHackingCryptocurrency
Hacks & Cheats
Games
ValorantCS2 / CS:GOCall of Duty / WarzoneFortniteApex LegendsEscape From Tarkov
+14 moreLeague of LegendsGTA VMinecraftRustROTMGBattlefieldTroveBattleOnCombat ArmsCrossFireBlackshotRuneScapeDayZDead by Daylight
Resources
Game Hacking TutorialsReverse EngineeringGeneral Game HackingAnti-CheatConsole Game Hacking
Tools
Game Hacking ToolsTrainers & CheatsHack/Release NewsNew
Submit a release →Share your cheat, tool, or config with the community.
AINEW
AI Tools
General & DiscussionPrompt EngineeringLLM JailbreaksHotAI Agents & AutomationLocal / Open Models
AI × Gaming
AI Aimbots & VisionML Anti-CheatGame Bots & Automation
Create
AI Coding / Vibe CodingAI Art & MediaAI Voice & TTS
The AI frontier →Where game hacking meets modern machine learning. Jump in.
Marketplace
Buy & Sell
SellingBuyingTradingUser Services
Trust & Safety
Middleman LoungeMarketplace TalkVouch Copy Profiles
Money
Cryptocurrency TalkCurrency ExchangeWork & Job Offers
Start selling →List accounts, services, and goods. Use the middleman to trade safe.
MPGH The Dark Arts

A community for offensive security research, reverse engineering, and AI.

Community

ForumMarketplaceSearch

Account

RegisterLog in

Legal

Privacy PolicyForum RulesHelp & FAQ
© 2026 MPGH · All rights reserved.Built by the community, for the community. For educational purposes onlyContent is shared for security research and education — we don't condone illegal use. You're responsible for complying with applicable laws. Use at your own risk.
Home › Forum › MultiPlayer Game Hacks & Cheats › Other MMORPG Hacks › Trove Hacks & Cheats › New EntityList

New EntityList

Posts 1–7 of 7 · Page 1 of 1
FAISAL32
FAISAL32
New EntityList
Alright before anything else

If youre trying to reach the Trove x64 entity list by copying random youtube tutorials about entitylists then honestly youre probably never gonna reach it

The reason is simple

Most games store entities inside a normal array vector or a simple linked list so most tutorials teach you to search for something like:

```
entity[0]
entity[1]
entity[2]
```

or one clean pointer that leads to everything

But Trove x64 doesnt really work like that anymore

What I found is much closer to a hash bucket linked list system

Meaning the objects are distributed between buckets and some buckets contain linked nodes because of collisions

So if youre searching for a classic entitylist youll probably just waste your time because the structure itself is different from the start

Anyway ill explain roughly what I did and anyone who wants the deeper technical details can read the full sheet because everything is documented there

Current layout:

```
owner = [Trove_x64.exe + 0x1396BC0]

hash list:
base = [owner + 0xE0]
stride = [owner + 0xE8]
count = [owner + 0xF0]

node:
next = [node + 0x00] & 0xFFFFFFFFFFFFFFFE
object = [node + 0x10]

object data:
name = [[object + 0x98] + 0x0]
position = [[object + 0x130] + 0x8] + 0xD0
scale = [[object + 0x130] + 0x8] + 0x178
health = [[object + 0x130] + 0x108] + 0xD8

level candidate:
level = [[object + 0x130] + 0xA8] + 0x208
```

The name position scale and health are already very stable for me

The level is still just my best candidate right now so I dont consider it fully confirmed yet until I test it more

The whole thing didnt start from magically finding an entitylist pointer

At first I was tracing actual game functions and trying to understand how the game itself reaches entities instead of forcing old methods onto x64

The first important thing I found was the HP instruction:

```
cvttsd2si rcx,[rdi+0xD8]
```

At first it looked simple

You see health at:

```
[rdi + 0xD8]
```

and naturally you think:
nice thats probably the entity object

But later it became obvious that `rdi` wasnt the real root entity object

It was more like a component or stat structure inside the entity

That was important because when I tried using:

```
object + 0xD8
```

directly on the objects from the list almost every health value was wrong or zero

So instead of assuming I already found the entity I started tracing backwards trying to understand where that health structure actually came from and what owned it

That eventually led me to:

```
object + 0x130
```

which turned out to behave like a component vector

From there I managed to pull position:

```
[[object + 0x130] + 0x8] + 0xD0
```

and scale:

```
[[object + 0x130] + 0x8] + 0x178
```

At that point I knew I had real entity data

Names positions scales everything

But the bigger question still wasnt answered

Where was the game actually storing all these objects

So I went back into the callers and functions interacting with those objects until eventually the traversal logic started becoming obvious

There was an owner object containing:

```
base
stride
count
```

And the game was basically doing:

```
node = base + index * stride
```

Then traversing linked nodes inside buckets

Which immediately explained why classic entitylist scans kept failing

Because this wasnt a normal array based entity system at all

Its basically a hash bucket linked list

Very similar in idea to older x86 structures just adapted for x64 layouts and pointer sizes

Eventually I found a stable global owner pointer:

```
[Trove_x64.exe + 0x1396BC0]
```

And after that the collector could read the whole structure directly without hooks

The final traversal flow became:

```
module + 0x1396BC0 -> owner

owner + 0xE0 -> base
owner + 0xE8 -> stride
owner + 0xF0 -> count

base + index * stride -> node

node + 0x10 -> object
node + 0x00 -> next
```

Then from the object itself I pull data like name position scale health etc

Names currently look like:

```
npc/pentapod_shadowcaster
npc/biped_crabwarrior
portal_dungeonbasic
placeable/...
abilities/...
services/...
```

Which also proved another important thing

This is not just a mob list

Its a general object registry containing many different object types

So if I only want mobs I can simply filter on:

```
npc/
```

After that I returned to health again because the old direct chain was obviously wrong

The strongest working candidate became:

```
health = [[object + 0x130] + 0x108] + 0xD8
```

And the values matched extremely well between normal mobs and boss candidates

Example:

```
normal npc/pentapod_shadowcaster
health = 100000000
scale = 0.650

boss candidate npc/pentapod_shadowcaster
health = 280000000
scale = 0.975
```

Then I started searching for level or rank using comparisons between identical mob names but different strengths

Best current candidate:

```
level = [[object + 0x130] + 0xA8] + 0x208
```

Currently it gives results like:

```
normal = 2
boss = 5
```

which makes logical sense because normal mobs usually stay under 4 while bosses go above that

But again this part still needs more validation before I fully confirm it

One important thing too

When you see values like:

```
53:1
127:0
```

Those are not chunk coordinates

They mean:

```
bucket index : chain depth
```

So:

```
53:1
```

means bucket 53 with node depth 1 inside the linked chain

The game may still have completely separate chunk systems for world blocks or spatial queries but the entity system I found here is not world chunks

Its a hash bucket linked list traversal

Anyway thats basically the short version of how I reached the current x64 entity traversal

Anyone interested in the full reverse engineering process deeper explanations assembly notes validation steps and extra structures can read the full sheet because everything is documented there

Ill also attach the currently working Python source code

Current version is still early and probably needs cleanup later but the main traversal itself is already working correctly

 
Sheet

# Trove x64 Entity Hash Linked List Discovery

## 1. Purpose

This document explains how we reached the current Trove x64 entity/object collector, why the final structure is a hash linked list rather than a normal linear EntityList, and how each part of the chain was verified.

The goal was not just to get a working script. The goal was to understand the same kind of structure that the old x86 collector used:

```python
for i in range(size):
node = base + i * step

while node:
next_raw = read_uint(node)
node = next_raw & 0xFFFFFFFE
```

The final x64 result is the same idea, but with 64-bit pointers and different offsets:

```text
owner = [Trove_x64.exe + 0x01396BC0]

base = [owner + 0xE0]
stride = [owner + 0xE8]
count = [owner + 0xF0]

for bucket_index in range(count):
node = base + bucket_index * stride

while node:
next_raw = [node + 0x00]
object = [node + 0x10]
node = next_raw & 0xFFFFFFFFFFFFFFFE
```

The object can then be used to read the name and position:

```text
name = [[object + 0x98] + 0x0]

component_vector_begin = [object + 0x130]
pos_object = [component_vector_begin + 0x8]
position = pos_object + 0xD0
```

---

## 2. Final Result Summary

### Global owner pointer

```text
owner = [Trove_x64.exe + 0x01396BC0]
```

Example from the confirmed log:

```text
module_base = 0x7FF711A30000
owner_global_ptr = 0x7FF712DC6BC0
owner = 0x1B371CE20A0
```

This is a direct pointer. It is not a double pointer.

```text
[module_base + 0x01396BC0] = owner
```

### Owner hash list header

The owner contains the hash list header at `owner + 0xE0`.

```text
hash_header = owner + 0xE0

[hash_header + 0x00] = base
[hash_header + 0x08] = stride
[hash_header + 0x10] = count
```

Because `hash_header = owner + 0xE0`, this can also be written as:

```text
base = [owner + 0xE0]
stride = [owner + 0xE8]
count = [owner + 0xF0]
```

Confirmed runtime values:

```text
base = 0x1B37ACA0080
stride = 0x18
count = 202
```

### Hash node layout

Each bucket is `stride` bytes, currently `0x18`.

```text
node + 0x00 = next pointer with low-bit marker
node + 0x08 = key / hash / flags-like field
node + 0x10 = object pointer
```

The important field is:

```text
object = [node + 0x10]
```

The next node is:

```text
next_node = [node + 0x00] & 0xFFFFFFFFFFFFFFFE
```

This is the x64 equivalent of the old x86 mask:

```text
old x86: next & 0xFFFFFFFE
new x64: next & 0xFFFFFFFFFFFFFFFE
```

---

## 3. Why We Did Not Search for a Normal EntityList

A normal EntityList usually means something simple like:

```text
entity_list_begin
entity_list_end
entity_count
```

or:

```text
array[i] -> entity
```

That is not what this game structure is.

The structure we found is a hash bucket table with linked collision chains:

```text
bucket[0] -> node -> maybe next node
bucket[1] -> node -> maybe next node
bucket[2] -> node -> maybe next node
...
```

This is why trying to find a classic linear EntityList would be misleading. You may find some arrays or temporary vectors, but they are not the root structure that behaves like the old collector.

The old x86 code also did not use a normal linear list. It used:

```text
base + i * step
while node:
next = [node] & ~1
```

That is hash linked list traversal, not plain array traversal.

So the correct mental model is:

```text
entity/object registry = hash bucket linked list
```

not:

```text
entity list = simple contiguous array of entities
```

---

## 4. Why This Is Not a Chunk List

This structure is not a spatial chunk list.

The bucket index is not a world chunk coordinate. For example, index `144` or `188` does not mean chunk X/Z. It is just a bucket index in a hash table.

The list contains mixed game objects:

```text
npc/...
placeable/...
portal_...
abilities/...
services/...
client/...
collections/...
```

So the current list is best described as:

```text
hash bucket linked list of game objects / entity-like objects
```

There may still be separate chunk systems elsewhere in the game for blocks, world regions, spatial partitioning, culling, or map data. But this specific structure is not that. This is the object registry/hash list we can traverse for entity-like objects.

---

## 5. Discovery Timeline

## 5.1 Starting Point: HP Update Instruction

The investigation started from a health-related instruction:

```asm
Trove_x64.exe+3276D0:
F2 48 0F 2C 8F D8 00 00 00
cvttsd2si rcx, [rdi+0xD8]
```

At runtime, `[rdi + 0xD8]` was a double HP value.

The related decompiled function was similar to:

```c
void FUN_140327680(longlong param_1) {
uVar3 = FUN_1408AFBD0(*(longlong *)(param_1 + 0x90), 1);
uVar4 = FUN_1408AC2C0(uVar3);
*(undefined8 *)(param_1 + 0xB0) = uVar4;

dVar6 = *(double *)(uVar3 + 0xD8);
*(longlong *)(param_1 + 0xA8) = (longlong)dVar6;
}
```

This told us that:

```text
tracker/self + 0x90 -> source object
source object eventually gives the HP object
HP double is at object + 0xD8
```

At that time, this was not the entity list. It was only a health/nameplate/update chain.

The important lesson was:

```text
HP chain is only the end of the chain, not the entity list root.
```

---

## 5.2 Tracking Callers with Frida

We used Frida because Ghidra showed multiple references and the static view alone was not enough.

The first goal was to answer:

```text
Who calls this update path?
Which object is being passed around?
Is this player or only non-player entities?
```

The Frida hook captured:

```text
self = tracker object
source = [self + 0x90]
statObject = RDI
hpAddress = RDI + 0xD8
```

This confirmed the chain:

```text
tracker/self + 0x90 -> statSource-like object
```

But this still did not give the owner/list.

---

## 5.3 Testing Old Offsets: `0x58`, Name, and Position

Old x86 code used chains like:

```text
entity + 0x58 -> ...
name: 0x58, 0x64, 0x0
position: 0x58, 0xC4, 0x4, 0x80/0x84/0x88
```

We tested whether `self + 0x58` was still valid.

The result was:

```text
self + 0x58 is not stable.
```

Sometimes it looked like a pointer. Sometimes it was a scalar or text-like data. So it was rejected as a stable owner/entity pointer.

However, position eventually led to a stronger chain:

```text
statSource = [tracker + 0x90]

statSource + 0x130 -> component vector
component_vector_entry_0 + 0x8 -> posObject
posObject + 0xD0 -> position vec3
```

In practical pointer form:

```text
vector_begin = [statSource + 0x130]
posObject = [vector_begin + 0x8]
position = posObject + 0xD0
```

This was a major step because we now had a reliable position chain from the object.

---

## 5.4 Discovering the Internal Component Vector

We then traced the function that walks vectors/components. The important function was:

```text
Trove_x64.exe + 0x8B0930
```

At first, this looked like it might be the list walker, but it was only walking a component/object vector inside each `statSource`.

We verified that:

```text
statSource + 0x130 = internal vector
entry[0] -> posObject
posObject + 0xD0 = position
```

This explained why `statSource + 0x130` was useful, but it was not the global entity/object list.

So the structure at this stage was:

```text
tracker
+0x90 -> statSource
+0x130 -> internal component vector
entry[0] -> posObject
+0xD0 -> position
```

Still not the root list.

---

## 5.5 Finding the Caller Above the Component Walker

We traced the caller of `0x8B0930` and found this important return address:

```text
caller_rva = 0x918BF4
```

Then we inspected the surrounding assembly around `0x918B10`.

The relevant assembly showed:

```asm
Trove_x64.exe+918B1F - mov rax, [rcx+0x290]
Trove_x64.exe+918B26 - lea rdi, [rcx+0xE0]
...
Trove_x64.exe+918B74 - mov rax, [rdi+0x10]
Trove_x64.exe+918B7D - dec rax
Trove_x64.exe+918B80 - imul rax, [rdi+0x08]
Trove_x64.exe+918B85 - add rax, [rdi]
...
Trove_x64.exe+918BA0 - mov rbx, [rbx+0x10]
...
Trove_x64.exe+918BDF - mov rcx, rbx
Trove_x64.exe+918BE2 - call qword ptr [rax+0x88]
Trove_x64.exe+918BEB - mov rcx, rbx
Trove_x64.exe+918BEE - call qword ptr [rax+0x90]
```

This was the key moment.

The game itself was doing:

```text
rdi = owner + 0xE0
count = [rdi + 0x10]
stride = [rdi + 0x08]
base = [rdi + 0x00]
node/object = [rbx + 0x10]
```

That proved the list header layout:

```text
owner + 0xE0:
+0x00 = base
+0x08 = stride
+0x10 = count
```

It also proved the object field:

```text
node + 0x10 = object
```

This is where `0x10` came from. It was not guessed. It came directly from the game assembly:

```asm
mov rbx, [rbx+0x10]
```

---

## 5.6 Capturing the Owner from `0x918B10`

At function start:

```text
Trove_x64.exe + 0x918B10
```

The register `RCX` was the owner object.

So we hooked it and captured:

```text
owner = RCX
```

The captured owner was:

```text
owner = 0x1B371CE20A0
```

Then reading the struct produced:

```text
header = owner + 0xE0 = 0x1B371CE2180
base = [owner + 0xE0] = 0x1B37ACA0080
stride = [owner + 0xE8] = 0x18
count = [owner + 0xF0] = 202
```

At this point, we had a working struct reader, but it still depended on hooking `0x918B10` once to capture the owner.

---

## 5.7 Finding the Global Owner Pointer

After that, we searched for where the owner pointer is stored and found:

```text
Trove_x64.exe + 0x01396BC0 -> owner
```

The test confirmed:

```text
module_base = 0x7FF711A30000
owner_global_ptr = 0x7FF712DC6BC0
owner = 0x1B371CE20A0
```

The important verification was:

```text
owner_mode = direct
```

Meaning:

```text
owner = [module_base + 0x01396BC0]
```

not:

```text
owner = [[module_base + 0x01396BC0]]
```

This allowed us to remove the hook completely and make a pure struct-based collector.

---

## 5.8 Confirming the Hash Linked List Behavior

The struct collector produced values like:

```text
buckets_visited = 202
nodes_visited = 227
```

The node count being greater than the bucket count is strong evidence of linked chains/collisions.

A normal array would usually have:

```text
nodes_visited == buckets_visited
```

But here we had:

```text
nodes_visited > buckets_visited
```

and rows with `depth = 1`, for example:

```text
idx 65 depth 1
idx 85 depth 1
idx 88 depth 1
idx 106 depth 1
idx 141 depth 1
idx 164 depth 1
idx 167 depth 1
```

That confirms the traversal is not just an array scan. It is:

```text
bucket -> linked node -> linked node ...
```

This matches the old x86 collector's behavior.

---

## 6. Current List Type

The current structure is best described as:

```text
hash bucket linked list of game objects / entity-like objects
```

It is not a normal vector and not a spatial chunk list.

### Header

```text
owner + 0xE0:
+0x00 = bucket base pointer
+0x08 = bucket stride / node stride
+0x10 = bucket count
```

### Bucket / node

```text
node + 0x00 = next pointer with low-bit marker
node + 0x08 = key/hash/flags-like field
node + 0x10 = object pointer
```

### Traversal

```text
node = base + index * stride

while node:
next_raw = [node + 0x00]
object = [node + 0x10]
node = next_raw & ~1
```

---

## 7. Final Object Chains

### Object pointer from node

```text
object = [node + 0x10]
```

### Name chain

Confirmed:

```text
name = [[object + 0x98] + 0x0]
```

In the Python collector this is represented as:

```python
NAME_CHAINS = [
[0x98, 0x0],
]
```

### Position chain

Confirmed:

```text
component_vector_begin = [object + 0x130]
posObject = [component_vector_begin + 0x8]
position = posObject + 0xD0
```

In list form:

```python
POSITION_OWNER_CHAIN = [0x130, 0x8]
POSITION_VALUE_CHAIN = [0xD0]
```

### Position fields

```text
x = float[posObject + 0xD0]
y = float[posObject + 0xD4]
z = float[posObject + 0xD8]
```

---

## 8. Mapping Old x86 Collector to New x64 Collector

### Old x86

```python
world = get_address(module, [0x10FA5AC, 0x0])
node_info = get_address(world, [0x7C])

base = read_uint(node_info + 0x0)
step = read_uint(node_info + 0x4)
size = read_uint(node_info + 0x8)

for i in range(size):
node = base + i * step

while node:
next_raw = read_uint(node)
node = next_raw & 0xFFFFFFFE
```

### New x64

```python
owner = read_ptr(module_base + 0x01396BC0)

base = read_ptr(owner + 0xE0)
stride = read_u64(owner + 0xE8)
count = read_u64(owner + 0xF0)

for i in range(count):
node = base + i * stride

while node:
next_raw = read_ptr(node + 0x00)
object = read_ptr(node + 0x10)
node = next_raw & 0xFFFFFFFFFFFFFFFE
```

### Conceptual mapping

```text
old base_addr = new [owner + 0xE0]
old step = new [owner + 0xE8]
old size = new [owner + 0xF0]
old node = new node
old next & ~1 = new next_raw & ~1
new object = [node + 0x10]
```

---

## 9. Why the HP Chain Was Useful but Not the Final List

The HP chain gave us a reliable starting point because it touched many non-player entities.

We saw:

```text
tracker + 0x90 -> statSource/object-like pointer
HP double at some downstream object + 0xD8
```

But that was only a runtime update chain. It did not directly expose the root registry.

The HP chain helped us identify object relationships:

```text
tracker -> statSource -> component vector -> posObject
```

Then tracing who updates/walks those objects led us upward to:

```text
0x918B10 -> owner + 0xE0 hash list
```

So the HP instruction was the entry point into the investigation, not the final entity list.

---

## 10. Why the Current Collector Works

The current collector works because it uses the same data layout the game uses in assembly.

The assembly showed:

```asm
lea rdi, [rcx+0xE0]
mov rax, [rdi+0x10]
imul rax, [rdi+0x08]
add rax, [rdi]
mov rbx, [rbx+0x10]
```

The collector does the same thing:

```python
owner = read_ptr(module_base + 0x01396BC0)
base = read_ptr(owner + 0xE0)
stride = read_u64(owner + 0xE8)
count = read_u64(owner + 0xF0)

node = base + index * stride
object = read_ptr(node + 0x10)
```

Then it follows the linked chain:

```python
next_raw = read_ptr(node + 0x00)
node = next_raw & 0xFFFFFFFFFFFFFFFE
```

This is why it matches the old approach instead of inventing a new list model.

---

## 11. Filtering Notes

The hash list contains more than just mobs.

Examples seen in output:

```text
npc/biped_crabwarrior
npc/depths/fish_wanderer_02
npc/pentapod_shadowcaster
placeable/refiller_healthflask_interactive
portal_dungeonbasic
custom_heads_service
client/flatlands
/kiwi/bin/server_data/scenario/services/...
```

So if the goal is AutoAim or mob ESP, filter by name:

```python
name.startswith("npc/")
```

If the goal is general ESP, keep more categories:

```python
name.startswith("npc/")
name.startswith("placeable/")
name.startswith("portal")
name.startswith("collections/")
name.startswith("abilities/")
```

If the goal is only visible or meaningful entities, also filter out zero positions:

```python
not (x == 0.0 and y == 0.0 and z == 0.0)
```

---

## 12. Final Confirmed Offsets

```text
Global owner:
Trove_x64.exe + 0x01396BC0 -> owner

Owner:
owner + 0xE0 -> base
owner + 0xE8 -> stride
owner + 0xF0 -> count

Node:
node + 0x00 -> next pointer with low-bit marker
node + 0x10 -> object

Object:
object + 0x98 -> name pointer
object + 0x130 -> component vector begin

Component vector:
[object + 0x130] + 0x8 -> posObject

Position object:
posObject + 0xD0 -> x
posObject + 0xD4 -> y
posObject + 0xD8 -> z
```

---

## 13. Final Mental Model

The correct mental model is:

```text
Trove_x64.exe + 0x01396BC0
-> owner / world-like object
+0xE0 -> hash list header
+0x00 -> bucket base
+0x08 -> bucket stride
+0x10 -> bucket count

bucket = base + index * stride
+0x00 -> next linked node
+0x10 -> object

object
+0x98 -> name pointer
+0x130 -> component vector

component vector entry 0
+0x8 -> posObject

posObject
+0xD0 -> position vec3
```

Short version:

```text
The game does not expose a simple EntityList here.
It stores objects in a hash bucket linked list.
We discovered the owner from the game update flow, verified the list header from assembly, confirmed the node layout from traversal results, then replaced the hook with a direct global pointer to owner.
```



 
Source Code

import math
import time
from dataclasses import dataclass

import pymem
import pymem.process


PROCESS_NAME = "Trove_x64.exe"
OWNER_RVA = 0x1396BC0

# All confirmed chains are kept here.
OFFSETS = {
# owner = [module_base + OWNER_RVA]
"hash_base": [0xE0],
"hash_stride": [0xE8],
"hash_count": [0xF0],

# Hash node layout.
"node_next": [0x00],
"node_object": [0x10],

# Entity fields.
"name": [0x98, 0x0],
"position": [0x130, 0x8, 0xD0],
"scale": [0x130, 0x8, 0x178],
"health": [0x130, 0x108, 0xD8],

# Current best level/rank candidate.
# Normal mobs usually showed 1..3, boss candidate showed 4+.
"level": [0x130, 0xA8, 0x208],
}

NODE_MARK_MASK = 0xFFFFFFFFFFFFFFFE

MAX_BUCKETS = 8192
MAX_CHAIN_DEPTH = 256
MAX_TOTAL_NODES = 30000

NAME_MAX_LEN = 160
REFRESH_DELAY = 1.0

CLEAR_SCREEN = False
SKIP_ZERO_POSITION = True

# Empty = show everything with name and position.
# Mobs only:
# NAME_PREFIX_FILTERS = ["npc/"]
NAME_PREFIX_FILTERS = ["npc/"]


@dataclass(slots=True)
class Entity:
index: str
name: str
health: float | None
level: int | None
scale: float | None
x: float
y: float
z: float


def is_plausible_ptr(value: int) -> bool:
return 0x10000 <= value <= 0x00007FFFFFFFFFFF


def read_ptr(pm: pymem.Pymem, address: int) -> int:
try:
value = pm.read_ulonglong(address)
return value if is_plausible_ptr(value) else 0
except Exception:
return 0


def read_u64(pm: pymem.Pymem, address: int) -> int | None:
try:
return pm.read_ulonglong(address)
except Exception:
return None


def read_u32(pm: pymem.Pymem, address: int) -> int | None:
try:
return pm.read_uint(address)
except Exception:
return None


def read_float(pm: pymem.Pymem, address: int) -> float | None:
try:
value = pm.read_float(address)

if not math.isfinite(value):
return None

return value
except Exception:
return None


def read_double(pm: pymem.Pymem, address: int) -> float | None:
try:
value = pm.read_double(address)

if not math.isfinite(value):
return None

return value
except Exception:
return None


def read_latin1_string(pm: pymem.Pymem, address: int, max_len: int = NAME_MAX_LEN) -> str:
if not is_plausible_ptr(address):
return ""

try:
data = pm.read_bytes(address, max_len)
except Exception:
return ""

raw = data.split(b"\0", 1)[0]

if not raw:
return ""

try:
return raw.decode("latin-1", errors="replace").strip()
except Exception:
return ""


def get_module_base(pm: pymem.Pymem, module_name: str) -> int:
module = pymem.process.module_from_name(pm.process_handle, module_name)

if module is None:
raise RuntimeError(f"Module not found: {module_name}")

return module.lpBaseOfDll


def resolve_pointer_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> int:
current = base

for offset in chain:
if not is_plausible_ptr(current):
return 0

current = read_ptr(pm, current + offset)

if not is_plausible_ptr(current):
return 0

return current


def resolve_value_address(pm: pymem.Pymem, base: int, chain: list[int]) -> int:
if not chain or not is_plausible_ptr(base):
return 0

current = base

# Every offset except the last one is a pointer hop.
for offset in chain[:-1]:
current = read_ptr(pm, current + offset)

if not is_plausible_ptr(current):
return 0

address = current + chain[-1]
return address if is_plausible_ptr(address) else 0


def read_string_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> str:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return ""

# This path usually resolves directly to the latin-1 text address.
direct = read_latin1_string(pm, address)

if direct:
return direct

# Fallback if the last address stores a pointer to the text.
pointed = read_ptr(pm, address)
return read_latin1_string(pm, pointed)


def read_float_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> float | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

return read_float(pm, address)


def read_double_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> float | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

return read_double(pm, address)


def read_u32_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> int | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

return read_u32(pm, address)


def read_vec3_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> tuple[float, float, float] | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

x = read_float(pm, address)
y = read_float(pm, address + 0x4)
z = read_float(pm, address + 0x8)

if x is None or y is None or z is None:
return None

if abs(x) > 1_000_000 or abs(y) > 1_000_000 or abs(z) > 1_000_000:
return None

if SKIP_ZERO_POSITION and abs(x) < 0.0001 and abs(y) < 0.0001 and abs(z) < 0.0001:
return None

return x, y, z


def read_hash_header(pm: pymem.Pymem, owner: int) -> tuple[int, int, int]:
base = resolve_pointer_chain(pm, owner, OFFSETS["hash_base"])

stride_address = resolve_value_address(pm, owner, OFFSETS["hash_stride"])
count_address = resolve_value_address(pm, owner, OFFSETS["hash_count"])

stride = read_u64(pm, stride_address)
count = read_u64(pm, count_address)

if not is_plausible_ptr(base):
raise RuntimeError(f"Bad hash base: 0x{base:X}")

if stride is None or stride <= 0 or stride > 0x400:
raise RuntimeError(f"Bad stride: {stride}")

if count is None or count <= 0 or count > MAX_BUCKETS:
raise RuntimeError(f"Bad count: {count}")

return base, int(stride), int(count)


def name_allowed(name: str) -> bool:
if not name:
return False

if not NAME_PREFIX_FILTERS:
return True

return any(name.startswith(prefix) for prefix in NAME_PREFIX_FILTERS)


def read_entity(pm: pymem.Pymem, bucket_index: int, depth: int, entity_object: int) -> Entity | None:
name = read_string_chain(pm, entity_object, OFFSETS["name"])

if not name_allowed(name):
return None

position = read_vec3_chain(pm, entity_object, OFFSETS["position"])

if position is None:
return None

health = read_double_chain(pm, entity_object, OFFSETS["health"])
level = read_u32_chain(pm, entity_object, OFFSETS["level"])
scale = read_float_chain(pm, entity_object, OFFSETS["scale"])

x, y, z = position

return Entity(
index=f"{bucket_index}:{depth}",
name=name,
health=health,
level=level,
scale=scale,
x=x,
y=y,
z=z,
)


def collect_entities(pm: pymem.Pymem, owner: int) -> tuple[list[Entity], dict[str, int]]:
base, stride, count = read_hash_header(pm, owner)

entities: list[Entity] = []
seen_nodes: set[int] = set()
seen_objects: set[int] = set()

stats = {
"base": base,
"stride": stride,
"count": count,
"nodes": 0,
"shown": 0,
"skipped": 0,
"duplicates": 0,
}

for bucket_index in range(count):
node = base + bucket_index * stride
depth = 0

while is_plausible_ptr(node) and depth < MAX_CHAIN_DEPTH:
if stats["nodes"] >= MAX_TOTAL_NODES:
break

if node in seen_nodes:
break

seen_nodes.add(node)
stats["nodes"] += 1

raw_next = resolve_pointer_chain(pm, node, OFFSETS["node_next"])
next_node = raw_next & NODE_MARK_MASK if raw_next else 0

entity_object = resolve_pointer_chain(pm, node, OFFSETS["node_object"])

if is_plausible_ptr(entity_object):
if entity_object in seen_objects:
stats["duplicates"] += 1
else:
seen_objects.add(entity_object)

entity = read_entity(pm, bucket_index, depth, entity_object)

if entity is None:
stats["skipped"] += 1
else:
entities.append(entity)

if raw_next == 1:
break

if not is_plausible_ptr(next_node):
break

if next_node == node:
break

node = next_node
depth += 1

entities.sort(
key=lambda item: (
item.name.lower(),
item.level if item.level is not None else 999999,
item.index,
)
)

stats["shown"] = len(entities)
return entities, stats


def format_health(value: float | None) -> str:
if value is None:
return "None"

if abs(value) >= 1_000_000:
return f"{value:.0f}"

if abs(value - round(value)) < 0.001:
return f"{value:.0f}"

return f"{value:.3f}"


def format_level(value: int | None) -> str:
if value is None:
return "None"

return str(value)


def format_scale(value: float | None) -> str:
if value is None:
return "None"

return f"{value:.3f}"


def print_entities(owner: int, stats: dict[str, int], entities: list[Entity]) -> None:
if CLEAR_SCREEN:
print("\033[2J\033[H", end="")

print(f"Owner : 0x{owner:X}")
print(f"Base : 0x{stats['base']:X}")
print(f"Stride : 0x{stats['stride']:X} ({stats['stride']})")
print(f"Count : {stats['count']}")
print(f"Nodes : {stats['nodes']}")
print(f"Shown : {stats['shown']}")
print(f"Skip : {stats['skipped']}")
print(f"Dup : {stats['duplicates']}")
print("-" * 150)

print(
f"{'#':<3} "
f"{'idx':<7} "
f"{'name':<64} "
f"{'health':>14} "
f"{'level':>7} "
f"{'scale':>8} "
f"position"
)

for row, entity in enumerate(entities, start=1):
position = f"({entity.x:.3f}, {entity.y:.3f}, {entity.z:.3f})"

print(
f"{row:<3} "
f"{entity.index:<7} "
f"{entity.name[:63]:<64} "
f"{format_health(entity.health):>14} "
f"{format_level(entity.level):>7} "
f"{format_scale(entity.scale):>8} "
f"{position}"
)

print()


def main() -> None:
pm = pymem.Pymem(PROCESS_NAME)
module_base = get_module_base(pm, PROCESS_NAME)

owner_ptr_address = module_base + OWNER_RVA
owner = read_ptr(pm, owner_ptr_address)

if not is_plausible_ptr(owner):
raise RuntimeError(f"Owner pointer is invalid: 0x{owner:X}")

print(f"Module base : 0x{module_base:X}")
print(f"Owner ptr address: 0x{owner_ptr_address:X}")
print(f"Owner : 0x{owner:X}")
print()

while True:
try:
entities, stats = collect_entities(pm, owner)
print_entities(owner, stats, entities)
time.sleep(REFRESH_DELAY)

except KeyboardInterrupt:
print("\nStopped")
break

except Exception as exc:
print(f"Collector error: {exc}")
time.sleep(REFRESH_DELAY)


if __name__ == "__main__":
main()
#1 · edited 1mo ago · 1mo ago
CW
cwelcwel
that stuff was already explained here https://www.mpgh.net/forum/showthread.php?t=1583596 and it still apply to 64 bit. it never changed.
#2 · 1mo ago
SN
snoopy397
Could you update your multicheat database after you successfully find all the pointers please? Currently unable to find any of them
#3 · 1mo ago
CH
chestare1223cool
Help?
i know you dont update any of the auto dungeon farm but do you know where i can get one/ if you could help that would be great.
t.
#4 · 1mo ago
fallenpowa
fallenpowa
could you also explain values ​​like noclip and speedhack?
#5 · 29d ago
CU
cutebun
Still on the hunt for the update, thanks @FAISAL32

just a heads up tho, they're paycheat dev, so pls don't share what you find or ppl will totally abuse your good intent
Quote Originally Posted by fallenpowa View Post
could you also explain values ​​like noclip and speedhack?
#6 · 29d ago
TI
tiqi
Quote Originally Posted by FAISAL32 View Post
Alright before anything else

If youre trying to reach the Trove x64 entity list by copying random youtube tutorials about entitylists then honestly youre probably never gonna reach it

The reason is simple

Most games store entities inside a normal array vector or a simple linked list so most tutorials teach you to search for something like:

```
entity[0]
entity[1]
entity[2]
```

or one clean pointer that leads to everything

But Trove x64 doesnt really work like that anymore

What I found is much closer to a hash bucket linked list system

Meaning the objects are distributed between buckets and some buckets contain linked nodes because of collisions

So if youre searching for a classic entitylist youll probably just waste your time because the structure itself is different from the start

Anyway ill explain roughly what I did and anyone who wants the deeper technical details can read the full sheet because everything is documented there

Current layout:

```
owner = [Trove_x64.exe + 0x1396BC0]

hash list:
base = [owner + 0xE0]
stride = [owner + 0xE8]
count = [owner + 0xF0]

node:
next = [node + 0x00] & 0xFFFFFFFFFFFFFFFE
object = [node + 0x10]

object data:
name = [[object + 0x98] + 0x0]
position = [[object + 0x130] + 0x8] + 0xD0
scale = [[object + 0x130] + 0x8] + 0x178
health = [[object + 0x130] + 0x108] + 0xD8

level candidate:
level = [[object + 0x130] + 0xA8] + 0x208
```

The name position scale and health are already very stable for me

The level is still just my best candidate right now so I dont consider it fully confirmed yet until I test it more

The whole thing didnt start from magically finding an entitylist pointer

At first I was tracing actual game functions and trying to understand how the game itself reaches entities instead of forcing old methods onto x64

The first important thing I found was the HP instruction:

```
cvttsd2si rcx,[rdi+0xD8]
```

At first it looked simple

You see health at:

```
[rdi + 0xD8]
```

and naturally you think:
nice thats probably the entity object

But later it became obvious that `rdi` wasnt the real root entity object

It was more like a component or stat structure inside the entity

That was important because when I tried using:

```
object + 0xD8
```

directly on the objects from the list almost every health value was wrong or zero

So instead of assuming I already found the entity I started tracing backwards trying to understand where that health structure actually came from and what owned it

That eventually led me to:

```
object + 0x130
```

which turned out to behave like a component vector

From there I managed to pull position:

```
[[object + 0x130] + 0x8] + 0xD0
```

and scale:

```
[[object + 0x130] + 0x8] + 0x178
```

At that point I knew I had real entity data

Names positions scales everything

But the bigger question still wasnt answered

Where was the game actually storing all these objects

So I went back into the callers and functions interacting with those objects until eventually the traversal logic started becoming obvious

There was an owner object containing:

```
base
stride
count
```

And the game was basically doing:

```
node = base + index * stride
```

Then traversing linked nodes inside buckets

Which immediately explained why classic entitylist scans kept failing

Because this wasnt a normal array based entity system at all

Its basically a hash bucket linked list

Very similar in idea to older x86 structures just adapted for x64 layouts and pointer sizes

Eventually I found a stable global owner pointer:

```
[Trove_x64.exe + 0x1396BC0]
```

And after that the collector could read the whole structure directly without hooks

The final traversal flow became:

```
module + 0x1396BC0 -> owner

owner + 0xE0 -> base
owner + 0xE8 -> stride
owner + 0xF0 -> count

base + index * stride -> node

node + 0x10 -> object
node + 0x00 -> next
```

Then from the object itself I pull data like name position scale health etc

Names currently look like:

```
npc/pentapod_shadowcaster
npc/biped_crabwarrior
portal_dungeonbasic
placeable/...
abilities/...
services/...
```

Which also proved another important thing

This is not just a mob list

Its a general object registry containing many different object types

So if I only want mobs I can simply filter on:

```
npc/
```

After that I returned to health again because the old direct chain was obviously wrong

The strongest working candidate became:

```
health = [[object + 0x130] + 0x108] + 0xD8
```

And the values matched extremely well between normal mobs and boss candidates

Example:

```
normal npc/pentapod_shadowcaster
health = 100000000
scale = 0.650

boss candidate npc/pentapod_shadowcaster
health = 280000000
scale = 0.975
```

Then I started searching for level or rank using comparisons between identical mob names but different strengths

Best current candidate:

```
level = [[object + 0x130] + 0xA8] + 0x208
```

Currently it gives results like:

```
normal = 2
boss = 5
```

which makes logical sense because normal mobs usually stay under 4 while bosses go above that

But again this part still needs more validation before I fully confirm it

One important thing too

When you see values like:

```
53:1
127:0
```

Those are not chunk coordinates

They mean:

```
bucket index : chain depth
```

So:

```
53:1
```

means bucket 53 with node depth 1 inside the linked chain

The game may still have completely separate chunk systems for world blocks or spatial queries but the entity system I found here is not world chunks

Its a hash bucket linked list traversal

Anyway thats basically the short version of how I reached the current x64 entity traversal

Anyone interested in the full reverse engineering process deeper explanations assembly notes validation steps and extra structures can read the full sheet because everything is documented there

Ill also attach the currently working Python source code

Current version is still early and probably needs cleanup later but the main traversal itself is already working correctly

 
Sheet

# Trove x64 Entity Hash Linked List Discovery

## 1. Purpose

This document explains how we reached the current Trove x64 entity/object collector, why the final structure is a hash linked list rather than a normal linear EntityList, and how each part of the chain was verified.

The goal was not just to get a working script. The goal was to understand the same kind of structure that the old x86 collector used:

```python
for i in range(size):
node = base + i * step

while node:
next_raw = read_uint(node)
node = next_raw & 0xFFFFFFFE
```

The final x64 result is the same idea, but with 64-bit pointers and different offsets:

```text
owner = [Trove_x64.exe + 0x01396BC0]

base = [owner + 0xE0]
stride = [owner + 0xE8]
count = [owner + 0xF0]

for bucket_index in range(count):
node = base + bucket_index * stride

while node:
next_raw = [node + 0x00]
object = [node + 0x10]
node = next_raw & 0xFFFFFFFFFFFFFFFE
```

The object can then be used to read the name and position:

```text
name = [[object + 0x98] + 0x0]

component_vector_begin = [object + 0x130]
pos_object = [component_vector_begin + 0x8]
position = pos_object + 0xD0
```

---

## 2. Final Result Summary

### Global owner pointer

```text
owner = [Trove_x64.exe + 0x01396BC0]
```

Example from the confirmed log:

```text
module_base = 0x7FF711A30000
owner_global_ptr = 0x7FF712DC6BC0
owner = 0x1B371CE20A0
```

This is a direct pointer. It is not a double pointer.

```text
[module_base + 0x01396BC0] = owner
```

### Owner hash list header

The owner contains the hash list header at `owner + 0xE0`.

```text
hash_header = owner + 0xE0

[hash_header + 0x00] = base
[hash_header + 0x08] = stride
[hash_header + 0x10] = count
```

Because `hash_header = owner + 0xE0`, this can also be written as:

```text
base = [owner + 0xE0]
stride = [owner + 0xE8]
count = [owner + 0xF0]
```

Confirmed runtime values:

```text
base = 0x1B37ACA0080
stride = 0x18
count = 202
```

### Hash node layout

Each bucket is `stride` bytes, currently `0x18`.

```text
node + 0x00 = next pointer with low-bit marker
node + 0x08 = key / hash / flags-like field
node + 0x10 = object pointer
```

The important field is:

```text
object = [node + 0x10]
```

The next node is:

```text
next_node = [node + 0x00] & 0xFFFFFFFFFFFFFFFE
```

This is the x64 equivalent of the old x86 mask:

```text
old x86: next & 0xFFFFFFFE
new x64: next & 0xFFFFFFFFFFFFFFFE
```

---

## 3. Why We Did Not Search for a Normal EntityList

A normal EntityList usually means something simple like:

```text
entity_list_begin
entity_list_end
entity_count
```

or:

```text
array[i] -> entity
```

That is not what this game structure is.

The structure we found is a hash bucket table with linked collision chains:

```text
bucket[0] -> node -> maybe next node
bucket[1] -> node -> maybe next node
bucket[2] -> node -> maybe next node
...
```

This is why trying to find a classic linear EntityList would be misleading. You may find some arrays or temporary vectors, but they are not the root structure that behaves like the old collector.

The old x86 code also did not use a normal linear list. It used:

```text
base + i * step
while node:
next = [node] & ~1
```

That is hash linked list traversal, not plain array traversal.

So the correct mental model is:

```text
entity/object registry = hash bucket linked list
```

not:

```text
entity list = simple contiguous array of entities
```

---

## 4. Why This Is Not a Chunk List

This structure is not a spatial chunk list.

The bucket index is not a world chunk coordinate. For example, index `144` or `188` does not mean chunk X/Z. It is just a bucket index in a hash table.

The list contains mixed game objects:

```text
npc/...
placeable/...
portal_...
abilities/...
services/...
client/...
collections/...
```

So the current list is best described as:

```text
hash bucket linked list of game objects / entity-like objects
```

There may still be separate chunk systems elsewhere in the game for blocks, world regions, spatial partitioning, culling, or map data. But this specific structure is not that. This is the object registry/hash list we can traverse for entity-like objects.

---

## 5. Discovery Timeline

## 5.1 Starting Point: HP Update Instruction

The investigation started from a health-related instruction:

```asm
Trove_x64.exe+3276D0:
F2 48 0F 2C 8F D8 00 00 00
cvttsd2si rcx, [rdi+0xD8]
```

At runtime, `[rdi + 0xD8]` was a double HP value.

The related decompiled function was similar to:

```c
void FUN_140327680(longlong param_1) {
uVar3 = FUN_1408AFBD0(*(longlong *)(param_1 + 0x90), 1);
uVar4 = FUN_1408AC2C0(uVar3);
*(undefined8 *)(param_1 + 0xB0) = uVar4;

dVar6 = *(double *)(uVar3 + 0xD8);
*(longlong *)(param_1 + 0xA8) = (longlong)dVar6;
}
```

This told us that:

```text
tracker/self + 0x90 -> source object
source object eventually gives the HP object
HP double is at object + 0xD8
```

At that time, this was not the entity list. It was only a health/nameplate/update chain.

The important lesson was:

```text
HP chain is only the end of the chain, not the entity list root.
```

---

## 5.2 Tracking Callers with Frida

We used Frida because Ghidra showed multiple references and the static view alone was not enough.

The first goal was to answer:

```text
Who calls this update path?
Which object is being passed around?
Is this player or only non-player entities?
```

The Frida hook captured:

```text
self = tracker object
source = [self + 0x90]
statObject = RDI
hpAddress = RDI + 0xD8
```

This confirmed the chain:

```text
tracker/self + 0x90 -> statSource-like object
```

But this still did not give the owner/list.

---

## 5.3 Testing Old Offsets: `0x58`, Name, and Position

Old x86 code used chains like:

```text
entity + 0x58 -> ...
name: 0x58, 0x64, 0x0
position: 0x58, 0xC4, 0x4, 0x80/0x84/0x88
```

We tested whether `self + 0x58` was still valid.

The result was:

```text
self + 0x58 is not stable.
```

Sometimes it looked like a pointer. Sometimes it was a scalar or text-like data. So it was rejected as a stable owner/entity pointer.

However, position eventually led to a stronger chain:

```text
statSource = [tracker + 0x90]

statSource + 0x130 -> component vector
component_vector_entry_0 + 0x8 -> posObject
posObject + 0xD0 -> position vec3
```

In practical pointer form:

```text
vector_begin = [statSource + 0x130]
posObject = [vector_begin + 0x8]
position = posObject + 0xD0
```

This was a major step because we now had a reliable position chain from the object.

---

## 5.4 Discovering the Internal Component Vector

We then traced the function that walks vectors/components. The important function was:

```text
Trove_x64.exe + 0x8B0930
```

At first, this looked like it might be the list walker, but it was only walking a component/object vector inside each `statSource`.

We verified that:

```text
statSource + 0x130 = internal vector
entry[0] -> posObject
posObject + 0xD0 = position
```

This explained why `statSource + 0x130` was useful, but it was not the global entity/object list.

So the structure at this stage was:

```text
tracker
+0x90 -> statSource
+0x130 -> internal component vector
entry[0] -> posObject
+0xD0 -> position
```

Still not the root list.

---

## 5.5 Finding the Caller Above the Component Walker

We traced the caller of `0x8B0930` and found this important return address:

```text
caller_rva = 0x918BF4
```

Then we inspected the surrounding assembly around `0x918B10`.

The relevant assembly showed:

```asm
Trove_x64.exe+918B1F - mov rax, [rcx+0x290]
Trove_x64.exe+918B26 - lea rdi, [rcx+0xE0]
...
Trove_x64.exe+918B74 - mov rax, [rdi+0x10]
Trove_x64.exe+918B7D - dec rax
Trove_x64.exe+918B80 - imul rax, [rdi+0x08]
Trove_x64.exe+918B85 - add rax, [rdi]
...
Trove_x64.exe+918BA0 - mov rbx, [rbx+0x10]
...
Trove_x64.exe+918BDF - mov rcx, rbx
Trove_x64.exe+918BE2 - call qword ptr [rax+0x88]
Trove_x64.exe+918BEB - mov rcx, rbx
Trove_x64.exe+918BEE - call qword ptr [rax+0x90]
```

This was the key moment.

The game itself was doing:

```text
rdi = owner + 0xE0
count = [rdi + 0x10]
stride = [rdi + 0x08]
base = [rdi + 0x00]
node/object = [rbx + 0x10]
```

That proved the list header layout:

```text
owner + 0xE0:
+0x00 = base
+0x08 = stride
+0x10 = count
```

It also proved the object field:

```text
node + 0x10 = object
```

This is where `0x10` came from. It was not guessed. It came directly from the game assembly:

```asm
mov rbx, [rbx+0x10]
```

---

## 5.6 Capturing the Owner from `0x918B10`

At function start:

```text
Trove_x64.exe + 0x918B10
```

The register `RCX` was the owner object.

So we hooked it and captured:

```text
owner = RCX
```

The captured owner was:

```text
owner = 0x1B371CE20A0
```

Then reading the struct produced:

```text
header = owner + 0xE0 = 0x1B371CE2180
base = [owner + 0xE0] = 0x1B37ACA0080
stride = [owner + 0xE8] = 0x18
count = [owner + 0xF0] = 202
```

At this point, we had a working struct reader, but it still depended on hooking `0x918B10` once to capture the owner.

---

## 5.7 Finding the Global Owner Pointer

After that, we searched for where the owner pointer is stored and found:

```text
Trove_x64.exe + 0x01396BC0 -> owner
```

The test confirmed:

```text
module_base = 0x7FF711A30000
owner_global_ptr = 0x7FF712DC6BC0
owner = 0x1B371CE20A0
```

The important verification was:

```text
owner_mode = direct
```

Meaning:

```text
owner = [module_base + 0x01396BC0]
```

not:

```text
owner = [[module_base + 0x01396BC0]]
```

This allowed us to remove the hook completely and make a pure struct-based collector.

---

## 5.8 Confirming the Hash Linked List Behavior

The struct collector produced values like:

```text
buckets_visited = 202
nodes_visited = 227
```

The node count being greater than the bucket count is strong evidence of linked chains/collisions.

A normal array would usually have:

```text
nodes_visited == buckets_visited
```

But here we had:

```text
nodes_visited > buckets_visited
```

and rows with `depth = 1`, for example:

```text
idx 65 depth 1
idx 85 depth 1
idx 88 depth 1
idx 106 depth 1
idx 141 depth 1
idx 164 depth 1
idx 167 depth 1
```

That confirms the traversal is not just an array scan. It is:

```text
bucket -> linked node -> linked node ...
```

This matches the old x86 collector's behavior.

---

## 6. Current List Type

The current structure is best described as:

```text
hash bucket linked list of game objects / entity-like objects
```

It is not a normal vector and not a spatial chunk list.

### Header

```text
owner + 0xE0:
+0x00 = bucket base pointer
+0x08 = bucket stride / node stride
+0x10 = bucket count
```

### Bucket / node

```text
node + 0x00 = next pointer with low-bit marker
node + 0x08 = key/hash/flags-like field
node + 0x10 = object pointer
```

### Traversal

```text
node = base + index * stride

while node:
next_raw = [node + 0x00]
object = [node + 0x10]
node = next_raw & ~1
```

---

## 7. Final Object Chains

### Object pointer from node

```text
object = [node + 0x10]
```

### Name chain

Confirmed:

```text
name = [[object + 0x98] + 0x0]
```

In the Python collector this is represented as:

```python
NAME_CHAINS = [
[0x98, 0x0],
]
```

### Position chain

Confirmed:

```text
component_vector_begin = [object + 0x130]
posObject = [component_vector_begin + 0x8]
position = posObject + 0xD0
```

In list form:

```python
POSITION_OWNER_CHAIN = [0x130, 0x8]
POSITION_VALUE_CHAIN = [0xD0]
```

### Position fields

```text
x = float[posObject + 0xD0]
y = float[posObject + 0xD4]
z = float[posObject + 0xD8]
```

---

## 8. Mapping Old x86 Collector to New x64 Collector

### Old x86

```python
world = get_address(module, [0x10FA5AC, 0x0])
node_info = get_address(world, [0x7C])

base = read_uint(node_info + 0x0)
step = read_uint(node_info + 0x4)
size = read_uint(node_info + 0x8)

for i in range(size):
node = base + i * step

while node:
next_raw = read_uint(node)
node = next_raw & 0xFFFFFFFE
```

### New x64

```python
owner = read_ptr(module_base + 0x01396BC0)

base = read_ptr(owner + 0xE0)
stride = read_u64(owner + 0xE8)
count = read_u64(owner + 0xF0)

for i in range(count):
node = base + i * stride

while node:
next_raw = read_ptr(node + 0x00)
object = read_ptr(node + 0x10)
node = next_raw & 0xFFFFFFFFFFFFFFFE
```

### Conceptual mapping

```text
old base_addr = new [owner + 0xE0]
old step = new [owner + 0xE8]
old size = new [owner + 0xF0]
old node = new node
old next & ~1 = new next_raw & ~1
new object = [node + 0x10]
```

---

## 9. Why the HP Chain Was Useful but Not the Final List

The HP chain gave us a reliable starting point because it touched many non-player entities.

We saw:

```text
tracker + 0x90 -> statSource/object-like pointer
HP double at some downstream object + 0xD8
```

But that was only a runtime update chain. It did not directly expose the root registry.

The HP chain helped us identify object relationships:

```text
tracker -> statSource -> component vector -> posObject
```

Then tracing who updates/walks those objects led us upward to:

```text
0x918B10 -> owner + 0xE0 hash list
```

So the HP instruction was the entry point into the investigation, not the final entity list.

---

## 10. Why the Current Collector Works

The current collector works because it uses the same data layout the game uses in assembly.

The assembly showed:

```asm
lea rdi, [rcx+0xE0]
mov rax, [rdi+0x10]
imul rax, [rdi+0x08]
add rax, [rdi]
mov rbx, [rbx+0x10]
```

The collector does the same thing:

```python
owner = read_ptr(module_base + 0x01396BC0)
base = read_ptr(owner + 0xE0)
stride = read_u64(owner + 0xE8)
count = read_u64(owner + 0xF0)

node = base + index * stride
object = read_ptr(node + 0x10)
```

Then it follows the linked chain:

```python
next_raw = read_ptr(node + 0x00)
node = next_raw & 0xFFFFFFFFFFFFFFFE
```

This is why it matches the old approach instead of inventing a new list model.

---

## 11. Filtering Notes

The hash list contains more than just mobs.

Examples seen in output:

```text
npc/biped_crabwarrior
npc/depths/fish_wanderer_02
npc/pentapod_shadowcaster
placeable/refiller_healthflask_interactive
portal_dungeonbasic
custom_heads_service
client/flatlands
/kiwi/bin/server_data/scenario/services/...
```

So if the goal is AutoAim or mob ESP, filter by name:

```python
name.startswith("npc/")
```

If the goal is general ESP, keep more categories:

```python
name.startswith("npc/")
name.startswith("placeable/")
name.startswith("portal")
name.startswith("collections/")
name.startswith("abilities/")
```

If the goal is only visible or meaningful entities, also filter out zero positions:

```python
not (x == 0.0 and y == 0.0 and z == 0.0)
```

---

## 12. Final Confirmed Offsets

```text
Global owner:
Trove_x64.exe + 0x01396BC0 -> owner

Owner:
owner + 0xE0 -> base
owner + 0xE8 -> stride
owner + 0xF0 -> count

Node:
node + 0x00 -> next pointer with low-bit marker
node + 0x10 -> object

Object:
object + 0x98 -> name pointer
object + 0x130 -> component vector begin

Component vector:
[object + 0x130] + 0x8 -> posObject

Position object:
posObject + 0xD0 -> x
posObject + 0xD4 -> y
posObject + 0xD8 -> z
```

---

## 13. Final Mental Model

The correct mental model is:

```text
Trove_x64.exe + 0x01396BC0
-> owner / world-like object
+0xE0 -> hash list header
+0x00 -> bucket base
+0x08 -> bucket stride
+0x10 -> bucket count

bucket = base + index * stride
+0x00 -> next linked node
+0x10 -> object

object
+0x98 -> name pointer
+0x130 -> component vector

component vector entry 0
+0x8 -> posObject

posObject
+0xD0 -> position vec3
```

Short version:

```text
The game does not expose a simple EntityList here.
It stores objects in a hash bucket linked list.
We discovered the owner from the game update flow, verified the list header from assembly, confirmed the node layout from traversal results, then replaced the hook with a direct global pointer to owner.
```



 
Source Code

import math
import time
from dataclasses import dataclass

import pymem
import pymem.process


PROCESS_NAME = "Trove_x64.exe"
OWNER_RVA = 0x1396BC0

# All confirmed chains are kept here.
OFFSETS = {
# owner = [module_base + OWNER_RVA]
"hash_base": [0xE0],
"hash_stride": [0xE8],
"hash_count": [0xF0],

# Hash node layout.
"node_next": [0x00],
"node_object": [0x10],

# Entity fields.
"name": [0x98, 0x0],
"position": [0x130, 0x8, 0xD0],
"scale": [0x130, 0x8, 0x178],
"health": [0x130, 0x108, 0xD8],

# Current best level/rank candidate.
# Normal mobs usually showed 1..3, boss candidate showed 4+.
"level": [0x130, 0xA8, 0x208],
}

NODE_MARK_MASK = 0xFFFFFFFFFFFFFFFE

MAX_BUCKETS = 8192
MAX_CHAIN_DEPTH = 256
MAX_TOTAL_NODES = 30000

NAME_MAX_LEN = 160
REFRESH_DELAY = 1.0

CLEAR_SCREEN = False
SKIP_ZERO_POSITION = True

# Empty = show everything with name and position.
# Mobs only:
# NAME_PREFIX_FILTERS = ["npc/"]
NAME_PREFIX_FILTERS = ["npc/"]


@dataclass(slots=True)
class Entity:
index: str
name: str
health: float | None
level: int | None
scale: float | None
x: float
y: float
z: float


def is_plausible_ptr(value: int) -> bool:
return 0x10000 <= value <= 0x00007FFFFFFFFFFF


def read_ptr(pm: pymem.Pymem, address: int) -> int:
try:
value = pm.read_ulonglong(address)
return value if is_plausible_ptr(value) else 0
except Exception:
return 0


def read_u64(pm: pymem.Pymem, address: int) -> int | None:
try:
return pm.read_ulonglong(address)
except Exception:
return None


def read_u32(pm: pymem.Pymem, address: int) -> int | None:
try:
return pm.read_uint(address)
except Exception:
return None


def read_float(pm: pymem.Pymem, address: int) -> float | None:
try:
value = pm.read_float(address)

if not math.isfinite(value):
return None

return value
except Exception:
return None


def read_double(pm: pymem.Pymem, address: int) -> float | None:
try:
value = pm.read_double(address)

if not math.isfinite(value):
return None

return value
except Exception:
return None


def read_latin1_string(pm: pymem.Pymem, address: int, max_len: int = NAME_MAX_LEN) -> str:
if not is_plausible_ptr(address):
return ""

try:
data = pm.read_bytes(address, max_len)
except Exception:
return ""

raw = data.split(b"\0", 1)[0]

if not raw:
return ""

try:
return raw.decode("latin-1", errors="replace").strip()
except Exception:
return ""


def get_module_base(pm: pymem.Pymem, module_name: str) -> int:
module = pymem.process.module_from_name(pm.process_handle, module_name)

if module is None:
raise RuntimeError(f"Module not found: {module_name}")

return module.lpBaseOfDll


def resolve_pointer_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> int:
current = base

for offset in chain:
if not is_plausible_ptr(current):
return 0

current = read_ptr(pm, current + offset)

if not is_plausible_ptr(current):
return 0

return current


def resolve_value_address(pm: pymem.Pymem, base: int, chain: list[int]) -> int:
if not chain or not is_plausible_ptr(base):
return 0

current = base

# Every offset except the last one is a pointer hop.
for offset in chain[:-1]:
current = read_ptr(pm, current + offset)

if not is_plausible_ptr(current):
return 0

address = current + chain[-1]
return address if is_plausible_ptr(address) else 0


def read_string_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> str:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return ""

# This path usually resolves directly to the latin-1 text address.
direct = read_latin1_string(pm, address)

if direct:
return direct

# Fallback if the last address stores a pointer to the text.
pointed = read_ptr(pm, address)
return read_latin1_string(pm, pointed)


def read_float_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> float | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

return read_float(pm, address)


def read_double_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> float | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

return read_double(pm, address)


def read_u32_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> int | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

return read_u32(pm, address)


def read_vec3_chain(pm: pymem.Pymem, base: int, chain: list[int]) -> tuple[float, float, float] | None:
address = resolve_value_address(pm, base, chain)

if not is_plausible_ptr(address):
return None

x = read_float(pm, address)
y = read_float(pm, address + 0x4)
z = read_float(pm, address + 0x8)

if x is None or y is None or z is None:
return None

if abs(x) > 1_000_000 or abs(y) > 1_000_000 or abs(z) > 1_000_000:
return None

if SKIP_ZERO_POSITION and abs(x) < 0.0001 and abs(y) < 0.0001 and abs(z) < 0.0001:
return None

return x, y, z


def read_hash_header(pm: pymem.Pymem, owner: int) -> tuple[int, int, int]:
base = resolve_pointer_chain(pm, owner, OFFSETS["hash_base"])

stride_address = resolve_value_address(pm, owner, OFFSETS["hash_stride"])
count_address = resolve_value_address(pm, owner, OFFSETS["hash_count"])

stride = read_u64(pm, stride_address)
count = read_u64(pm, count_address)

if not is_plausible_ptr(base):
raise RuntimeError(f"Bad hash base: 0x{base:X}")

if stride is None or stride <= 0 or stride > 0x400:
raise RuntimeError(f"Bad stride: {stride}")

if count is None or count <= 0 or count > MAX_BUCKETS:
raise RuntimeError(f"Bad count: {count}")

return base, int(stride), int(count)


def name_allowed(name: str) -> bool:
if not name:
return False

if not NAME_PREFIX_FILTERS:
return True

return any(name.startswith(prefix) for prefix in NAME_PREFIX_FILTERS)


def read_entity(pm: pymem.Pymem, bucket_index: int, depth: int, entity_object: int) -> Entity | None:
name = read_string_chain(pm, entity_object, OFFSETS["name"])

if not name_allowed(name):
return None

position = read_vec3_chain(pm, entity_object, OFFSETS["position"])

if position is None:
return None

health = read_double_chain(pm, entity_object, OFFSETS["health"])
level = read_u32_chain(pm, entity_object, OFFSETS["level"])
scale = read_float_chain(pm, entity_object, OFFSETS["scale"])

x, y, z = position

return Entity(
index=f"{bucket_index}:{depth}",
name=name,
health=health,
level=level,
scale=scale,
x=x,
y=y,
z=z,
)


def collect_entities(pm: pymem.Pymem, owner: int) -> tuple[list[Entity], dict[str, int]]:
base, stride, count = read_hash_header(pm, owner)

entities: list[Entity] = []
seen_nodes: set[int] = set()
seen_objects: set[int] = set()

stats = {
"base": base,
"stride": stride,
"count": count,
"nodes": 0,
"shown": 0,
"skipped": 0,
"duplicates": 0,
}

for bucket_index in range(count):
node = base + bucket_index * stride
depth = 0

while is_plausible_ptr(node) and depth < MAX_CHAIN_DEPTH:
if stats["nodes"] >= MAX_TOTAL_NODES:
break

if node in seen_nodes:
break

seen_nodes.add(node)
stats["nodes"] += 1

raw_next = resolve_pointer_chain(pm, node, OFFSETS["node_next"])
next_node = raw_next & NODE_MARK_MASK if raw_next else 0

entity_object = resolve_pointer_chain(pm, node, OFFSETS["node_object"])

if is_plausible_ptr(entity_object):
if entity_object in seen_objects:
stats["duplicates"] += 1
else:
seen_objects.add(entity_object)

entity = read_entity(pm, bucket_index, depth, entity_object)

if entity is None:
stats["skipped"] += 1
else:
entities.append(entity)

if raw_next == 1:
break

if not is_plausible_ptr(next_node):
break

if next_node == node:
break

node = next_node
depth += 1

entities.sort(
key=lambda item: (
item.name.lower(),
item.level if item.level is not None else 999999,
item.index,
)
)

stats["shown"] = len(entities)
return entities, stats


def format_health(value: float | None) -> str:
if value is None:
return "None"

if abs(value) >= 1_000_000:
return f"{value:.0f}"

if abs(value - round(value)) < 0.001:
return f"{value:.0f}"

return f"{value:.3f}"


def format_level(value: int | None) -> str:
if value is None:
return "None"

return str(value)


def format_scale(value: float | None) -> str:
if value is None:
return "None"

return f"{value:.3f}"


def print_entities(owner: int, stats: dict[str, int], entities: list[Entity]) -> None:
if CLEAR_SCREEN:
print("\033[2J\033[H", end="")

print(f"Owner : 0x{owner:X}")
print(f"Base : 0x{stats['base']:X}")
print(f"Stride : 0x{stats['stride']:X} ({stats['stride']})")
print(f"Count : {stats['count']}")
print(f"Nodes : {stats['nodes']}")
print(f"Shown : {stats['shown']}")
print(f"Skip : {stats['skipped']}")
print(f"Dup : {stats['duplicates']}")
print("-" * 150)

print(
f"{'#':<3} "
f"{'idx':<7} "
f"{'name':<64} "
f"{'health':>14} "
f"{'level':>7} "
f"{'scale':>8} "
f"position"
)

for row, entity in enumerate(entities, start=1):
position = f"({entity.x:.3f}, {entity.y:.3f}, {entity.z:.3f})"

print(
f"{row:<3} "
f"{entity.index:<7} "
f"{entity.name[:63]:<64} "
f"{format_health(entity.health):>14} "
f"{format_level(entity.level):>7} "
f"{format_scale(entity.scale):>8} "
f"{position}"
)

print()


def main() -> None:
pm = pymem.Pymem(PROCESS_NAME)
module_base = get_module_base(pm, PROCESS_NAME)

owner_ptr_address = module_base + OWNER_RVA
owner = read_ptr(pm, owner_ptr_address)

if not is_plausible_ptr(owner):
raise RuntimeError(f"Owner pointer is invalid: 0x{owner:X}")

print(f"Module base : 0x{module_base:X}")
print(f"Owner ptr address: 0x{owner_ptr_address:X}")
print(f"Owner : 0x{owner:X}")
print()

while True:
try:
entities, stats = collect_entities(pm, owner)
print_entities(owner, stats, entities)
time.sleep(REFRESH_DELAY)

except KeyboardInterrupt:
print("\nStopped")
break

except Exception as exc:
print(f"Collector error: {exc}")
time.sleep(REFRESH_DELAY)


if __name__ == "__main__":
main()


Hi Faisal32 Your script for auto finder no longer works and I updated it to x64 but I only found that

EntityList / World: Trove_x64.exe+1396BC0 (0.0000s) | LocalPlayer/Fixedbase: NOT FOUND (0.0083s)
Fishing / Lure: NOT FOUND (0.0101s) | Statics / Static Name: NOT FOUND (0.0169s)
InputBox/Chat: NOT FOUND (0.0073s) | Settings / Rendering: NOT FOUND (0.0070s)
Silent Aim / Vector X: NOT FOUND (0.0081s) | Silent Aim / Vector Y: Trove_x64.exe+48A888 (0.0016s)
Silent Aim / Vector Z: NOT FOUND (0.0082s)
#7 · 16d ago
Posts 1–7 of 7 · Page 1 of 1

Post a Reply

Similar Threads

  • Silent Aim & EntityListBy FAISAL32 in Trove Hacks & Cheats
    15Last post 7mo ago
  • I need EntityList for Call of Duty 1 Multiplayer, version 1.5 as 2025.By lookatrobot in Call of Duty 1 Hacks
    0Last post 10mo ago

Tags for this Thread

None