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
# 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.
```
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()
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()



