Skip to content

Instantly share code, notes, and snippets.

@Hulzenga
Created February 8, 2020 22:38
Show Gist options
  • Save Hulzenga/2da728dea43d1e206423b95c44e78a4f to your computer and use it in GitHub Desktop.
Save Hulzenga/2da728dea43d1e206423b95c44e78a4f to your computer and use it in GitHub Desktop.
Python script to extract Disco Elysium Passive checks
import re
import collections
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
#-------------------------------------------#
# extract check counts from game code #
#-------------------------------------------#
'''
Difficulty checks are coded in the following format:
[3]
0 Field data
1 string title = "DifficultyPass"
1 string value = "2"
0 int type = 1
1 string typeString = "CustomFieldType_Number"
[4]
0 Field data
1 string title = "Actor"
1 string value = "386"
0 int type = 5
1 string typeString = "CustomFieldType_Actor"
[5]
The value under the "DifficultyPass" sets the difficulty of the check
The value below "Actor" sets the skill which is checked, in this case "Suggestion" (see ACTOR_DICT)
'''
#mapping of actor codes to in-game skills, note that there are 5 different perception checks!
#('normal', smell, hearing, taste, sight)
ACTOR_DICT = {
376: 'Conceptualization',
377: 'Logic',
378: 'Encyclopedia',
379: 'Rhetoric',
380: 'Drama',
381: 'Visual Calculus',
382: 'Empathy',
383: 'Inland Empire',
384: 'Volition',
385: 'Authority',
386: 'Suggestion',
387: 'Esprit de Corps',
388: 'Endurance',
389: 'Physical Instrument',
390: 'Shivers',
391: 'Pain Threshold',
392: 'Electro-Chemistry',
393: 'Half Light',
394: 'Hand-Eye Coordination',
395: 'Reaction Speed',
396: 'Savoir Faire',
397: 'Interfacing',
398: 'Composure',
399: 'Perception',
400: 'Perception',
401: 'Perception',
402: 'Perception',
403: 'Perception',
}
#highest check value is 14
HIGH_CHECK = 15
#check count arrays
passive_checks = {k: [0]*HIGH_CHECK for k in ACTOR_DICT.values()}
antipassive_checks = {k: [0]*HIGH_CHECK for k in ACTOR_DICT.values()}
#helper fun to diagnose unparsable DifficultyPasses
def print_error(message, deq):
print(message)
print('***')
for e in deq:
print(e.rstrip())
print('***')
print()
print()
#disco.txt is the exported asset "MonoBehaviour Disco Elysium" from the \disco_Data\StreamingAssets\AssetBundles\Windows\dialoguebundle file
with open('disco.txt', mode='r', encoding='utf-8') as disco_file:
#regex to capture int value from line
regex_int_value = re.compile(r'.*string value = "(\d+)"')
#read lines in a sliding window
WINDOW_PRE = 21
WINDOW_POST = 21
WINDOW_CENTER = WINDOW_PRE
WINDOW_LENGTH = WINDOW_PRE+WINDOW_POST+1
deq = collections.deque()
i = 0
count = 0
for line in disco_file:
#build up window
if (i < WINDOW_LENGTH):
i += 1
deq.append(line)
continue
#add new line to window and drop oldest
deq.popleft()
deq.append(line)
if("DifficultyPass" in (deq[WINDOW_CENTER])):
#check if anti-passive
is_passive = True
for j in range(0, WINDOW_CENTER):
if 'string title = "Antipassive"' in deq[j]:
is_passive = False
break
#get difficulty pass value
diff = 0
try:
diff = int(regex_int_value.match(deq[WINDOW_CENTER + 1]).groups()[0])
except:
print_error('could not extract DifficultyPass value', deq)
break
#find relevant actor
actor = ''
for j in range(WINDOW_CENTER+1, WINDOW_LENGTH):
if ('Actor' in deq[j]):
try:
actor_code = int(regex_int_value.match(deq[j+1]).groups()[0])
actor = ACTOR_DICT[actor_code]
break
except:
print_error('could not extract Actor from text', deq)
else:
print_error('could not find Actor -- consider extending Window Size', deq)
break
if is_passive:
passive_checks[actor][diff] += 1
else:
antipassive_checks[actor][diff] += 1
#--------------------#
# Plot results #
#--------------------#
#in-game skill order
DISPLAY_ORDER = ['Logic',
'Encyclopedia',
'Rhetoric',
'Drama',
'Conceptualization',
'Visual Calculus',
'Volition',
'Inland Empire',
'Empathy',
'Authority',
'Esprit de Corps',
'Suggestion',
'Endurance',
'Pain Threshold',
'Physical Instrument',
'Electro-Chemistry',
'Shivers',
'Half Light',
'Hand-Eye Coordination',
'Perception',
'Reaction Speed',
'Savoir Faire',
'Interfacing',
'Composure']
#map of DifficultyPass values to in-game difficulty
DIFF_MAP = {
0: 6,
1: 8,
2: 10,
3: 12,
4: 14,
5: 16,
6: 18,
7: 20,
8: 7,
9: 9,
10: 11,
11: 13,
12: 15,
13: 17,
14: 19
}
#skill grid dimensions
X = 6
Y = 4
#chart colours
ROW_COLOURS = ['skyblue', 'orchid', 'indianred', 'gold']
BG_COLOUR = 'black'
SPINE_COLOUR = 'white'
for tup in [('Passive Checks', passive_checks), ('AntiPassive Checks', antipassive_checks)]:
#unpack tuple
title = tup[0]
checks = tup[1]
#setup all Y*X subplopts
fig, ax = plt.subplots(nrows=Y, ncols=X, sharex=True)
ax[0,0].set_xticks(list(range(6,20+1,2)))
#set figure title, size and, background colour
fig.suptitle(title, color=SPINE_COLOUR, fontsize=22)
fig.set_size_inches(24, 13.5)
fig.set_facecolor('black')
#go through all skills
for y in range(0,Y):
for x in range(0, X):
#skill being drawn
skill = DISPLAY_ORDER[x + X*y]
#calculate total number of skill checks and find highest skill check
count = sum(checks[skill])
max_skill = max(map(lambda dc: DIFF_MAP[dc], [ j for (i,j) in zip(checks[skill], range(15)) if i > 0 ]), default=0)
#style chart
col = ROW_COLOURS[y]
ax[y, x].set_title(DISPLAY_ORDER[X*y+x], color=col)
ax[y, x].set_facecolor(BG_COLOUR)
for spine in ax[y,x].spines.values():
spine.set_color(SPINE_COLOUR)
ax[y, x].tick_params(colors=SPINE_COLOUR)
#allow max of 5 y ticks and only use integer values
ax[y, x].yaxis.set_major_locator(ticker.MaxNLocator(nbins=5, integer=True))
#remove y ticks if there is no data
if count == 0:
ax[y,x].tick_params(axis='y',which='both', left=False, labelleft=False)
## else:
## ax[y,x].set_yscale('log')
#draw bar chart
ax[y, x].bar(list(map(lambda d: DIFF_MAP[d], range(0,15))), checks[skill], color=col)
#add stat data
ax[y, x].text(0.75, 0.85, f'Σ={count}', color=SPINE_COLOUR, transform = ax[y, x].transAxes)
ax[y, x].text(0.75, 0.75, f'M={max_skill}', color=SPINE_COLOUR, transform = ax[y, x].transAxes)
#save figures with max window size
mng = plt.get_current_fig_manager()
mng.window.state('zoomed')
fig.savefig(title + ".png", facecolor=fig.get_facecolor(), transparent=False)
plt.show()
@Hulzenga
Copy link
Author

Hulzenga commented Feb 9, 2020

Output:
Passive Checks
AntiPassive Checks

@RikCost
Copy link

RikCost commented Apr 26, 2024

Where i can find Disco Elysium.json?

@Hulzenga
Copy link
Author

I don't remember a JSON file, it's been a while though. To get the dialogue bundle from the game see line #84. I also have a Reddit post with the raw skill check numbers, in case you were looking for those. Another user reformatted the results into a nicer chart.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment