Content Warning is a co-op horror game where you film spooky stuff with your friends to try and go viral. You can buy it on steam. However, please remember that this article is a technical exchange for learning purposes. Please DO NOT affect other people’s gaming experience.

It is assumed here that readers already have relevant practical experience in ESP. We’ll mainly focuses on describing how to obtain the coordinates of ghosts on the screen with uniref and how to draw it non-intrusively using Python, rather than the principles of ESP.

Get World Coordinates

Because the game is not packaged using IL2CPP, we can use dnSpy to decompile it at the source code level. Let’s take a look at these class names.

Ghost's World Coordinates

As we can see, there are multiple classes starting with Bot_, representing different types of ghosts. In the BotHandler class, there is a bots member, which is of type List<Bot>. Then this variable probably stores all the ghost instances in the current level.

Ghost's World Coordinates

Fortunately, there is also a static member instance in this class. This way we don’t have to work hard to find the instance address of BotHandler. So let’s write some basic code first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from uniref import WinUniRef

ref = WinUniRef("Content Warning.exe")

def read_list(instance: int) -> list:
cls_list = ref.find_class_in_image("mscorlib", "System.Collections.Generic.List`1")
cls_list.set_instance(instance)
list_items = cls_list.find_field("_items").value
list_size = cls_list.find_field("_size").value
return ref.injector.mem_read_pointer_array(list_items + 0x20, list_size)

def get_all_bots() -> list:
cls_bot_handler = ref.find_class_in_image("Assembly-CSharp", "BotHandler")
cls_bot_handler.set_instance(cls_bot_handler.find_field("instance").value)

list_instance = cls_bot_handler.find_field("bots").value
return read_list(list_instance)

print(get_all_bots())

Now we have all active instances of Bot. So how to get the coordinates of these objects? Note that Bot inherits from MonoBehaviour, which means that we can obtain the Vector3 coordinates of the object through bot.transform.position in the C# style. So the question becomes how to translate this C# code into uniref based code.

Here is the solution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Vector3:

def __init__(self, data: list) -> None:
self.x = data[0]
self.y = data[1]
self.z = data[2]

def __str__(self) -> str:
return f"x: {round(self.x, 3)} y: {round(self.y, 3)} z: {round(self.z, 3)}"

def get_transform_instance(obj_instance: int):
cls_component = ref.find_class_in_image("UnityEngine.CoreModule", "UnityEngine.Component")
cls_component.set_instance(obj_instance)
method_get_transform = cls_component.find_method("get_transform")
return method_get_transform()

def get_object_position(obj_instance: int) -> Vector3:
cls_transform = ref.find_class_in_image("UnityEngine.CoreModule", "UnityEngine.Transform")
cls_transform.set_instance(get_transform_instance(obj_instance))
method_get_position = cls_transform.find_method("get_position_Injected")

out = ref.injector.mem_alloc()
method_get_position(args=(out,))
position = Vector3(ref.injector.mem_read_float_array(out, 3))

ref.injector.mem_free(out)
return position

print(get_object_position(get_all_bots()[0]))

As the function get_transform_instance shows, getting the transform of an object is simple. Just assign the instance address to its base class Component and call the get_transform method. However, when getting the position member of the Transform instance, you cannot directly call the get_position method, otherwise the game will crash. After my testing, CE also has this problem.

Let’s see how this function is implemented.

Ghost's World Coordinates

It calls the get_position_Injected function internally and passes a variable to receive the return value. btw, this function is implemented in C and you can find it by analyzing UnityPlayer.dll. So we allocate a new page and use its address as the parameter to directly call get_position_Injected, which can achieve the same effect without crashing.

World To Screen

You can use traditional methods and calculate the screen coordinates yourself based on the camera matrix. You can also use the WorldToScreenPoint function that comes with the Unity camera to calculate it like I did. So now, our goal is to call Camera.main.WorldToScreenPoint.

World To Screen

World To Screen

In order to prevent crashes, we also prepare parameters ourselves to call WorldToScreenPoint_Injected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def world_to_screen(pos: Vector3):
cls_camera = ref.find_class_in_image("UnityEngine.CoreModule", "UnityEngine.Camera")
cls_camera.set_instance(cls_camera.find_method("get_main")())
method_world_to_screen = cls_camera.find_method("WorldToScreenPoint_Injected")

params = ref.injector.mem_alloc()
ref.injector.mem_write_float_array(params, [pos.x, pos.y, pos.z])

method_world_to_screen(args=(params, 2, params + 0x100))
screen = Vector3(ref.injector.mem_read_float_array(params + 0x100, 3))

ref.injector.mem_free(params)
return screen

world_pos = get_object_position(get_all_bots()[0])
print(world_to_screen(world_pos))

ref.injector.mem_alloc will allocate a memory page by default. So we can use the beginning of the page as the first parameter, and the offset 0x100 to receive the return value.

Draw Text

Now we have everything, just need to draw it on the screen. As writing d3d hook in Python is painful, I prefer to use pygame to draw a transparent window on top.

Here is the simplest framework.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import time
import pygame
import win32api
import win32con
import win32gui

FPS = 60
screen_width = win32api.GetSystemMetrics(0)
screen_height = win32api.GetSystemMetrics(1)

pygame.init()
pygame.mixer.init()
pygame.display.set_caption("Overlay")
screen = pygame.display.set_mode((screen_width, screen_height))
clock = pygame.time.Clock()

bg_color = (0, 0, 0)
hwnd = pygame.display.get_wm_info()["window"]
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,
win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED)
win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*bg_color), 0, win32con.LWA_COLORKEY)
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)

def draw_text(screen, text, size, x, y):
color = (255, 254, 0)
font = pygame.font.SysFont("simhei", size)
text_fmt = font.render(text, 1, color)
screen.blit(text_fmt, (x, y))

while True:
clock.tick(FPS)
screen.fill(bg_color)
screen.set_alpha(128)

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()

# draw here
draw_text(screen, "hello", 50, 500, 500)

pygame.display.flip()
time.sleep(0.01)

So we get this. looks pretty good right?

Ghost's World Coordinates

In the same way, you can use pygame.draw.line or pygame.draw.rect to draw something else.

Let’s put it all together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
while True:
clock.tick(FPS)
screen.fill(bg_color)
screen.set_alpha(128)

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()

bots = get_all_bots()
for idx, bot in enumerate(bots):
world_pos = get_object_position(bot)
screen_pos = world_to_screen(world_pos)
screen_x, screen_y = screen_pos.x - 20, screen_height - screen_pos.y - 20

# check if the target is within the screen
if screen_x < screen_width and screen_y < screen_height and screen_pos.z >= 0:
draw_text(screen, f"bot_{idx+1}", 25, screen_x, screen_y)

pygame.display.flip()
time.sleep(0.01)

How does it feel?

Ghost's World Coordinates

You can get the full code here. But for educational purposes, the code does not have any optimizations. If you run it, you will find that the coordinates update very slowly. You can optimize by moving the unnecessary find_* function call outside the loop. Good luck!