Created
February 8, 2020 22:38
-
-
Save Hulzenga/2da728dea43d1e206423b95c44e78a4f to your computer and use it in GitHub Desktop.
Python script to extract Disco Elysium Passive checks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Where i can find Disco Elysium.json?
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
Output: