How would you make images innaccessible from the file explorer?

Most games do not store their images as .bmp’s on the hard-drive, and it’s not a mystery why that is.

image

I’m using SDL2_Image and IMG_Load() to load .bmp’s from the hard-drive.

How would you do it so that users can’t see all of the things which are hidden in the game’s world just by clicking on the folder? Has this been done before in an SDL game?

Most engines seem to group together sprites in a big archive file and have some magical way of loading parts of that file. For older games, they might’ve used more platform-specific code to load assets from files having proprietary file-type.

Is this something SDL developers talk about?

Do I need to spend months getting smarter and writing my own stuff? Is there a clever way to do this quickly?

If your game can extract assets from a bulk file, the user will be able to do so too. The question is what to do so that, on the one hand, the game can load assets quickly and conveniently, and on the other hand, to best protect the content against intruders, if you wish to do so.


Simple solution—create a bulk file, give it a strange extension (or don’t use any), write the assets to it one by one, add an array with asset offsets at the beginning of the file. It is easy to generate and modify such a file, very easy to read those assets by the game, but it cannot be opened by any known program, so most users will drop out at this stage.

You can also encrypt individual resources by generating a private key from known and immutable data that exists on the user’s computer—each user will have different data. You can use, for example, the parameters of the hard drive and the game installation path, file name and asset name/ID. All assets/files would be encrypted during game installation. It will be even more difficult for users to access resources.


Remember that no matter what you do, a determined and curious user will sooner or later figure out how the files are structured, how to extract resources from them and how to modify them to mod the game. If the user needs to debug the game code, they will do so. And then they will create an editor and everyone will be able to conveniently mod the game.

So this is something to think about. Perhaps it is not worth inventing complicated solutions, but simply hide the resources and politely ask the user not to spoil the fun for themselves and others (as in the case of “Binding of Isaac”, as you showed).

1 Like

How much effort do you want to go through to obfuscate? Here’s something you could set up in a day:

Create a program that splits each image into 16x16 pixel squares. Use a random number generator to rotate those squares by multiples of 90 degrees (it’s ok, we will save that number for later).
Use GUID’s as file names when you save each square using IMG_SavePNG, randomized but retrievable since you have the name when you saved it.
You should design the scrambling program to output a final string of text that works as a map that can be used to put everything back together again in the right order. Place that string of text directly into your game’s code. The binary can still be scraped for text data, so you could also come up with some simple encryption algorithm on that string.
Your game now needs a loadImage("originalImageName.png") function that references that string, loads each square, reverses the rotation, and then stamps them all back onto a target texture.

This is just enough to block the average player from spoiling themselves by accident (Like if they did a general search for all png images on their computer).
There’s no way to stop it in reality since “Let’s Play” videos are a thing. (And let’s not talk about DLL injection). Honestly, if they’re digging into your project files or watching “let’s Plays” then it’s their fault for spoiler-ing themselves. Your focus should be on making the game fun to play, don’t bother yourself too much about someone seeing your artwork early.

1 Like

Furthermore, remember that this is a good thing for your game and trying to shut it down is not a wise idea. 30 years later, there’s still an active DOOM community, entirely because mods became available almost immediately. 13 years later, Skyrim is going stronger than ever, again because of mods. How many games without mods remain relevant for even 5 years, let alone multiple decades?

1 Like

Why are you quoting me? This is @fizz asking about how to hide data from the user and block the ability to mod the game.

Game resources should be hidden so that players can play what the developers have carefully prepared and optimized, and so that they don’t steal them for their own use (or, worse still, publish them online) and spoil the game secrets online, depriving others of the fun. If someone is creating a game that is not intended to be modded, they should secure their assets.

I will also prepare my game in such a way that it is not possible to easily view and extract assets. At most, in the future, when all the secrets of the game are known, I will provide my own tools—those that I created and used to build the game, and that I am confident in working.

The ones that are great will remain, not the ones that can be modded.

1 Like

I mean, the easiest way is to just not use BMPs and other common file formats in the first place. This also frees you to store assets in ways that are easier and/or faster to load, use file structures that meet your game/engine’s specific needs, store images that use hardware formats like DXT, RGB10A2, etc.

This will also make it harder for people who want to steal your game assets for their own use. Yeah, yeah, yeah, anyone who tries hard enough to steal your stuff will eventually succeed, but this will deter the majority of asset thieves.

As for Doom: while id Software wasn’t trying to obfuscate the game assets, they weren’t shipping a loose collection of .PCX and .WAV files either.

3 Likes

[Edit: having read the OP again, I realize I’m way off-base with my code below since the original question was more about batching files together than obfuscation. But the data could easily be stored in a large xml file, and SDL_RWFromConstMem could be key to loading/handling chunks of those files at a time. I’ll leave things like this for now, but I’ll work on a better option to post tomorrow.]

Here’s something simple yet complex:
Most image formats expect to have a “magic number” in a determined position in the file. So shift it by one byte and change the file extension.

This following code will specifically take a test file named “test.png” in the active folder and does the above to it by adding the letter ‘D’ to the beginning of the data.
It outputs the obfuscated data as “output.flux”.
Then the retrieve function reads that file, ignoring the first letter while reading it, so the data is the original PNG data again. I am pretty confident that any file explorer or image viewer will throw a fit when they try to open “output.flux”

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <string>
#include <vector>
#include <iostream>
#include <fstream>

SDL_Texture * textureFromVec(SDL_Renderer * screen, std::vector <Uint8> &data)
{
	SDL_RWops* imageStream = SDL_RWFromConstMem(&data[0], data.size());
	if(imageStream == NULL)
	{
		SDL_Log("Failed to load texture data in textureFromVec(), datasize: %d", data.size());
	}
	return IMG_LoadTexture_RW(screen, imageStream, 1);
}

void obfuscate(std::string imagePath, std::string output)
{
	std::ifstream ifile(imagePath.c_str(), std::fstream::binary);
	ifile.seekg(0, ifile.end);
	size_t fileSize = ifile.tellg();
	ifile.seekg(0, ifile.beg);
	std::vector <Uint8> data;
	data.push_back({'D'});

	data.reserve(fileSize + 4);
	ifile.read(reinterpret_cast<char *>(&data[1]), fileSize);
	ifile.close();

	std::ofstream ofile(output.c_str(), std::fstream::binary);
	ofile.write(reinterpret_cast<char *>(&data[0]), fileSize + 1);
	ofile.close();
}

// returns NULL on failure. Returned texture must be freed by caller.
SDL_Texture * retrieve(SDL_Renderer * screen, std::string filePath)
{
	std::ifstream ifile(filePath.c_str(), std::fstream::binary);
	ifile.seekg(0, ifile.end);
	size_t fileSize = ifile.tellg();
	ifile.seekg(0, ifile.beg);
	std::vector <Uint8> data;
	data.reserve(fileSize + 4);
	char * temp = new char[fileSize + 1];
	ifile.read(temp, fileSize);
	for(size_t i = 1; i < fileSize; i ++)
	{
		data.push_back(temp[i]);
	}
	ifile.close();

	SDL_Texture * image = textureFromVec(screen, data);
	if(image == NULL)
	{
		SDL_Log("Error on textureFromVec: %s", SDL_GetError());
	}
	
	return image;
}

int main()
{
	std::string testImagePath = "test.png";
	SDL_Init(SDL_INIT_EVERYTHING);
	IMG_Init(IMG_INIT_PNG);
	SDL_Window * win = SDL_CreateWindow("imageFromStr", 10, 10, 400, 400, SDL_WINDOW_SHOWN);
	SDL_Renderer * screen = SDL_CreateRenderer(win, -1, SDL_RENDERER_PRESENTVSYNC);

	obfuscate(testImagePath, "output.flux");
	SDL_Texture * texture = retrieve(screen, "output.flux");

	SDL_Rect pos = {10, 10, 200, 200};

	SDL_SetRenderDrawColor(screen, 120, 120, 120, 255);
	bool run = true;
	while(run)
	{
		SDL_Event ev;
		while(SDL_PollEvent(&ev))
		{
			switch(ev.type)
			{
				case SDL_QUIT:
					run = false;
					break;
			}
		}
		SDL_RenderClear(screen);
		SDL_RenderCopy(screen, texture, NULL, &pos);
		SDL_RenderPresent(screen);
	}
	SDL_DestroyTexture(texture);
	SDL_DestroyWindow(win);
	IMG_Quit();
	SDL_Quit();
}
1 Like

Here’s a slightly rushed example of batching binary files. I skipped out on implementing xml in order to reduce complexity, and I omitted any attempts to obfuscate things.
What the following code does is take a list of files, slams them all together into one *.dat file, and provides a table of contents *.tab that tells the batcher where to jump to in the *.dat file to start and end reading.

So, give this program at least two image files at the command line and it will batch/append them together. (any future files you feed to it will be added to the batch file as well, there are currently no tests for repeated files)
The demo is a preview of the first file in the batch: Hitting any key will retrieve the next file in the batch using the original path-string to that image as a kind of hash or access key. [TO DO: come up with an image naming scheme that makes the table easier to access].

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>

#include <fstream>
#include <vector>
#include <string>

class batcher
{
	public:
	batcher(std::string name)
	{
		batchName = name;
	}
	void addImages(std::vector <std::string> files)
	{
		size_t len = files.size();
		for(size_t i = 0; i < len; i ++)
		{
			filePaths.push_back(files[i]);
		}
	}
	void addFile(std::string file)
	{
		filePaths.push_back(file);
	}

	// saves two project files: name.dat contains binary data, name.tab stores reference postions in that data file
	void save()
	{
		std::string tablePath = batchName;
		tablePath += ".tab";
		std::string batchPath = batchName;
		batchPath += ".dat";
		std::ofstream batchOut(batchPath.c_str(), std::ifstream::app | std::ifstream::binary);
		std::ofstream tableOut(tablePath.c_str(), std::ifstream::app);
		size_t start = batchOut.tellp();
		size_t end = start;
		size_t len = filePaths.size();
		SDL_Log("Appending %d files", len);
		// for each file, read its data into batch.
		for(size_t i = 0; i < len; i ++)
		{
			start = end;
			std::ifstream ifile(filePaths[i], std::ifstream::binary);
			ifile.seekg(0, ifile.end);
			size_t fileSize = ifile.tellg();
			end += fileSize;
			ifile.seekg(0, ifile.beg);
			char * temp = new char[fileSize];
			ifile.read(temp, fileSize);
			ifile.close();

			batchOut.write(temp, fileSize);
			std::string tableInfo = filePaths[i];
			tableInfo += " ";
			tableInfo += std::to_string(start);
			tableInfo += " ";
			tableInfo += std::to_string(end);
			tableInfo += "\n";
			tableOut << tableInfo;
			delete [] temp;
		}
		batchOut.close();
		tableOut.close();
	}

	
	SDL_Texture * retrieve(SDL_Renderer * screen, std::string fileName)
	{
		std::string tablePath = batchName;
		tablePath += ".tab";
		std::ifstream tableIn(tablePath.c_str());
		size_t start = 0;
		size_t end = 0;
		while(tableIn.good())
		{
			// this is not well optimized, but should be OK for less that a thousand files.
			std::string temp;
			tableIn >> temp;
			if(temp.find(fileName) != std::string::npos)
			{
				// found the file in the batch table
				tableIn >> start;
				tableIn >> end;
				tableIn.close();
			}
		}
		if(end != 0)
		{
			size_t fileSize = end - start;
			std::string batchPath = batchName;
			batchPath += ".dat";
			std::ifstream dataIn(batchPath.c_str());
			dataIn.seekg(start);
			char * data = new char [fileSize];
			dataIn.read(data, fileSize);

			SDL_RWops* imageStream = SDL_RWFromConstMem(data, fileSize);
			if(imageStream == NULL)
			{
				SDL_Log("Failed to load texture data in texture from data, datasize: %d", fileSize);
			}
			SDL_Texture * retImage = IMG_LoadTexture_RW(screen, imageStream, 1);
			delete [] data;
			return retImage;

		}
		return NULL;
	}
	std::vector <std::string> getFileList()
	{
		std::vector <std::string> fileList;
		std::string tablePath = batchName;
		tablePath += ".tab";
		std::ifstream ifile(tablePath.c_str());
		std::string line;
		while(getline(ifile, line))
		{
			std::string filePath = line.substr(0, line.find(' '));
			fileList.push_back(filePath);
		}
		return fileList;
	}
	public:
	std::string batchName;
	std::vector <std::string> filePaths;
};


int main(int argc, char ** argv)
{
	SDL_Init(SDL_INIT_EVERYTHING);
	IMG_Init(IMG_INIT_PNG);

	batcher pack("level_01");
	std::vector <std::string> imagePaths = pack.getFileList();
	for(int i = 1; i < argc; i ++)
	{
		imagePaths.push_back(argv[i]);
		pack.addFile(argv[i]);
	}
	if(argc > 1)
	{
		pack.save();
	}
	if(imagePaths.size() < 1)
	{
		SDL_Log("Please provide at least one file to start the batch file.");
		IMG_Quit();
		SDL_Quit();
		exit(0);
	}

	SDL_Window* win = SDL_CreateWindow("Batching Image Files", 10, 10, 1000, 1000, SDL_WINDOW_SHOWN);
	SDL_Renderer * screen = SDL_CreateRenderer(win, -1, SDL_RENDERER_PRESENTVSYNC);


	SDL_Texture * texture = pack.retrieve(screen, imagePaths[0]);
	SDL_Rect pos = {10, 10, 300, 300};

	int cursor = 0;
	bool run = true;
	while(run)
	{
		SDL_Event ev;
		while(SDL_PollEvent(&ev))
		{
			switch(ev.type)
			{
				case SDL_KEYDOWN:
					cursor ++;
					if(cursor >= imagePaths.size())
					{
						cursor = 0;
					}
					SDL_DestroyTexture(texture);
					texture = pack.retrieve(screen, imagePaths[cursor]);
					break;
				case SDL_QUIT:
					run = false;
					break;
			}
		}
		SDL_RenderClear(screen);
		SDL_RenderCopy(screen, texture, NULL, &pos);
		SDL_RenderPresent(screen);
	}
	SDL_Quit();
}

Notes for improvement:

  • Ideally you would set up your resource manager to handle the reading of the batch file so that you could load the entire batch file once into memory, and set up retrieve to grab the resource as needed from RAM.
  • The retrieve function I provided works specifically with images, but if you had it dish out data instead of SDL_Texture pointers, then you could actually store any type of file in the batch. (audio, text, video, etc)

Hope this helps, and thanks for the fun prompt.

1 Like

If you don’t want to write your own code to pack files together, there’s always @icculusPhysicsFS

2 Likes