diff --git a/LICENSE.txt b/LICENSE.txt index bf05a36..2aacb8d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,5 @@ Undo for FFG Games -Copyright (C) 2017 Christopher Gurnee +Copyright (C) 2017-2019 Christopher Gurnee All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 9da5dec..0ae0c22 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you'd prefer to run Undo for FFG Games from the Python source code directly: 2. Download the latest version of Undo for FFG Games from and unzip it to a location of your choice. - 3. In the folder you unzipped above, double-click the `Undo_MoM2e.pyw` file for Mansions of Madness, `Undo_RtL.pyw` for Road to Legend, or `Undo_LotA.pyw` for Legends of the Alliance. + 3. In the folder you unzipped above, double-click the `Undo_MoM2e.pyw` file for Mansions of Madness, `Undo_RtL.pyw` for Road to Legend, `Undo_LotA.pyw` for Legends of the Alliance, or `Undo_JiME.pyw` for Journeys in Middle-Earth. ## Q & A ## diff --git a/Undo_JiME.pyw b/Undo_JiME.pyw new file mode 100644 index 0000000..97feb65 --- /dev/null +++ b/Undo_JiME.pyw @@ -0,0 +1,40 @@ +#!python3.8 +# Undo_LotA.py - stub for Undo for LotR Journeys in Middle-Earth +# Copyright (C) 2019 Christopher Gurnee +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Undo_MoM2e, traceback, sys +from tkinter import messagebox + +if __name__ == '__main__': + try: + Undo_MoM2e.main(Undo_MoM2e.JIME) + except Exception: + msg = ''.join(traceback.format_exc()) + messagebox.showerror('Exception', msg) + if Undo_MoM2e.root: + Undo_MoM2e.root.destroy() + sys.exit(msg) diff --git a/Undo_MoM2e.pyw b/Undo_MoM2e.pyw index afd0f49..ab37d2a 100644 --- a/Undo_MoM2e.pyw +++ b/Undo_MoM2e.pyw @@ -1,6 +1,6 @@ #!python3.8 # Undo_MoM2e.py - Undo for Fantasy Flight Games apps -# Copyright (C) 2017 Christopher Gurnee +# Copyright (C) 2017-2019 Christopher Gurnee # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -37,12 +37,14 @@ import tkinter as tk from tkinter import ttk, messagebox, simpledialog, filedialog import nrbf -__version__ = '2.1' +__version__ = '3.0' DEFAULT_MAX_UNDO_STATES = 20 MOM = 'MoM2e' RTL = 'RtL' LOTA = 'LotA' +JIME = 'JiME' +ALL_GAMES = MOM, RTL, LOTA, JIME DEFAULT_GAME = MOM @@ -308,20 +310,17 @@ def parse_mom_gamedata(savefile): scenario = scenario[:-4] # InvestigatorIds is a comma-separated string; count its values: with suppress(AttributeError): players = savedata.InvestigatorIds.count(',') + 1 - with suppress(AttributeError): round = savedata.Round + with suppress(AttributeError): round = savedata.Round or '' return scenario, reqdtiles, players, round # Read the contents of a MoM_SaveGame file to retrieve the tile count, monster count, # and highest-threat monster (ignoring errors resulting from format changes) def parse_mom_savegame(savefile): savedata = nrbf.read_stream(savefile) - tiles = 0 - try: - for tile in savedata.TileSaveData.values(): - tiles += 1 if tile.Visible else 0 - except AttributeError: - tiles = '' - monsters = 0 + tiles = '' + with suppress(AttributeError): + tiles = sum(1 if tile.Visible else 0 for tile in savedata.TileSaveData.values()) or '' + monsters = 0 threat_name = '' threat_is_unique = False threat_max_damage = 0 @@ -377,7 +376,7 @@ def parse_rtl_savedgame(savefile): with suppress(AttributeError): group = savedata.PartyName with suppress(AttributeError): scenario = RTL_SCENARIOS_BY_ID .get(savedata.CampaignId, '') with suppress(AttributeError): difficulty = RTL_DIFFICULTIES_BY_ID.get(savedata.CampaignDifficulty, '') - with suppress(AttributeError): players = len(savedata.HeroIds) + with suppress(AttributeError): players = len(savedata.HeroIds) or '' with suppress(AttributeError): in_quest = savedata.CurrentScene.value__ == 2 with suppress(AttributeError): for string_var in savedata.GlobalVarData.StringVars: @@ -398,31 +397,26 @@ def parse_rtl_savedgame(savefile): with suppress(AttributeError): for int_var in savedata.GlobalVarData.IntVars: if int_var.Name == 'Round': - round = int_var.Value + round = int_var.Value or 1 break - tiles = 0 - try: - for tile in savedata.QuestData.TileSaveData.values(): - if tile.Visible: - tiles += 1 - except AttributeError: - tiles = '' + with suppress(AttributeError): + tiles = sum(1 if tile.Visible else 0 for tile in savedata.QuestData.TileSaveData.values()) or '' else: location = RTL_CITIES_BY_ID.get(location, 'Wilderness') return group, scenario, difficulty, players, location, round, tiles # Read the contents of a LotA SavedGameA file to retrieve the squad name, campaign, difficulty, # player count, combat round and tile count (ignoring errors resulting from format changes). -LOTA_SCENARIOS_BY_ID = {'CAM_T':'Tutorial', 'CAM_1':'Freedom Fighter', 'CAM_2':"Jabba's Realm", 'CAM_3':'Return to Hoth', +LOTA_CAMPAIGNS_BY_ID = {'CAM_T': 'Tutorial', 'CAM_1': 'Freedom Fighter', 'CAM_2': "Jabba's Realm", 'CAM_3': 'Return to Hoth', 'RAID_1':'Malastarian Outpost', 'RAID_2':'Gray Cap Cantina'} def parse_lota_savedgame(savefile): savedata = json.load(savefile) squad = campaign = difficulty = players = round = tiles = '' in_quest = False with suppress(KeyError): squad = savedata['PartyName'] - with suppress(KeyError): campaign = LOTA_SCENARIOS_BY_ID [savedata['CampaignId']] + with suppress(KeyError): campaign = LOTA_CAMPAIGNS_BY_ID [savedata['CampaignId']] with suppress(KeyError): difficulty = RTL_DIFFICULTIES_BY_ID[savedata['CampaignDifficulty']] # (same as RtL) - with suppress(KeyError): players = len(savedata['HeroIds']) + with suppress(KeyError): players = len(savedata['HeroIds']) or '' with suppress(KeyError): in_quest = savedata['CurrentScene'] == 2 if campaign == 'Tutorial': with suppress(KeyError): @@ -433,24 +427,77 @@ def parse_lota_savedgame(savefile): for bool_var in savedata['GlobalVarData']['BoolVars']: if bool_var['Name'] == 'IsTutorial': if bool_var['Value'] is False: - campaign = LOTA_SCENARIOS_BY_ID['CAM_1'] + campaign = LOTA_CAMPAIGNS_BY_ID['CAM_1'] break if in_quest: - with suppress(KeyError): round = savedata['QuestData']['RoundCount'] - if round == 0: - round = 1 - tiles = 0 - try: - for tile in savedata['QuestData']['SceneData']['TileSaveData']: - if tile['Visible']: - tiles += 1 - except KeyError: - tiles = '' + with suppress(KeyError): round = savedata['QuestData']['RoundCount'] or 1 + with suppress(KeyError): + tiles = sum(1 if tile['Visible'] else 0 for tile in savedata['QuestData']['SceneData']['TileSaveData']) or '' return squad, campaign, difficulty, players, round, tiles +# Read the contents of a JiME SavedGameA file to retrieve the party name, campaign, difficulty, player count, +# chapter, combat round, tile count, threat, and active threats (ignoring errors resulting from format changes). +JIME_CAMPAIGNS_BY_ID = { 1:'Bones of Arnor', 2:'The Ember Crown' } +JIME_CHAPTERS_BY_IDS = { + 1 : { # Bones of Arnor + 1 : 'A Shadow in Eriador', + 2 : "The Thieves' Lair", + 3 : 'Dark Eglantine', + 4 : 'The Forsaken Inn', + 5 : 'The Silent Scepter', + 6 : 'Flight from Fornost', + 7 : "Ul\u00fbk's Trail", + 8 : 'Ul\u00fbk', + 9 : 'Crossing the Brandywine', + 10 : 'The Gates of Ann\u00faminas', + 11 : 'The Mirror of Spirits', + 12 : 'The Aid of Evensong', + 13 : 'Gulgotar', + 14 : 'Campaign Finale' + } +} +def parse_jime_savedgame(savefile): + savedata = json.load(savefile) + party = campaign = difficulty = players = chapter = round = tiles = threat = active_threats = '' + campaign_id = chapter_num = in_quest = False + with suppress(KeyError): party = savedata['PartyName'] + with suppress(KeyError): campaign_id = savedata['CampaignId'] + with suppress(KeyError): campaign = JIME_CAMPAIGNS_BY_ID[campaign_id] + with suppress(KeyError): difficulty = RTL_DIFFICULTIES_BY_ID[savedata['CampaignDifficulty']] # (same as RtL) + with suppress(KeyError): players = len(savedata['HeroInfo']) or '' + with suppress(KeyError): chapter_num = len(savedata['CompletedAdventureIds']) + 1 + with suppress(KeyError): in_quest = savedata['CurrentScene'] == 2 + if chapter_num: + chapter = f'Ch. {chapter_num}' + with suppress(KeyError): + chapter += (' ' if chapter else '') + JIME_CHAPTERS_BY_IDS[campaign_id][savedata['CurrentAdventureId']] + if in_quest: + with suppress(KeyError): + for int_var in savedata['GlobalVarData']['IntVars']: + if int_var['Name'] == 'Round': + round = int_var['Value'] or 1 + if threat != '': # if they've both been found, break + break + if int_var['Name'] == 'Threat': + threat = int_var['Value'] + if round != '': # if they've both been found, break + break + if adventure_data := savedata.get('AdventureData'): + with suppress(KeyError): tiles = \ + sum(1 if tile['Visible'] else 0 for tile in adventure_data['Tiles']) + \ + sum(1 if tile['IsVisible'] and tile['TokenType'] == 50 else 0 for tile in adventure_data['Terrain']) \ + or '' + if threat != '': + with suppress(KeyError): threat = f"{threat} / {adventure_data['ThreatMax']}" + with suppress(KeyError): + active_threats = ', '.join(str(t_v) for t_v in sorted( + a_t['ThreatValue'] for a_t in adventure_data['ActiveThreatEvents'])) + return party, campaign, difficulty, players, chapter, round, tiles, threat, active_threats + + def init_gamespecific_globals(game): - assert game in (MOM, RTL, LOTA) + assert game in ALL_GAMES global FFG_GAME FFG_GAME = game @@ -461,7 +508,7 @@ def init_gamespecific_globals(game): if FFG_GAME == MOM: SLOT_COUNT = 0 # 0 is a flag for the special case where there's exactly one save slot known_undostate_hexhashes = [collections.OrderedDict()] - elif FFG_GAME in (RTL, LOTA): + elif FFG_GAME in (RTL, LOTA, JIME): SLOT_COUNT = 5 known_undostate_hexhashes = [collections.OrderedDict() for i in range(SLOT_COUNT)] else: assert False @@ -483,6 +530,9 @@ def init_gamespecific_globals(game): elif FFG_GAME == LOTA: STEAM_ID = 703980 init_gamedir_globals('Imperial Assault', r'Imperial Assault_Data\SavedGames') + elif FFG_GAME == JIME: + STEAM_ID = 1003400 + init_gamedir_globals('Journeys in Middle-earth', r'JiME_Data\SavedGames') else: assert False # Game-specific GUI strings @@ -496,6 +546,9 @@ def init_gamespecific_globals(game): elif FFG_GAME == LOTA: GAME_NAME_TEXT = 'Legends of the Alliance' OPEN_BUTON_TEXT = 'Open Legends\nof the Alliance' + elif FFG_GAME == JIME: + GAME_NAME_TEXT = 'Journeys in Middle-Earth' + OPEN_BUTON_TEXT = 'Open Journeys\nin Middle-Earth' else: assert False def init_gamedir_globals(default_dir, savegame_dir): @@ -565,6 +618,8 @@ class UndoApplication(ttk.Frame): col_headings = 'Group', 'Scenario', 'Difficulty', 'Players', 'Quest / Location', 'Round', 'Tiles', 'Timestamp' elif FFG_GAME == LOTA: col_headings = 'Group', 'Scenario', 'Difficulty', 'Players', 'Round', 'Tiles', 'Timestamp' + elif FFG_GAME == JIME: + col_headings = 'Group', 'Scenario', 'Difficulty', 'Players', 'Chapter', 'Round', 'Tiles', 'Threat', 'Future Threats', 'Timestamp' else: assert False self.states_treeview = ttk.Treeview(frame, columns = [c.lower() for c in col_headings] + ['current'], @@ -580,16 +635,23 @@ class UndoApplication(ttk.Frame): if col != 'Timestamp': self.states_treeview.column(col.lower(), anchor=tk.CENTER, width=60) self.states_treeview.column('#1', anchor=tk.E, width=160) # Scenario for MoM, Group for RtL - if FFG_GAME == RTL: - self.states_treeview.column('scenario', width=110) + if FFG_GAME == MOM: + self.states_treeview.column('tiles required', width=140) + self.states_treeview.column('main threat', width=120) + elif FFG_GAME == RTL: + self.states_treeview.column('scenario', width=110) self.states_treeview.column('quest / location', width=180) - if FFG_GAME == LOTA: + elif FFG_GAME == LOTA: self.states_treeview.heading('group', text='Squad') self.states_treeview.heading('scenario', text='Campaign') self.states_treeview.column ('scenario', width=120) - if FFG_GAME == MOM: - self.states_treeview.column('tiles required', width=140) - self.states_treeview.column('main threat', width=120) + elif FFG_GAME == JIME: + self.states_treeview.heading('group', text='Party') + self.states_treeview.heading('scenario', text='Campaign') + self.states_treeview.column ('scenario', width=110) + self.states_treeview.column ('chapter', width=180) + self.states_treeview.column ('future threats', width=90) + else: assert False self.states_treeview.column('timestamp', width=120) self.states_treeview.column('current', width=60) for slot in range(SLOT_COUNT): @@ -696,7 +758,7 @@ class UndoApplication(ttk.Frame): timestamp = app.states_treeview.set(hexhash_slot, 'timestamp') filename = '' scenario_or_group = scenario - if FFG_GAME in (RTL, LOTA): + if FFG_GAME in (RTL, LOTA, JIME): scenario_or_group = app.states_treeview.set(hexhash_slot, 'group') scenario_or_group = ''.join(c if c.isalnum() or c in " -'!" else '_' for c in scenario_or_group) # sanitize if scenario_or_group: @@ -706,7 +768,7 @@ class UndoApplication(ttk.Frame): filename += ', ' elif players: filename += f'{players} players, ' - if FFG_GAME in (RTL, LOTA): + if FFG_GAME in (RTL, LOTA, JIME): if scenario: filename += scenario if difficulty := app.states_treeview.set(hexhash_slot, 'difficulty'): @@ -715,6 +777,9 @@ class UndoApplication(ttk.Frame): if FFG_GAME == RTL: if location := app.states_treeview.set(hexhash_slot, 'quest / location'): filename += f'{location}, ' + elif FFG_GAME == JIME: + if chapter := app.states_treeview.set(hexhash_slot, 'chapter'): + filename += f'{chapter}, ' if round: filename += f'round {round}, ' filename += timestamp[:-3].replace(':', '.') @@ -723,7 +788,11 @@ class UndoApplication(ttk.Frame): if filename: glob_pattern = f'{FFG_GAME} ????-??-?? ??.??.?? {hexhash_slot}.zip' src_filename = next(MYDATA_DIR.glob(glob_pattern)) # next() gets the first (should be the only) filename - shutil.copyfile(src_filename, filename) + try: + shutil.copyfile(src_filename, filename) + except OSError as e: + messagebox.showerror('Error', f"Can't save Undo State to the chosen file.\n{e}") + return def handle_restore_from_clicked(self): if is_game_running(): @@ -739,13 +808,16 @@ class UndoApplication(ttk.Frame): for zipped_filename in unzipper.namelist(): # If at least one of the savegame files is present, assume it's valid if FFG_GAME == MOM and zipped_filename.lower() in ('gamedata.dat', 'mom_savegame') or \ - FFG_GAME in (RTL, LOTA) and zipped_filename.lower() == 'savedgamea': + FFG_GAME in (RTL, LOTA, JIME) and zipped_filename.lower() == 'savedgamea': with unzipper.open(zipped_filename) as savefile: if FFG_GAME in (MOM, RTL): if nrbf.serialization(savefile).read_header(): # returns True if it's a valid header break elif FFG_GAME == LOTA: - if savefile.read(2) == b'{"': # if it looks like the beginning of a json object + if savefile.read(2) == b'{"': # looks like the beginning of a minimized json object + break + elif FFG_GAME == JIME: + if savefile.read(7) == b'{\n "': # looks like the beginning of an indented json object break else: assert False else: @@ -753,6 +825,9 @@ class UndoApplication(ttk.Frame): except BadZipFile as e: messagebox.showerror('Error', f'This file is not a {GAME_NAME_TEXT} Undo file.\n({e})') return + except OSError as e: + messagebox.showerror('Error', f"Can't restore Undo State from the selected file.\n{e}") + return if SLOT_COUNT: slot = simpledialog.askinteger('Slot?', '\nWhich Save Slot would you like this\n' @@ -855,11 +930,12 @@ def load_undo_states(): elif zipped_filename.lower() == 'mom_savegame': with unzipper.open(zipped_filename) as savefile: treeview_values.extend(parse_mom_savegame(savefile)) - elif FFG_GAME in (RTL, LOTA): + elif FFG_GAME in (RTL, LOTA, JIME): if zipped_filename.lower() == 'savedgamea': with unzipper.open(zipped_filename) as savefile: - treeview_values = parse_rtl_savedgame (savefile) if FFG_GAME == RTL \ - else parse_lota_savedgame(savefile) + treeview_values = parse_rtl_savedgame (savefile) if FFG_GAME == RTL \ + else parse_lota_savedgame(savefile) if FFG_GAME == LOTA \ + else parse_jime_savedgame(savefile) else: assert False # Insert the Undo State into the treeview UI at the top @@ -957,12 +1033,13 @@ def handle_new_savegame(slot, use_filetime = False): main_savename = f with f.open('rb') as savefile: treeview_values.extend(parse_mom_savegame(savefile)) - elif FFG_GAME in (RTL, LOTA): + elif FFG_GAME in (RTL, LOTA, JIME): if lower_name == 'savedgamea': main_savename = f with f.open('rb') as savefile: - treeview_values = parse_rtl_savedgame (savefile) if FFG_GAME == RTL \ - else parse_lota_savedgame(savefile) + treeview_values = parse_rtl_savedgame (savefile) if FFG_GAME == RTL \ + else parse_lota_savedgame(savefile) if FFG_GAME == LOTA \ + else parse_jime_savedgame(savefile) else: assert False zipper.close() binhash = hash.digest() @@ -1073,8 +1150,8 @@ def restore_undo_state(zip_filename, slot, update_save_index = False): unzipper.extract(zipped_filename, savegame_dir) extracted_filenames.append(zipped_filename) - # For RtL and LotA, the SaveIndex (what the UI calls the save slot) is stored - # inside the SavedGameA/B files, so if it's different it must be updated + # For RtL, LotA, and JiME, the SaveIndex (what the UI calls the save slot) is + # stored inside the SavedGameA/B files, so if it's different it must be updated if update_save_index: savegame_filenames = [ savegame_dir / 'SavedGameA', savegame_dir / 'SavedGameB' ] if not savegame_filenames[-1].is_file(): @@ -1086,16 +1163,21 @@ def restore_undo_state(zip_filename, slot, update_save_index = False): savedata = save_ser.read_stream() if slot != savedata.SaveIndex: save_ser.overwrite_member(savedata, 'SaveIndex', slot) - elif FFG_GAME == LOTA: + elif FFG_GAME in (LOTA, JIME): for savegame_filename in savegame_filenames: - with savegame_filename.open('r+', encoding='utf-8') as savefile: + with savegame_filename.open('r+', encoding='utf-8', newline='') as savefile: savedata = json.load(savefile) if slot != savedata['SaveIndex']: savedata['SaveIndex'] = slot savefile.seek(0) - savefile.truncate() # Write it back out using the same JSON formatting - json.dump(savedata, savefile, ensure_ascii=False, separators=(',', ':')) + if FFG_GAME == LOTA: + json.dump(savedata, savefile, ensure_ascii=False, separators=(',', ':')) + elif FFG_GAME == JIME: + json.dump(savedata, savefile, ensure_ascii=False, indent=4) + else: assert False + savefile.truncate() + else: assert False except Exception: # Undo any file extractions if there were any errors @@ -1145,7 +1227,7 @@ def main(game): root.update() assert root.getvar('tcl_platform(threaded)'), 'tcl has multithreading support' - if FFG_GAME in (RTL, LOTA) and not STEAMAPPS_DIR.is_dir(): + if FFG_GAME in (RTL, LOTA, JIME) and not STEAMAPPS_DIR.is_dir(): raise RuntimeError(f"Undo can't find either the {GAME_NAME_TEXT} nor the SteamApps folder") if not SAVEGAME_DIR.is_dir(): answered_yes = messagebox.askyesno("Can't find SaveGame folder", @@ -1211,16 +1293,13 @@ if __name__ == '__main__': if len(sys.argv) > 1: if len(sys.argv) == 2 and sys.argv[1].startswith('--game='): game_arg = sys.argv[1][7:].lower() - if game_arg == 'mom': - game = MOM - elif game_arg == 'rtl': - game = RTL - elif game_arg == 'lota': - game = LOTA + for game in ALL_GAMES: + if game_arg == game.lower(): + break else: - sys.exit(f'Unsupported game, must be one of: mom, rtl, lota') + sys.exit(f'Unsupported game, must be one of: {", ".join(ALL_GAMES)}') else: - sys.exit(f'Usage: {sys.argv[0]} [--game=mom|rtl|lota]') + sys.exit(f'Usage: {sys.argv[0]} [--game={"|".join(ALL_GAMES)}]') else: game = DEFAULT_GAME try: diff --git a/installer/README.md b/installer/README.md index 05f06a9..471306d 100644 --- a/installer/README.md +++ b/installer/README.md @@ -7,4 +7,4 @@ 3. Download `VC_redist.x86.exe` (Microsoft Visual C++ 2015-2019 Redistributable (x86)) into this directory from here: . - 4. Double-click the `build_installer.py` file in this directory. The built installer (`Undo_v2.1_for_FFG_setup.exe`) will be placed in this directory. + 4. Double-click the `build_installer.py` file in this directory. The built installer (`Undo_v3.0_for_FFG_setup.exe`) will be placed in this directory. diff --git a/installer/Undo_MoM2e.nsi b/installer/Undo_MoM2e.nsi index a151fff..8f2f0eb 100644 --- a/installer/Undo_MoM2e.nsi +++ b/installer/Undo_MoM2e.nsi @@ -3,7 +3,7 @@ !include MUI2.nsh Name "Undo for FFG Games" -OutFile "Undo_v2.1_for_FFG_setup.exe" +OutFile "Undo_v3.0_for_FFG_setup.exe" RequestExecutionLevel admin ManifestSupportedOS all @@ -36,13 +36,13 @@ Var StartMenuFolder !insertmacro MUI_LANGUAGE "English" -VIProductVersion "2.1.0.0" +VIProductVersion "3.0.0.0" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductName" "Undo for FFG Games" -VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "2.1" +VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "3.0" VIAddVersionKey /LANG=${LANG_ENGLISH} "Comments" "Installer distributed from https://github.com/gurnec/Undo_FFG/releases" -VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "Copyright © 2017 Christopher Gurnee. All rights reserved." +VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "Copyright © 2017-2019 Christopher Gurnee. All rights reserved." VIAddVersionKey /LANG=${LANG_ENGLISH} "FileDescription" "Undo for FFG Games Installer" -VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "2.1" +VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "3.0" ; The install script @@ -91,10 +91,11 @@ Section ; Install the Start Menu shortcuts !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$StartMenuFolder" - CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Mansions of Madness.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=mom" - CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Road to Legend.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=rtl" - CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Legends of the Alliance.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=lota" - CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Uninstall Undo for FFG.lnk" "$INSTDIR\Uninstall.exe" + CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Mansions of Madness.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=mom2e" + CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Road to Legend.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=rtl" + CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Legends of the Alliance.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=lota" + CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Undo for Journeys in Middle-Earth.lnk" "$INSTDIR\Undo_MoM2e.exe" "--game=jime" + CreateShortcut "$SMPROGRAMS\$StartMenuFolder\Uninstall Undo for FFG.lnk" "$INSTDIR\Uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_END ; Install the uninstaller @@ -102,8 +103,8 @@ Section WriteRegStr HKLM "Software\Undo for MoM2e" "" $INSTDIR WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "DisplayIcon" "$INSTDIR\Undo_MoM2e.ico" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "DisplayName" "Undo for FFG Games" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "DisplayVersion" "2.1" - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "EstimatedSize" 16722 + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "DisplayVersion" "3.0" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "EstimatedSize" 15348 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "HelpLink" "https://github.com/gurnec/Undo_FFG/issues" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Undo for MoM2e" "NoRepair" 1 @@ -147,6 +148,7 @@ Section "Uninstall" Delete /REBOOTOK "$SMPROGRAMS\$StartMenuFolder\Undo for Mansions of Madness.lnk" Delete /REBOOTOK "$SMPROGRAMS\$StartMenuFolder\Undo for Road to Legend.lnk" Delete /REBOOTOK "$SMPROGRAMS\$StartMenuFolder\Undo for Legends of the Alliance.lnk" + Delete /REBOOTOK "$SMPROGRAMS\$StartMenuFolder\Undo for Journeys in Middle-Earth.lnk" Delete /REBOOTOK "$SMPROGRAMS\$StartMenuFolder\Uninstall Undo for FFG.lnk" RMDir /REBOOTOK "$SMPROGRAMS\$StartMenuFolder" diff --git a/installer/build_installer.py b/installer/build_installer.py index be319cd..bbe6c2e 100644 --- a/installer/build_installer.py +++ b/installer/build_installer.py @@ -63,7 +63,7 @@ if cert_name: print() - filename_to_sign = 'Undo_v2.1_for_FFG_setup.exe', + filename_to_sign = 'Undo_v3.0_for_FFG_setup.exe', run(sign_args_sha1 + filename_to_sign, check=True) run(sign_args_sha256 + filename_to_sign, check=True) diff --git a/installer/file_version_info.txt b/installer/file_version_info.txt index 02e8471..993ec07 100644 --- a/installer/file_version_info.txt +++ b/installer/file_version_info.txt @@ -6,8 +6,8 @@ VSVersionInfo( ffi=FixedFileInfo( # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) # Set not needed items to zero 0. - filevers=(2, 1, 0, 0), - prodvers=(2, 1, 0, 0), + filevers=(3, 0, 0, 0), + prodvers=(3, 0, 0, 0), # Contains a bitmask that specifies the valid bits 'flags'r mask=0x3f, # Contains a bitmask that specifies the Boolean attributes of the file. @@ -30,8 +30,8 @@ VSVersionInfo( StringTable( u'040904b0', [StringStruct(u'FileDescription', u'Undo for FFG Games'), - StringStruct(u'FileVersion', u'2.1.0.0'), - StringStruct(u'LegalCopyright', u'Copyright © 2017 Christopher Gurnee. All rights reserved.'), + StringStruct(u'FileVersion', u'3.0.0.0'), + StringStruct(u'LegalCopyright', u'Copyright © 2017-2019 Christopher Gurnee. All rights reserved.'), StringStruct(u'OriginalFilename', u'Undo_MoM2e.exe')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])])