Skip to content

Development Notes: Save Games

Nico Borgsmüller edited this page Dec 12, 2020 · 1 revision

Copied from my OneDrive

Cities: Skylines Multiplayer Game Notes

Introduction

This document contains notes I have taken when deconstructing the game.

I’ll be using the tool "JetBrains dotPeek" to decompile and view the source code.

Game code can be found in the "Assembly-CSharp.dll" file.

Level Loading

When clicking in Load Game on the main menu, the game calls:

UIView.library.ShowModal("LoadPanel")

This loads the LoadPanel.cs class. Under the quick load method (and others) we have the following code:

if (SavePanel.isSaving || !Singleton<LoadingManager>.exists || Singleton<LoadingManager>.instance.m_currentlyLoading)
      return;
    Package.Asset latestSaveGame = SaveHelper.GetLatestSaveGame();
    SaveGameMetaData saveGameMetaData = !(latestSaveGame != (Package.Asset) null) ? (SaveGameMetaData) null : latestSaveGame.Instantiate<SaveGameMetaData>();
    if (saveGameMetaData == null)
      return;
    SimulationMetaData ngs = new SimulationMetaData()
    {
      m_CityName = saveGameMetaData.cityName,
      m_updateMode = SimulationManager.UpdateMode.LoadGame
    };
    if (latestSaveGame.package != (Package) null && latestSaveGame.package.GetPublishedFileID() != PublishedFileId.invalid)
      ngs.m_disableAchievements = SimulationMetaData.MetaBool.True;
    Singleton<LoadingManager>.instance.LoadLevel(saveGameMetaData.assetRef, "Game", "InGame", ngs, false);

which can be split into a few key parts

  1. Looks like this contains a lot of metadata, which can be serialized and deserialized.
SimulationMetaData ngs = new SimulationMetaData()
{
      m_CityName = saveGameMetaData.cityName,
      m_updateMode = SimulationManager.UpdateMode.LoadGame
};
  1. This is the method of the most intrest.
Singleton<LoadingManager>.instance.LoadLevel(saveGameMetaData.assetRef, "Game", "InGame", ngs, false);

Appears to take in 5 parameters, is called under QuickLoad() and LoadRoutine().

Coroutine LoadLevel(Package.Asset asset, string playerScene, string uiScene, SimulationMetaData ngs, bool forceEnvironmentReload = false)

These values should be the default.

forceEnvironmentReload = false
uiScene = "InGame"
playerScene = "Game"

At this point we need to figure out how the game loads the level (so it can be sent across), it seems like both the Package.Asset?? and SimulationMetaData will be sent (which will do stuff like set the city name, time etc.)

LoadPanel seems to load the levels in with

this.GetListingData(this.m_SaveList.selectedIndex);

Quick load uses a method called

SaveHelper.GetLatestSaveGame()

to get the latest saved game, then loads it using

SaveGameMetaData saveGameMetaData = !(latestSaveGame != (Package.Asset) null) ? (SaveGameMetaData) null : latestSaveGame.Instantiate<SaveGameMetaData>();

This save helper has a few other methods which look useful.

GetMapsOnDisk()
GetSavesOnDisk()
FileInfo[] GetFileInfo(string location)

These also look useful

private static readonly string m_SaveExtension = ".ccs";
 private static readonly string m_MapExtension = ".cct";

But the maps appear to be saved as .crp files? It's a bit weird, I see the .crp files and it loads them from there?

 public static List<Package.Asset> GetSavesOnDisk()
  {
    List<Package.Asset> assetList = new List<Package.Asset>();
    FileInfo[] fileInfo1 = SaveHelper.GetFileInfo(DataLocation.saveLocation);
    if (fileInfo1 != null)
    {
      foreach (FileInfo fileInfo2 in fileInfo1)
      {
        if (string.Compare(Path.GetExtension(fileInfo2.Name), SaveHelper.m_SaveExtension, StringComparison.OrdinalIgnoreCase) == 0)
        {
          Package.Asset asset = new Package.Asset(Path.GetFileNameWithoutExtension(fileInfo2.Name), fileInfo2.FullName);
          assetList.Add(asset);
        }
      }
    }
    return assetList;
  }

Need to figure out how to get the file for the current level that's open.

PauseMenu.SaveGame() -> 
SavePanel.

Actually GetLatestSavedGame() may actually work, gotta figure out a way to force the game to save first.

Notes from root#0042

As discussed in discord, here's the way to load a savegame (from filepath). I'll separate them into a few key-points I found out while creating this.

Path from existing savegames

On exising savegames (=assets) the location should be stored in one of those variables:

var sourcePath = asset.pathOnDisk;
if (string.IsNullOrEmpty(sourcePath))
       sourcePath = asset.package.packagePath;

That way you could copy existing single player games for multiplayer. I would rather not "just use" the SP save as it is. This is necessary because of cloud saves I think. Not sure tho.

Loading game from path

There are 3 steps that need to be done to load a savegame from a filepath:

  1. Create a package from the path
  2. Create SaveGameMetaData
  3. Create SimulationMetaData

Now with these 3 things its then possible to call the LoadLevel method to load the level. Probably this has to be called in the right thread. I created this 4 months ago so I'm not sure about that anymore. Anyways. Here is the way that worked for me:

// Ensure that the LoadingManager is ready. Don't know if thats really necessary but doesn't hurt either.
Singleton<LoadingManager>.Ensure();

// saveName should be the path to the file (full qualified, including save file extension)
var package = GetSaveGameFromPath(saveName);
var savegameMetaData = GetMetaDataFromPackage(package);

_logger.Trace($"CityName: {savegameMetaData.cityName}");
_logger.Trace($"Timestamp: {savegameMetaData.timeStamp}");

var metaData = new SimulationMetaData()
{
	m_CityName = savegameMetaData.cityName,
	m_updateMode = SimulationManager.UpdateMode.LoadGame,
	m_environment = UIView.GetAView().panelsLibrary.Get<LoadPanel>("LoadPanel").m_forceEnvironment
};

Singleton<LoadingManager>.instance.LoadLevel(savegameMetaData.assetRef, "Game", "InGame", metaData, false);

Helper methods

private Package GetSaveGameFromPath(string path)
{
	if (!File.Exists(path))
		throw new FileNotFoundException(path);

	var package = new Package(Path.GetFileNameWithoutExtension(path), path);

	return package;
}

private SaveGameMetaData GetMetaDataFromPackage(Package package)
{
	if (package == null)
		throw new ArgumentNullException(nameof(package));

	var asset = package.Find(package.packageMainAsset);
	var metaData = asset.Instantiate<SaveGameMetaData>();

	return metaData;
}