Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add change host method in Co-op game #50

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,60 @@ Dependencies:
- Python 3
- [uesave-rs](https://github.com/trumank/uesave-rs)

### Migrate server

Command:
`python fix-host-save.py <uesave.exe> <save_path> <new_guid> <old_guid>`
`python fix_host_save.py <uesave.exe> <save_path> <new_guid> <old_guid>`
`<uesave.exe>` - Path to your uesave.exe
`<save_path>` - Path to your save folder
`<new_guid>` - GUID of the player on the new server
`<old_guid>` - GUID of the player from the old server

Example:
`python fix-host-save.py "C:\Users\John\.cargo\bin\uesave.exe" "C:\Users\John\Desktop\my_temporary_folder\2E85FD38BAA792EB1D4C09386F3A3CDA" 6E80B1A6000000000000000000000000 00000000000000000000000000000001`
`python fix_host_save.py "C:\Users\John\.cargo\bin\uesave.exe" "C:\Users\John\Desktop\my_temporary_folder\2E85FD38BAA792EB1D4C09386F3A3CDA" 6E80B1A6000000000000000000000000 00000000000000000000000000000001`

### Change Co-op host

Prepare:

Fill `uesave_path`、`save_path` and `player_list` in *config.json*, for example:

```json
{
"uesave_path": "C:\\Users\\John\\.cargo\\bin\\uesave.exe",
"save_path": "C:\\Users\\John\\Desktop\\my_temporary_folder\\2E85FD38BAA792EB1D4C09386F3A3CDA",
"player_list": [
{
"GUID": "6E80B1A6000000000000000000000000",
"name": "Arthur"
Comment on lines +50 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified the script and dumped .sav to .json, then I found there's no player name inside the save file. How do I identify which player owns the save file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "name" serves an internal purpose within the script, primarily to simplify the command for running the script and make it more memorable. The exported JSON files still rely on the GUID to distinguish players.

},
{
"GUID": "F5A892D9000000000000000000000000",
"name": "John"
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
}
]
}
```

Command:

`python change_coop_host.py John`

## How to migrate a co-op save to a Windows dedicated server

Expand Down Expand Up @@ -79,6 +124,21 @@ Steps:

If someone wants to make sure this kind of migration works and then create the instructions to do it, I'd accept a PR for them.

## How to change host in Local 4-player Co-op game

Prerequisites:

- Install the dependencies [above](#usage).
- The original host need to get a standard GUID, by joining a friend's world or building and joining a dedicated server, more details can refer to the tutorial above.

Steps:

1. **Make a backup of your save**
2. Fill `uesave_path`、`save_path` and `player_list` in *config.json*, `player_list` should include all players who have joined this save, you can add more GUID-name pairs if needed. Note that all GUID are standard GUID, not `00000000000000000000000000000001`.
3. Run *change_coop_host.py* with a `host` parameter, which can be a GUID or just a nickname for convenience, after that this guy will become the new host.
4. Send this new save to the new host, and replace *LocalData.sav* from this new save file with that from the original save file of the new host, since *LocalData.sav* saves personal map and tutorial data.
5. Let the new host starts the game and invites you and other friends.

## Known bugs

### ~~\[Guild bug\]~~
Expand Down
163 changes: 163 additions & 0 deletions change_coop_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import argparse
import json
import os
from pathlib import Path
import sys

from fix_host_save import sav_to_json, json_to_sav, clean_up_files

# GUID of current host is 00..01, we need to get his standard GUID.
def old_host(player_list, player_save_path):
all_guid_list = [player['GUID'] for player in player_list]
current_guid_list = [save.stem for save in player_save_path.iterdir()]
old_host = set(all_guid_list) - set(current_guid_list)
return old_host.pop()

# Get new host's standard GUID.
def new_host(name_or_guid, player_list):
for player in player_list:
if player['name'] == name_or_guid or player['GUID'] == name_or_guid:
return player['GUID']
return None

def change_guid(old_guid, new_guid, save_path, uesave_path):
# Apply expected formatting for the GUID.
new_guid_formatted = '{}-{}-{}-{}-{}'.format(new_guid[:8], new_guid[8:12], new_guid[12:16], new_guid[16:20], new_guid[20:]).lower()
old_level_formatted = ''
new_level_formatted = ''

# Player GUIDs in a guild are stored as the decimal representation of their GUID.
# Every byte in decimal represents 2 hexidecimal characters of the GUID
# 32-bit little endian.
for y in range(8, 36, 8):
for x in range(y-1, y-9, -2):
temp_old = str(int(old_guid[x-1] + old_guid[x], 16))+',\n'
temp_new = str(int(new_guid[x-1] + new_guid[x], 16))+',\n'
old_level_formatted += temp_old
new_level_formatted += temp_new

old_level_formatted = old_level_formatted.rstrip("\n,")
new_level_formatted = new_level_formatted.rstrip("\n,")
old_level_formatted = list(map(int, old_level_formatted.split(",\n")))
new_level_formatted = list(map(int, new_level_formatted.split(",\n")))

level_sav_path = save_path + '/Level.sav'
old_sav_path = save_path + '/Players/'+ old_guid + '.sav'
new_sav_path = save_path + '/Players/' + new_guid + '.sav'
level_json_path = level_sav_path + '.json'
old_json_path = old_sav_path + '.json'

# Convert save files to JSON so it is possible to edit them.
sav_to_json(uesave_path, level_sav_path)
sav_to_json(uesave_path, old_sav_path)
print('Converted save files to JSON')

# Parse our JSON files.
with open(old_json_path) as f:
old_json = json.load(f)
with open(level_json_path) as f:
level_json = json.load(f)
print('JSON files have been parsed')

# Replace all instances of the old GUID with the new GUID.

# Player data replacement.
old_json["root"]["properties"]["SaveData"]["Struct"]["value"]["Struct"]["PlayerUId"]["Struct"]["value"]["Guid"] = new_guid_formatted
old_json["root"]["properties"]["SaveData"]["Struct"]["value"]["Struct"]["IndividualId"]["Struct"]["value"]["Struct"]["PlayerUId"]["Struct"]["value"]["Guid"] = new_guid_formatted
old_instance_id = old_json["root"]["properties"]["SaveData"]["Struct"]["value"]["Struct"]["IndividualId"]["Struct"]["value"]["Struct"]["InstanceId"]["Struct"]["value"]["Guid"]

# Level data replacement.
instance_ids_len = len(level_json["root"]["properties"]["worldSaveData"]["Struct"]["value"]["Struct"]["CharacterSaveParameterMap"]["Map"]["value"])
for i in range(instance_ids_len):
instance_id = level_json["root"]["properties"]["worldSaveData"]["Struct"]["value"]["Struct"]["CharacterSaveParameterMap"]["Map"]["value"][i]["key"]["Struct"]["Struct"]["InstanceId"]["Struct"]["value"]["Guid"]
if instance_id == old_instance_id:
level_json["root"]["properties"]["worldSaveData"]["Struct"]["value"]["Struct"]["CharacterSaveParameterMap"]["Map"]["value"][i]["key"]["Struct"]["Struct"]["PlayerUId"]["Struct"]["value"]["Guid"] = new_guid_formatted
break

# Guild data replacement.
group_ids_len = len(level_json["root"]["properties"]["worldSaveData"]["Struct"]["value"]["Struct"]["GroupSaveDataMap"]["Map"]["value"])
for i in range(group_ids_len):
group_id = level_json["root"]["properties"]["worldSaveData"]["Struct"]["value"]["Struct"]["GroupSaveDataMap"]["Map"]["value"][i]
if group_id["value"]["Struct"]["Struct"]["GroupType"]["Enum"]["value"] == "EPalGroupType::Guild":
group_raw_data = group_id["value"]["Struct"]["Struct"]["RawData"]["Array"]["value"]["Base"]["Byte"]["Byte"]
raw_data_len = len(group_raw_data)
for i in range(raw_data_len-15):
if group_raw_data[i:i+16] == old_level_formatted:
group_raw_data[i:i+16] = new_level_formatted
print('Changes have been made')

# Dump modified data to JSON.
with open(old_json_path, 'w') as f:
json.dump(old_json, f, indent=2)
with open(level_json_path, 'w') as f:
json.dump(level_json, f, indent=2)
print('JSON files have been exported')

# Convert our JSON files to save files.
json_to_sav(uesave_path, level_json_path)
json_to_sav(uesave_path, old_json_path)
print('Converted JSON files back to save files')

# Clean up miscellaneous GVAS and JSON files which are no longer needed.
clean_up_files(level_sav_path)
clean_up_files(old_sav_path)
print('Miscellaneous files removed')

# We must rename the patched save file from the old GUID to the new GUID for the server to recognize it.
if os.path.exists(new_sav_path):
os.remove(new_sav_path)
os.rename(old_sav_path, new_sav_path)
print(f'Changed GUID {old_guid} -> {new_guid}')


def main():
parser = argparse.ArgumentParser(description='Change host in local 4-player co-op game.')
parser.add_argument('host', help='GUID or name of new co-op host')
args = parser.parse_args()

# Warn the user about potential data loss.
print('WARNING: Running this script WILL change your save files and could \
potentially corrupt your data. It is HIGHLY recommended that you make a backup \
of your save folder before continuing. Press enter if you would like to continue.')
input('> ')

config_path = Path(__file__).parent / 'config.json'
with open(config_path, 'r') as file:
config = json.load(file)

uesave_path = config['uesave_path']
save_path = config['save_path']
player_list = config['player_list']
player_list = [player for player in player_list if player['GUID'] != '']
print(f'uesave_path: {uesave_path}')
print(f'save_path: {save_path}')
print(f'player_list: {player_list}')

# uesave_path must point directly to the executable, not just the path it is located in.
if not os.path.exists(uesave_path) or not os.path.isfile(uesave_path):
print('ERROR: Your given <uesave_path> of "' + uesave_path + '" is invalid. It must point directly to the executable. For example: C:\\Users\\Bob\\.cargo\\bin\\uesave.exe')
exit(1)

# save_path must exist in order to use it.
if not os.path.exists(save_path):
print('ERROR: Your given <save_path> of "' + save_path + '" does not exist. Did you enter the correct path to your save folder?')
exit(1)

# player_list must have at least 2 players.
if len(player_list) < 2:
print('ERROR: You must have at least 2 players in your <players> list, add more players to your config.json file.')
exit(1)

# host must be a valid GUID or name.
if args.host not in [player['GUID'] for player in player_list] and args.host not in [player['name'] for player in player_list]:
print('ERROR: Your given <host> of "' + args.host + '" is not a valid GUID or name. Please refer to your config.json file.')
exit(1)

old_host_guid = old_host(player_list, Path(save_path)/'Players')
new_host_guid = new_host(args.host, player_list)
change_guid('00000000000000000000000000000001', old_host_guid, save_path, uesave_path)
change_guid(new_host_guid, '00000000000000000000000000000001', save_path, uesave_path)
print('Host change has been applied! Have fun!')

if __name__ == '__main__':
main()
30 changes: 30 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"uesave_path": "",
"save_path": "",
"player_list": [
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
},
{
"GUID": "",
"name": ""
}
]
}