CensoredUsername / unrpyc

A ren'py script decompiler
Other
861 stars 157 forks source link

Instantiating classes with arguments #102

Closed jackmcbarn closed 3 years ago

jackmcbarn commented 3 years ago

While looking into #101, I tried to decompile the rest of that game, and a whole bunch of stuff is broken. In particular, for a lot of the files, even dumping them doesn't work. They give the following error:

Traceback (most recent call last):
  File "/home/jackmcbarn/unrpyc/unrpyc.py", line 164, in worker
    no_pyexpr=args.no_pyexpr, comparable=args.comparable, translator=translator, tag_outside_block=args.tag_outside_block, init_offset=args.init_offset)
  File "/home/jackmcbarn/unrpyc/unrpyc.py", line 129, in decompile_rpyc
    ast = read_ast_from_file(in_file)
  File "/home/jackmcbarn/unrpyc/unrpyc.py", line 107, in read_ast_from_file
    data, stmts = magic.safe_loads(raw_contents, class_factory, {"_ast", "collections"})
  File "/home/jackmcbarn/unrpyc/decompiler/magic.py", line 599, in safe_loads
    encoding=encoding, errors=errors).load()
  File "/usr/lib/python2.7/pickle.py", line 864, in load
    dispatch[key](self)
  File "/usr/lib/python2.7/pickle.py", line 1089, in load_newobj
    obj = cls.__new__(cls, *args)
  File "/home/jackmcbarn/unrpyc/decompiler/magic.py", line 113, in __new__
    raise FakeUnpicklingError("{0} was instantiated with unexpected arguments {1}, {2}".format(cls, args, kwargs))
FakeUnpicklingError: <class 'store._ELSE_COND'> was instantiated with unexpected arguments ('True',), {}

The following code elsewhere in the game looks relevant:

python early:
    class _ELSE_COND(str):
        def __new__(cls, *args, **kwargs):
            if not hasattr(cls, 'instance'):
                cls.instance = super(_ELSE_COND, cls).__new__(cls, *args, **kwargs)
            return cls.instance
    ELSE_COND = _ELSE_COND("True")

At first glance, it seems that magic doesn't handle overriding __new__ properly.

CensoredUsername commented 3 years ago

There's no nice way to handle overriding __new__ unfortunately. When the object gets pickled the pickling machinery can just note down the path of the object and with what arguments to call __new__ on it to get it to the current state.

Except usually this wouldn't be an issue as the script data shouldn't normally contain user-created datastructures. So this leads me to guess that people are subclassing renpy.ast nodes in their script files again, so their user code changes what is expected in the pickled datastructures. There's really no good way to solve that one :| .

The user code shown is indeed responsible, although it's missing the part that causes it to show up in the script files. If you can give magic a decent-enough implementation of __new__ (looking at it just informing magic that it's a subclass of str should be fine). See this for an example of how to do it.

That should hopefully get it to actually load, but considering they went through all this effort for some weird singleton it might not be the only one. And after that you're probably going to run into an error of unknown ast node 'store.WhatEver' when you find what caused user-created runtime datastructures to be in the serialized script file data.

There's unfortunately no nice way of handling this, you're basically dealing with a softmodded ren'py parser. The only way is to search the script files which do load (and there will be at least one, which will have a python early statement in there to figure out what kind of node they added to the thing and how to reverse it back to text.

jackmcbarn commented 3 years ago

Hacked around in a game-specific branch.