- Home /
Making A Patch System For My Game
So I have a working patch system using the Mac's terminal with the following commands(I'm adding this because I have seen a lot of similar questions around):
diff -Naur oldfile newfile > patch.txt // this will make a 'patch' in the execution directory
Then to apply the patch:
patch -p0 < patch.txt
I have verified this working. It works like a charm.
However now for my question. I want to do the same type of thing on Windows. I haven't been able to figure this one out. I tried using UnixUtils and it just breaks my files(Not a Valid windows application). I also looked at WinMerge but haven't figured out how it works. Is there anything that I can do to make a patch file that people could download and apply? Any help on this one would be great. I am looking for something pretty simple that I can manage.
I would like to figure out any information on this subject. I am already aware of the Crafty system and Hegza Patcher.
Update 12/27/2016:
This still has a few bugs but you can start looking into this system as well. This is a repo of many scripts that I have been working on an planning to release to the public. Far from complete but part of this collection is a Patching System (not done):
https://github.com/wesleywh/GameDevRepo/tree/master/Scripts/Patching
and a bit of documentation I put together for that patching library:
https://github.com/wesleywh/GameDevRepo/wiki/Patch-System
I have put this aside for now, but if there is still interest I will start working on this again.
I would look into C# libraries for handling textual differences (like google-diff-match-patch or diffplex) to write something built into your game for handling the patch files you distribute. I doubt that you really want your users to have to go to their ter$$anonymous$$al or other application to manually patch your game files.
Thanks for the reply. I haven't heard of any of these so I will definite have a look at these. Also I have an xcode application that I made that does the patching for me on the mac. While it is executing ter$$anonymous$$al commands it isn't visible to the user. I will be doing something similar, I hope, with some of these links you provided. I will read up on these to get a better idea what is available out there.
I still haven't found anything that can really do the job here. :( unfortunatly google-diff & diffplex is for text files only, that really isn't going to work for a binary diff need.
Is it possible to use Patch for Windows? The binary download comes with a bunch of folders but patch.exe in the bin/ subdirectory seems to work fine alone. Could just throw it into Strea$$anonymous$$gAssets and run it from there on Windows, and it would be nice to have the same workflow on $$anonymous$$ac and Windows.
I will look into this one. I also found this:
https://www.assetstore.unity3d.com/en/#!/content/18438
I honestly have no idea how good it is. $$anonymous$$aybe it will help me get started as well. I really am interested in Patch for Windows though that sounds interesting.
Answer by wesleywh · Sep 08, 2015 at 09:18 AM
Alright Folks I have done it! I have made a patch system that will work for me. This goes way beyond just making a patch file. You are more than welcome to use my code.
Background: I store all of my raw files on Google Drive (15GB free file hosting). This will essentially work with any server or file hosting out there.
Any spaces are considered invalid in file names for url usage. So I had to format the text that I read from files to make sure it met valid character criteria.
You can't edit files that are currently open, that's a security measure on all operating systems. So I had to create a separate patching system.
First off, right when the user opens my game I have it look in my Google Drive for a text file called "version.txt". This simply has the version in it like "1.0.1". I connect to my google drive, read the file, remove all white spaces, change it to a number, and finally check to make sure that number is greater than the current version number of the game running. It does this extremely quickly (normally less than a second but I added wait times for visuals).
To make a file on google drive downloadable for everyone you have to make it public, there is a lot of info about how to do this online -- very easy. Second you have to make a link out of the file like the following:
http://googledrive.com/host/<Google Drive Folder>/<filename>
mine looks like this:
http://googledrive.com/host/0B9EJGlco2Hy6fmUxQzN3ZVAtYkczRzA0S29ubjhfVGVBUFpHTm9HRmxOeldraDhhOXBCa1E/level0
Simply copy and paste the link into a browser to make sure it worked (It should start to immediately download).
Next, if it recognizes that version.txt version number is greater than the game version number than I notify the user that there is an update and give them the choice of whether or not to download it. If they accept to download it then I will close their game and open the patcher system.
The patcher system is much more complicated and is where all the magic takes place. It opens and reads a file called "patch.txt", it has the diff of the files.
How do I get a diff of my files?
This is where my code could be improved but what I did wasn't code it at all. I downloaded a free program for windows called "WinMerge". In Winmerge you can compair two folders and it will tell you everything that is different between the two folders. I simply copied that list of file names it produced into the patch.txt file seperate by enter/return spaces. On a Mac there is a "diff" command that you can run and make it spit out to a text file.
The patch.txt file simply will list the files that need to be downloaded for that patch. Now if a user gets very behind on patches I don't want to have to make them download the same file over and over to work through the patches to be up to date. So I made it so it will work by downloading all the files for all patches up to the current one, all at once ;). My patch.txt file looks like the following:
patch
1.0.0
level0
level2
level3
patch
1.0.1
level1
level2
leve3
leve4
So what it does is if you are on v0.0.9 you will need to download files: level0, level1, level2, level3, and level4. I makes sure that you don't download level2 or level3 twice. Cool eh?
With the files downloaded it will find the data folder of my game, called "The Dark Tower", and replace the files there with the new ones. It does this relatively so the patcher and "The Dark Tower" game can be anywhere on the computer and it will work. However, the patcher and the dark tower game need to be in the same folder in order for it to work. It's not sophisticated enough to scan your computer for the .exe file.
Also this needs to work on both Mac and Windows and they have a different folder structure. Have no fear! I have taken care of that as well, The Windows is fully tested and working but the Mac version isn't.
With that extensive explanation here is the code to check if there is an update (The Dark Tower Game.exe):
var buildVersion:String = "1.0.0";
private var updateDone:boolean = false;
private var downloadingFiles:boolean = false;
private var savePath:String = "";
private var originalMsg : String = "";
private var download:WWW;
private var patch:String = "";
private var rawDataFolder = "";
private var rawExc = "";
Start(){
checkForUpdates();
}
function Update(){
if(downloadingFiles){
updateMsg = originalMsg+"\n Download Progress: "+(download.progress * 100).ToString("#00.00")+"%\n";
}
}
//-------------------------FOR PATCHING -----------------------------------------------------
function checkForUpdates(){ //for the patcher. Check the version first.
buildVersion = "1.0.0";
updating = true;
showUpdateButton = false;
updateMsg = "\n\n\n\n\nChecking For Updates..."; //GUI update for user
yield WaitForSeconds(1.0f); //make it visible
var url = "https://googledrive.com/host/0B9EJGlco2Hy6fkU4QVVzMlQ0QnBqTmVjazZTR2dTSkJJal83eEhlYVJsU1puSjA2b3h3VU0/version.txt"; //txt file with version # in it
updateMsg = "\n\n\n\n\nEstablishing Connection...";//GUI update for user
var patchwww:WWW = WWW(url); //create new web connection to the build number site
yield patchwww; // wait for download to finish
var updateVersion = (patchwww.text).Trim();
if (updateVersion == buildVersion){ //check version
updateMsg = "\n\n\nCurrently update to date.";
yield WaitForSeconds(1.0f);
updating = false;
showGUI = true;
}
else {
patch = patchwww.text;
updateMsg = "Update Available.\n\n Current Version: "+buildVersion+"\n Patch Version: "+patchwww.text+"\n\n Would you like to download updates?\n\nThis will close this program \nand \n will launch the patching program.";
showUpdateButton = true;
}
}
//----------------------------------------------------------------
function applyUpdate(){
updateMsg = "\n\n\nIdentifying Platform";
if(Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor){
Application.OpenURL(Application.dataPath+"/../../TheDarkTowerPatcher.app");
}
else if(Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor){
Application.OpenURL(Application.dataPath+"/../TheDarkTowerPatcher.exe");
}
Application.Quit();
}
GUI.skin = myskin;
GUI.skin.font = font;
GUI.Label(Rect(Screen.width-100, Screen.height-80, 100, 80),"<size=20>"+guiTextVersion+"</size>");
if(updating){
if(backgroundImage)
GUI.DrawTexture(Rect(0,0,Screen.width,Screen.height), backgroundImage);
GUI.Box(Rect(Screen.width*0.5f-250,Screen.height*0.5f-250,500,300), "<size=25>"+updateMsg+"</size>");
if(showUpdateButton == true){
if(GUI.Button(Rect(Screen.width*0.5f-100,Screen.height*0.5f+82,200,100),"Update")){
GetComponent.<AudioSource>().clip = MouseClickSound;
GetComponent.<AudioSource>().Play();
showUpdateButton = false;
applyUpdate();
}
if(GUI.Button(Rect(Screen.width*0.5f-100,Screen.height*0.5f+192,200,100),"Cancle")){
GetComponent.<AudioSource>().clip = MouseClickSound;
GetComponent.<AudioSource>().Play();
updating = false;
showGUI = true;
}
}
if(errorOccured == true || updateDone == true){
if(GUI.Button(Rect(Screen.width*0.5f-100,Screen.height*0.5f+192,200,100),"Okay")){
GetComponent.<AudioSource>().clip = MouseClickSound;
GetComponent.<AudioSource>().Play();
updating = false;
showGUI = true;
}
}
}
Here is the code for the actual patcher that will patch The Dark Tower Game (The Dark Tower Game Patcher.exe)
#pragma strict
import System.Collections.Generic;
import System.Linq; //to make the ToList function work
var font:Font;
var patcherVersion:String = "Patcher Version 1.0.0";
var backgroundImage:Texture2D;
var myskin:GUISkin;
private var updateMsg:String = "";
private var showUpdateButton:boolean = false;
private var originalMsg:String = "";
private var savePath:String ="";
private var download:WWW;
private var errorOccured:boolean = false;
private var updateDone:boolean = false;
private var updating:boolean = false;
private var version:String = "";
function Start () {
checkVersion();
}
function checkVersion(){ //for the patcher. Check the version first.
updateMsg = "\n\n\n\n\nChecking the Current Version";
var buildVersion = Application.dataPath;
if(Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor){
buildVersion += "/../../"; //finds the exectuable
}
else if(Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor){
buildVersion += "/../"; //finds the exectuable
}
buildVersion += "version.txt";//have this file next to app/exe
version = System.IO.File.ReadAllText(buildVersion);
version = version.Replace(".","");
version = version.Trim();
updateMsg = "\n\n\n\n\nChecking For Updates..."; //GUI update for user
yield WaitForSeconds(1.0f); //make it visible
var url = "https://googledrive.com/host/0B9EJGlco2Hy6fkU4QVVzMlQ0QnBqTmVjazZTR2dTSkJJal83eEhlYVJsU1puSjA2b3h3VU0/version.txt"; //txt file with version # in it
updateMsg = "\n\n\n\n\nEstablishing Connection...";//GUI update for user
var patchwww:WWW = WWW(url); //create new web connection to the build number site
yield patchwww; // wait for download to finish
var convertedText = patchwww.text;
convertedText = convertedText.Replace(".","");
convertedText = convertedText.Replace("\n","");
convertedText = convertedText.Replace("\r","");
convertedText = convertedText.Trim();
var availablePatch = parseFloat(convertedText);
if (parseFloat(version) >= availablePatch){ //check version
updateMsg = "\n\n\nCurrently update to date.\n\nWould you like to launch The Dark Tower?";
updateDone = true;
}
else {
applyUpdate();
}
}
//----------------------------------------------------------------
function applyUpdate(){
updateMsg = "\n\n\nIdentifying Platform";
//Find the place to save all of the data
savePath = Application.dataPath;
if(Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor){
updateMsg = "\n\n\n\n\nIdentifying updates for the Mac platform\n\nEstablishing Connection...";
savePath += "/../../Contents/";
}
else if(Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor){
updateMsg = "\n\n\n\n\nIdentifying updates for the Windows platform\n\nEstablishing Connection...";
savePath += "/../TheDarkTower_Data/";
}
//open and read the patch file contents
var currPatch:String = "https://googledrive.com/host/0B9EJGlco2Hy6fkU4QVVzMlQ0QnBqTmVjazZTR2dTSkJJal83eEhlYVJsU1puSjA2b3h3VU0/patch.txt";
var patchlist:WWW = WWW(currPatch);
yield patchlist;
var fileList = patchlist.text;
var fileListArray:String[] = fileList.Split("\n" [0]); //every single line in patch.txt seperated into an array
var fileSaveSuccess = true;
//generate the list of files to download (removing duplicates)
var downloadList = Array();
var convertedString:float;
var addFiles:boolean = false;
var found = false;
for(var j =0; j < fileListArray.Length; j++){ //Look through list and build files to download list
if(fileListArray[j].Trim() == "patch"){
convertedString = parseFloat(fileListArray[(j+1)].Replace(".","").Trim());
if(convertedString > parseFloat(version)) {
addFiles = true;
j = j + 2;
}
}
if(addFiles == true){
for(var k = 0; k < downloadList.length; k++) {
if(downloadList[k] == fileListArray[j].Trim()){
found = true;
break;
}
}
if(found == false)
downloadList.Push(fileListArray[j].Trim());
}
found = false;
}
for (var i = 0; i < downloadList.length; i++) //<-----START DOWNLOADING INDIVIDUAL FILES
{
updateMsg = "\n\n\nFile "+(i+1)+" of "+downloadList.length+"\n\nDownloading "+downloadList[i];
var fileName = downloadList[i].ToString().Trim();
updateMsg = "\n\n\n\nDownloading File "+(i+1)+" of "+downloadList.length+"\n";
originalMsg = "\n\n\n\nDownloading File "+(i+1)+" of "+downloadList.length+"\n";
yield downloadFile(fileName);
if(errorOccured == true){
break;
}
}
if(errorOccured == false){
var versionURL = "https://googledrive.com/host/0B9EJGlco2Hy6fkU4QVVzMlQ0QnBqTmVjazZTR2dTSkJJal83eEhlYVJsU1puSjA2b3h3VU0/version.txt";
var versionText:WWW = WWW(versionURL);
yield versionText;
System.IO.File.WriteAllBytes (savePath+"/../version.txt", versionText.bytes);
updateMsg = "\n\n\n\nSuccessfully Updated. \n\nWould you like to relaunch The Dark Tower?";
updateDone = true;
}
}
//----------------------------------------------------------------
function downloadFile(file:String){
var rawDataFolder = "http://googledrive.com/host/0B9EJGlco2Hy6fjBneExJUXRNSDBKaUx6akcyeU9kMTV6Zlp1UXRlOUQwRXN1QTkzYm96d2s/";
var url:String = (rawDataFolder+file).ToString();
download = WWW(url); //download file from platforms raw folder
while(!download.isDone){
if(download.error){
errorOccured = true;
}
else {
updateMsg = originalMsg+"\n\n"+file+"\nDownload Progress: "+(download.progress * 100).ToString("##0.00")+"%";
}
}
yield download; // wait for download to finish
try{
updateMsg +="...saving...";
System.IO.File.WriteAllBytes (savePath+file, download.bytes);
updateMsg +="success!";
}
catch(error){
updateMsg ="Update Failed with error message:\n\n"+error.ToString();
errorOccured = true;
}
}
function openDarkTowerApp(){
var fileToOpen:String = Application.dataPath;
if(Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.OSXEditor){
fileToOpen += "/../../TheDarkTower.app";
}
else if(Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor){
fileToOpen += "/../TheDarkTower.exe";
}
Application.OpenURL(fileToOpen);
Application.Quit();
}
function OnGUI(){
GUI.skin = myskin;
GUI.skin.font = font;
GUI.Label(Rect(Screen.width-130, Screen.height-80, 100, 80),"<size=20>"+patcherVersion+"</size>");
if(backgroundImage)
GUI.DrawTexture(Rect(0,0,Screen.width,Screen.height), backgroundImage);
GUI.Box(Rect(Screen.width*0.5f-250,Screen.height*0.5f-250,500,300), "<size=25>"+updateMsg+"</size>");
if(updateDone == true){
if(GUI.Button(Rect(Screen.width*0.5f-100,Screen.height*0.5f+82,200,100),"Yes")){
updateDone = false;
openDarkTowerApp();
}
if(GUI.Button(Rect(Screen.width*0.5f-100,Screen.height*0.5f+192,200,100),"No")){
Application.Quit();
}
}
if(errorOccured == true){
if(GUI.Button(Rect(Screen.width*0.5f-100,Screen.height*0.5f+192,200,100),"Close")){
Application.Quit();
}
}
}
Final few thoughts. This patcher doesn't account for a "man in the middle" attack. Even though it says "https://" it isn't secure. Unity doesn't care about security like that. So you will have to do some research on certificates and authentication to protect your users against these kinds of attacks. However, for me it is fine since these downloads will more than likely not be on public networks and will be done over home networks that a much more secure(hopefully). Also this isn't a huge game just something between my friends so it's not going to be a problem.
Hope this teaches people that they want to know about patching and gives them an alternative to paying $30+ for a patching system. Granted those systems on the Unity Asset store are going to be much more robust than what I have got here but with a little more coding I think people can make it as strong as they would like.
Just a review of what you will need:
Patch system right next to game launcher.
version.txt in container folder (where patcher and game are)
version.txt on server (for me google drive)
patch.txt that contains all files for patch on server
updated version in code in game so it recognizes correct version(could change this around to look at version.txt in local directory)
Here is a picture of my final folder structure for windows
Good Luck!
EDIT: I have since made edits to my main game (not the patcher) that will check the version.txt file just like the patcher does, just for consistency sake. I would probably suggest to edit the code to do the same.... its easier to follow that way.
ya no problem. Glad to help another coder in need. This stuff was kind of a pain to find.
Great Work btw, However when I try to get the version file from online it gives me the HT$$anonymous$$L code ins$$anonymous$$d of the Version number. Do you know of a way to fix that because I can't find it anywhere. I tried your link and It pulled the data but $$anonymous$$e isn't working and I'm using google drive like you.
Just noticed this comment. Does the file have HT$$anonymous$$L code in it? If so the code is simply pulling down all of the text and not caring about tags such as those. So just put a number in that document or parse the text for HT$$anonymous$$L tags on your client side to display it correctly. There might be a better way to pull down a file if you want to display HT$$anonymous$$L code vs plain text.
The problem about using "http://googledrive.com/host/..." is that it will require the user to login into his "google account" before downloading. When I try to download files from that link (without logging in), all files are returned as a "Google 'Select Account' HT$$anonymous$$L" page and the update fails.
Hum. I guess I never tested it while I was not logged in. That's a great point to bring up. You don't need to use google drive though any repository will do. Just the above example uses the URL from google. Simply replace that with whatever repository you would like to use.
First of all, what a fantastic answer. If I could upvote *10, I would :D
Now the sad part...
I had this bookmarked for a long time, but when I finally implemented it in Unity5.4.0 , sadly I get the error :
Error : Failed to initialize player
Failed to load PlayerSettings (internal index #0). $$anonymous$$ost likely data file is corrupted, or built with mismatching editor and platform support versions.
Looking at all the previous comments, this did once work. Unless I've done something wrong in creating the list of changed files, it seems that there is some kind off ID system now regarding all the files in a build.
Searching the above error led me to a couple of s$$anonymous$$m forums, most noticeably for $$anonymous$$erbal Space Program. IIRC S$$anonymous$$m uses a type of version control like this patch method. However the only solutions I read was to completely uninstall/reinstall.
This question came up in the forums, so I have cross-linked this post in the hope that the community can work to evolve and resolve this system for everybody. Once again, thank you so much @wesleywh for putting in the time and effort in this answer.
Forum Question : https://forum.unity3d.com/threads/how-to-create-patches.436933/
Well thank you I appreciate that. This is quite old. This was originally made with unity 4 and since then many things have changed. So I do need to update this method. I am actually working on quite a large library of game ready scripts that I plan on releasing for free. I need to convert this to a better, more universal method. This was something that I did while I was still learning. The concepts while generally will remain the same the actual syntax for the scripts could be quite different now. I will see what I can come up with here in the near future to see if I can give a better answer. I know this is a common issue that I see often on the internet so I know it will help out many others. One thing I would like to change for example is where I store the files for download. I will probably not use Google Drive since that had issues in the past and google has recently stop supporting the web page method that I use here (Got an email from google about this a little while ago). So I am sure a few things will be different. I will probably link to my new method here once I figure out how I would like to do it.
Sorry for bumping this thread. But this was really useful. I did too encounter the Failed to load player settings problem. I fixed it by removing all the globalgamemanager files from the change list. Thank you wesleywh for your time to post this code! Really helped me and my $$anonymous$$m :)
Answer by wizard40 · Aug 03, 2017 at 08:06 AM
Use www.patchkit.net is the best way to create a patch for your game :D