Hiding My (Python) Text-Strings

My current main project has a number of text-strings (object-names, character-thoughts, lore-entries, etc.). These are, for the most part, implemented as Python-files containing string-variables, with the variable-names then being used to fetch a given piece of text. In addition, there are a few that are simply text-files. (The latter generally being longer pieces of text, like in-game documents, I think.)

With the thought that the game–or a later one built using similar elements–might one day perhaps be localised, I want it to be fairly straightforward to add text-localisations. Thus, my build-process excludes the directory containing these files from the usual process applied to Python modules, and instead just has it copied into the build’s folder. One can thus just drop a text-localisation in beside the current English text and have it be detected by the game.

However, this means that all of the game’s text is available for easy reading.

Hence my question: how might I hide this text from a cursory examination?

The approach needn’t be rigorous. If players want to get at the text, they likely eventually will, I daresay. I just want the text to not be available at a glance.

Does anyone have any suggestions?

Simply packing my language-files into a multifile is one thought–but I don’t know whether it’s feasible to import a module from a multifile. I suppose that I could just load the files as text, and then “eval” all of it…

You can in fact import modules from a .mf file, using the VFSImporter. That approach lets you compress and even (if you are so inclined) encrypt them. It might be the most convenient option given your current setup.

Python also lets you import modules from a .zip file, if you give it some other extension than .zip to make it less obvious.

Perhaps in the future we will add support to deploy-ng for deploying code into separately loadable .pyd/.so modules.

Another approach is a custom data format that you feed through zlib with a custom header prepended or so.

Ah, very interesting–thank you for all that!

I didn’t know about the VFSImporter–that sounds like just the thing indeed! Excellent! :slight_smile:

I’m still impressed by the sheer number of features that Panda has.

Another option may be to use gettext and ship the localization files instead of Python files.

Interesting, thank you–I didn’t know about that module. Right now I have code that handles fetching the appropriate string, so I’m inclined to stick with what I have (especially given the availability of the VFSImporter). However, if I do run into problems with my solution, it may be worth considering turning to that module instead!

I’m having a little trouble using the VFSImporter, and would appreciate some help, please. I’m not at all sure that I’m using it correctly, and what I’m doing doesn’t seem to be working. I’ve tried myriad combinations of paths, to no avail thus far.

Here’s what I have thus far:

My text-strings are stored in various files within a directory-structure, all contained in a directory named for its language. Something like this:

 |__ Misc.py
 |__ level1.py
 |__ etc...
 |   |
 |   |__ intro.py
 |   |__ etc...

I’ve now applied the “multify” command to that language-folder, resulting in a multifile by the same name (e.g. “eng.mf”). Judging by the output, it includes the language-name directory (e.g. “eng/”) in its storage, so all file-paths within it presumably start with that directory.

Then, in my code I do the following:

# In my imports:
from panda3d.core import VirtualFileSystem
# ...
from direct.showbase import VFSImporter

# Elsewhere...
vfs.mount(Filename(Common.LANGUAGE_DIRECTORY + "/" + language + ".mf"),

# And elsewhere still...

# The "module" variable has the name of the Python-module
#  to be imported.
# For example, if we were importing "Misc.py" above,
#  it would contain "Misc".
importer = VFSImporter.VFSImporter(Filename("%s/%s.mf/%s" % (Common.LANGUAGE_DIRECTORY, Common.language, Common.language)))
loader = importer.find_module(module)
result = loader.load_module(module)

Alas, the result of “find_module” seems to consistently be “None”, regardless of the variations in the above that I’ve tried. :/

A print-out of “vfs.getMounts()” includes the value “Languages/eng.mf”, and as noted above the multifile seems to contain the “eng” directory, hence the Filename given to the VFSImporter constructor.

The use of “register” above is based on this post. The last section of code above is based on the code found in “AppRunner.py”.

When I was using Python-importation, I instead used the following line:

result = __import__("%s.%s.%s" % (Common.LANGUAGE_DIRECTORY, Common.language, entry), fromlist = module)

(The “entry” variable in this case holds the same string as the “module” variable, I believe.)

Based on a look at the “register” function, I had thought that perhaps this was enough, that such importations were redirected through VFSImporter–but that doesn’t seem to be the case for “__import__”, unless I’m missing something. I did try just “import” (instead of “__import__”), but that produced a syntax-error; being less familiar with that version of dynamic importation, I may well have been misusing it.

So… where am I going wrong? :/

Don’t try to use any of the VFSImporter calls. Just do VFSImporter.register() once and then do your usual __import__ line. You don’t need to instantiate it or pass it anything or anything of the sort.

If it’s not working, you need to verify that the directory containing the package is on sys.path and that the directory structure inside the Multifile is such that after mounting the right structure is in place. Also, of course, make sure that the __init__.py files are present.

Running python with the -v flag may help shed some light at what Python is doing.

Ah, thank you! You were right about the directory: I think that I tried the “__import__” approach before I realised that the “Language” directory wasn’t part of the mount.

In short, having mounted “Languages/<some language>.mf”, instead of calling the following:

result = __import__("%s.%s.%s" % (Common.LANGUAGE_DIRECTORY, Common.language, entry), fromlist = module)

I should have been calling something like this:

result = __import__("%s.%s" % (Common.language, module), fromlist = module)

Note the lack of “Common.LANGUAGE_DIRECTORY” there.

And indeed, that seems to work!

Thank you very much for your help. :slight_smile:

I don’t understand why you don’t use regular encryption. For example: https://pypi.org/project/pycrypto/

Encryption seems like overkill, to me. I don’t require that the files be secure–just not immediately obvious. As I said, if players want to get to them, then they likely will, eventually. Thus just putting the text into a MultiFile and loading from there seems to me like a good approach!

You can write logic with the passage of the game files will be decrypted.

Of course, but I don’t require anything like that here. Hidden is enough. Why go the lengths of encryption?

And if anyone wants to make localization? Although you can create utilities for this. But it would seem to me if the file is outside the archive it will be made easier.

If they want to localise, then multify will extract the relevant data, and pack it again into a language-Multifile. That said, if that’s too inconvenient, then I might take rdb’s suggestion above of having Python import from a zip file (albeit a renamed one).