Last active
August 29, 2015 14:10
-
-
Save bradbeattie/276db65f08730f296cb3 to your computer and use it in GitHub Desktop.
Parse and modify prison architect save files
This file contains hidden or 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
# These functions will allow you to | |
# - Read in Prison Architect save files | |
# - Modify the read content | |
# - Save content back into Prison Architect save file formats | |
import re | |
from collections import OrderedDict | |
from pprint import PrettyPrinter | |
TOKEN_BEGIN = 1 | |
TOKEN_END = 2 | |
TOKEN_CONTENT = 3 | |
CATEGORY_PROTECTED = "Protected" | |
CATEGORY_MINSEC = "MinSec" | |
CATEGORY_MEDSEC = "Normal" | |
CATEGORY_MAXSEC = "MaxSec" | |
CATEGORY_SUPERMAX = "SuperMax" | |
PRISONER_CATEGORIES = (CATEGORY_PROTECTED, CATEGORY_MINSEC, CATEGORY_MEDSEC, CATEGORY_MAXSEC, CATEGORY_SUPERMAX) | |
RELEASED = "Released" | |
DANGEROUS_REPUTATIONS = set(("Volatile", "Instigator", "CopKiller", "Deadly")) | |
DANGEROUS_MISCONDUCTS = set(("InjuredPrisoner", "InjuredStaff", "Murder", "Destruction")) | |
def parse_tokens(tokens): | |
content = OrderedDict() | |
index = 0 | |
if type(tokens) is list: | |
tokens = enumerate(tokens) | |
for index, token in tokens: | |
if token[0] == TOKEN_END: | |
break | |
elif token[0] == TOKEN_BEGIN: | |
secondIndex, secondToken = next(tokens) | |
if secondToken[0] != TOKEN_CONTENT: | |
raise Exception("Expected a CONTENT token after a BEGIN token") | |
parsed_tokens, subcontentLength = parse_tokens(tokens) | |
content.setdefault(secondToken[1].strip('"'), []) | |
content[secondToken[1].strip('"')].append(parsed_tokens) | |
else: | |
secondIndex, secondToken = next(tokens) | |
content.setdefault(token[1].strip('"'), []) | |
content[token[1].strip('"')].append(secondToken[1]) | |
return content, index | |
def escaped(name): | |
return ('"%s"' % name) if " " in name else name | |
def generate_prison_format(parsedPrison, indent=0): | |
content = [] | |
for name, values in parsedPrison.iteritems(): | |
for value in values: | |
if type(value) is OrderedDict: | |
subContent = generate_prison_format(value, indent + 1) | |
if len(subContent) >= 2: | |
content.append("BEGIN %s" % escaped(name)) | |
for subEntry in subContent: | |
content.append("".join((" ", subEntry))) | |
content.append("END") | |
else: | |
content.append("BEGIN %s END" % " ".join([escaped(name)] + subContent)) | |
else: | |
content.append(" ".join((escaped(name), str(value)))) | |
return content | |
def security_hearings(parsedPrison): | |
for id, entry in parsedPrison["Objects"][0].iteritems(): | |
if type(entry[0]) == OrderedDict and entry[0].get("Type", None) == ["Prisoner"]: | |
prisonerGrade = grade_prisoner(parsedPrison, id, entry) | |
if prisonerGrade == RELEASED: | |
print "EARLY PAROLE FOR", id | |
entry[0]["Bio"][0]["Sentence"][0] = float(entry[0]["Bio"][0]["Served"][0]) | |
elif prisonerGrade is not None: | |
if entry[0]["Category"][0] != prisonerGrade: | |
print "CHANGING", id, "FROM", entry[0]["Category"][0], "TO", prisonerGrade | |
entry[0]["Category"][0] = prisonerGrade | |
def grade_prisoner(parsedPrison, id, entry): | |
# Don't grade unrevealed prisoners | |
reputationRevealed = entry[0]["Bio"][0]["ReputationRevealed"][0] == "true" | |
if not reputationRevealed: | |
return | |
# Collect relevant prisoner information | |
daysInPrison = float(entry[0]["Experience"][0]["Experience"][0]["TotalTime"][0].rstrip(".")) / 1440 | |
sentence = float(entry[0]["Bio"][0].get("Sentence", [100])[0]) | |
served = float(entry[0]["Bio"][0].get("Served", [0])[0]) | |
parole = float(entry[0]["Bio"][0]["Parole"][0]) | |
reputations = set(entry[0]["Bio"][0].get("Reputation", [])) | |
highReputations = set(entry[0]["Bio"][0].get("ReputationHigh", [])) | |
programsPassed = sum(int(program[0].get("Passed", [0])[0]) for program in entry[0]["Experience"][0]["Results"][0].values()) | |
dangerous = reputations & DANGEROUS_REPUTATIONS or highReputations & DANGEROUS_REPUTATIONS | |
try: | |
misconducts = len([ | |
True | |
for misconduct in parsedPrison["Misconduct"][0]["MisconductReports"][0][id][0]["MisconductEntries"][0].values() | |
if type(misconduct[0]) is OrderedDict and misconduct[0]["Convicted"][0] == "true" | |
]) | |
except KeyError: | |
misconducts = 0 | |
try: | |
violent_behavior = len([ | |
True | |
for misconduct in parsedPrison["Misconduct"][0]["MisconductReports"][0][id][0]["MisconductEntries"][0].values() | |
if type(misconduct[0]) is OrderedDict and misconduct[0]["Type"][0] in DANGEROUS_MISCONDUCTS | |
]) | |
except KeyError: | |
violent_behavior = 0 | |
# Grading a prisoner costs $100 | |
parsedPrison["Finance"][0]["Balance"][0] = "%g" % (float(parsedPrison["Finance"][0]["Balance"][0]) - 100) | |
# Consider early release for well-behaved prisoners | |
if not dangerous and not violent_behavior and served + programsPassed > parole + misconducts and programsPassed - misconducts > 2: | |
parsedPrison["Finance"][0]["Balance"][0] = "%g" % (float(parsedPrison["Finance"][0]["Balance"][0]) + 1000) | |
return RELEASED | |
# Don't recategorize Protected prisoners | |
category = entry[0]["Category"][0] | |
if category == CATEGORY_PROTECTED: | |
return | |
# Violent or dangerous MinSec prisoners will get boosted up to MedSec | |
if category == CATEGORY_MINSEC: | |
return CATEGORY_MEDSEC if violent_behavior or dangerous else CATEGORY_MINSEC | |
# Legendary prisoners have two options: MaxSec or SuperMax | |
if "Legendary" in highReputations: | |
if violent_behavior or dangerous: | |
return CATEGORY_SUPERMAX | |
else: | |
return CATEGORY_MAXSEC if daysInPrison > 8 else category | |
# All others have two options: MedSec or MaxSec | |
else: | |
if violent_behavior or dangerous: | |
return CATEGORY_MAXSEC | |
else: | |
return CATEGORY_MEDSEC if daysInPrison > 4 else category | |
inFile = r"Alpha27-600.prison" | |
outFile = r"Alpha27-600-new.prison" | |
with open(inFile, "r") as oldPrisonFile, open(outFile, "w") as newPrisonFile: | |
scanner=re.Scanner([ | |
(r"\s+", None), | |
(r"BEGIN", lambda scanner,token:(TOKEN_BEGIN, token)), | |
(r"END", lambda scanner,token:(TOKEN_END, token)), | |
(r'".*"', lambda scanner,token:(TOKEN_CONTENT, token)), | |
(r"[^\s]*", lambda scanner,token:(TOKEN_CONTENT, token)), | |
]) | |
tokens, remainder = scanner.scan(oldPrisonFile.read()) | |
parsedPrison = parse_tokens(tokens)[0] | |
security_hearings(parsedPrison) | |
newPrisonFile.write("\n".join(generate_prison_format(parsedPrison))) |
Todo:
- Is there a way to get prisoner grading out or do I have to generate that myself?
- Ideally, each security hearing would be a 1h session with the security chief, each parole hearing through referrals by the security chief to the warden, making them useful again. I can create the first session with the current modding tools, but I can't read the necessary misconduct data nor create a referral to the warden. Ugh.
Hey thanks heaps for posting this - I've been mulling over in my head the best way to parse a .prison file for the last couple of days (for completely different reasons), I'll use your code as a reference point! I'm guessing from syntax this is in Python?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Okay, maybe overkill, but I got frustrated by the lack of decent modding tools in Prison Architect. So I set out to write something that could (A) read in the save file format, (B) make modifications to the data, and then (C) write those modifications back into the format in question.
The net result here is an example of a mod I'd like to be able to build in game (1h security hearing sessions for prisoners with a psychologist that can automatically adjust their security ratings). Until such time as the mod tools are improved, this'll do just fine.