Wargroove: Implement New Game (#1401)

This adds Wargroove to the list of supported games. Wargroove uses a custom non-linear campaign over the vanilla and double trouble campaigns. A Wargroove client has been added which does a lot of heavy lifting for the Wargroove implementation and must be always on during gameplay. The mod source files can be found here: https://github.com/FlySniper/WargrooveArchipelagoMod
This commit is contained in:
FlySniper 2023-02-24 01:35:09 -05:00 committed by GitHub
parent 7c68e91d4a
commit 5966aa5327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1251 additions and 0 deletions

View File

@ -151,6 +151,8 @@ components: Iterable[Component] = (
Component('ChecksFinder Client', 'ChecksFinderClient'),
# Starcraft 2
Component('Starcraft 2 Client', 'Starcraft2Client'),
# Wargroove
Component('Wargroove Client', 'WargrooveClient'),
# Zillion
Component('Zillion Client', 'ZillionClient',
file_identifier=SuffixIdentifier('.apzl')),

View File

@ -310,6 +310,9 @@ def get_default_options() -> OptionsType:
"lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
},
"wargroove_options": {
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
}
}
return options

443
WargrooveClient.py Normal file
View File

@ -0,0 +1,443 @@
from __future__ import annotations
import os
import sys
import asyncio
import random
import shutil
from typing import Tuple, List, Iterable, Dict
from worlds.wargroove import WargrooveWorld
from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData
import ModuleUpdate
ModuleUpdate.update()
import Utils
import json
import logging
if __name__ == "__main__":
Utils.init_logging("WargrooveClient", exception_logger="Client")
from NetUtils import NetworkItem, ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
wg_logger = logging.getLogger("WG")
class WargrooveClientCommandProcessor(ClientCommandProcessor):
def _cmd_resync(self):
"""Manually trigger a resync."""
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_commander(self, *commander_name: Iterable[str]):
"""Set the current commander to the given commander."""
if commander_name:
self.ctx.set_commander(' '.join(commander_name))
else:
if self.ctx.can_choose_commander:
commanders = self.ctx.get_commanders()
wg_logger.info('Unlocked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if unlocked)))
wg_logger.info('Locked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if not unlocked)))
else:
wg_logger.error('Cannot set commanders in this game mode.')
class WargrooveContext(CommonContext):
command_processor: int = WargrooveClientCommandProcessor
game = "Wargroove"
items_handling = 0b111 # full remote
current_commander: CommanderData = faction_table["Starter"][0]
can_choose_commander: bool = False
commander_defense_boost_multiplier: int = 0
income_boost_multiplier: int = 0
starting_groove_multiplier: float
faction_item_ids = {
'Starter': 0,
'Cherrystone': 52025,
'Felheim': 52026,
'Floran': 52027,
'Heavensong': 52028,
'Requiem': 52029,
'Outlaw': 52030
}
buff_item_ids = {
'Income Boost': 52023,
'Commander Defense Boost': 52024,
}
def __init__(self, server_address, password):
super(WargrooveContext, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "appdata" in os.environ:
options = Utils.get_options()
root_directory = options["wargroove_options"]["root_directory"].replace("/", "\\")
data_directory = "lib\\worlds\\wargroove\\data\\"
dev_data_directory = "worlds\\wargroove\\data\\"
appdata_wargroove = os.path.expandvars("%APPDATA%\\Chucklefish\\Wargroove\\")
if not os.path.isfile(root_directory + "\\win64_bin\\wargroove64.exe"):
print_error_and_close("WargrooveClient couldn't find wargroove64.exe. "
"Unable to infer required game_communication_path")
self.game_communication_path = root_directory + "\\AP"
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
if not os.path.isdir(appdata_wargroove):
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
"Boot Wargroove and then close it to attempt to fix this error")
if not os.path.isdir(data_directory):
data_directory = dev_data_directory
if not os.path.isdir(data_directory):
print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!")
shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True)
else:
print_error_and_close("WargrooveClient couldn't detect system type. "
"Unable to infer required game_communication_path")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(WargrooveContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(WargrooveContext, self).connection_closed()
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root + "/" + file)
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
await super(WargrooveContext, self).shutdown()
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
filename = f"AP_settings.json"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
slot_data = args["slot_data"]
json.dump(args["slot_data"], f)
self.can_choose_commander = slot_data["can_choose_commander"]
print('can choose commander:', self.can_choose_commander)
self.starting_groove_multiplier = slot_data["starting_groove_multiplier"]
self.income_boost_multiplier = slot_data["income_boost"]
self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"]
f.close()
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
self.update_commander_data()
self.ui.update_tracker()
random.seed(self.seed_name + str(self.slot))
# Our indexes start at 1 and we have 24 levels
for i in range(1, 25):
filename = f"seed{i}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(str(random.randint(0, 4294967295)))
f.close()
if cmd in {"RoomInfo"}:
self.seed_name = args["seed_name"]
if cmd in {"ReceivedItems"}:
received_ids = [item.item for item in self.items_received]
for network_item in self.items_received:
filename = f"AP_{str(network_item.item)}.item"
path = os.path.join(self.game_communication_path, filename)
# Newly-obtained items
if not os.path.isfile(path):
open(path, 'w').close()
# Announcing commander unlocks
item_name = self.item_names[network_item.item]
if item_name in faction_table.keys():
for commander in faction_table[item_name]:
logger.info(f"{commander.name} has been unlocked!")
with open(path, 'w') as f:
item_count = received_ids.count(network_item.item)
if self.buff_item_ids["Income Boost"] == network_item.item:
f.write(f"{item_count * self.income_boost_multiplier}")
elif self.buff_item_ids["Commander Defense Boost"] == network_item.item:
f.write(f"{item_count * self.commander_defense_boost_multiplier}")
else:
f.write(f"{item_count}")
f.close()
print_filename = f"AP_{str(network_item.item)}.item.print"
print_path = os.path.join(self.game_communication_path, print_filename)
if not os.path.isfile(print_path):
open(print_path, 'w').close()
with open(print_path, 'w') as f:
f.write("Received " +
self.item_names[network_item.item] +
" from " +
self.player_names[network_item.player])
f.close()
self.update_commander_data()
self.ui.update_tracker()
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage, Image
from kivy.uix.stacklayout import StackLayout
from kivy.uix.label import Label
from kivy.properties import ColorProperty
from kivy.uix.image import Image
import pkgutil
class TrackerLayout(BoxLayout):
pass
class CommanderSelect(BoxLayout):
pass
class CommanderButton(ToggleButton):
pass
class FactionBox(BoxLayout):
pass
class CommanderGroup(BoxLayout):
pass
class ItemTracker(BoxLayout):
pass
class ItemLabel(Label):
pass
class WargrooveManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("WG", "WG Console"),
]
base_title = "Archipelago Wargroove Client"
ctx: WargrooveContext
unit_tracker: ItemTracker
trigger_tracker: BoxLayout
boost_tracker: BoxLayout
commander_buttons: Dict[int, List[CommanderButton]]
tracker_items = {
"Swordsman": ItemData(None, "Unit", False),
"Dog": ItemData(None, "Unit", False),
**item_table
}
def build(self):
container = super().build()
panel = TabbedPanelItem(text="Wargroove")
panel.content = self.build_tracker()
self.tabs.add_widget(panel)
return container
def build_tracker(self) -> TrackerLayout:
try:
tracker = TrackerLayout(orientation="horizontal")
commander_select = CommanderSelect(orientation="vertical")
self.commander_buttons = {}
for faction, commanders in faction_table.items():
faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70)
commander_group = CommanderGroup()
commander_buttons = []
for commander in commanders:
commander_button = CommanderButton(text=commander.name, group="commanders")
if faction == "Starter":
commander_button.disabled = False
commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text))
commander_buttons.append(commander_button)
commander_group.add_widget(commander_button)
self.commander_buttons[faction] = commander_buttons
faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10))
faction_box.add_widget(commander_group)
commander_select.add_widget(faction_box)
item_tracker = ItemTracker(padding=[0,20])
self.unit_tracker = BoxLayout(orientation="vertical")
other_tracker = BoxLayout(orientation="vertical")
self.trigger_tracker = BoxLayout(orientation="vertical")
self.boost_tracker = BoxLayout(orientation="vertical")
other_tracker.add_widget(self.trigger_tracker)
other_tracker.add_widget(self.boost_tracker)
item_tracker.add_widget(self.unit_tracker)
item_tracker.add_widget(other_tracker)
tracker.add_widget(commander_select)
tracker.add_widget(item_tracker)
self.update_tracker()
return tracker
except Exception as e:
print(e)
def update_tracker(self):
received_ids = [item.item for item in self.ctx.items_received]
for faction, item_id in self.ctx.faction_item_ids.items():
for commander_button in self.commander_buttons[faction]:
commander_button.disabled = not (faction == "Starter" or item_id in received_ids)
self.unit_tracker.clear_widgets()
self.trigger_tracker.clear_widgets()
for name, item in self.tracker_items.items():
if item.type in ("Unit", "Trigger"):
status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1)
label = ItemLabel(text=name, color=status_color)
if item.type == "Unit":
self.unit_tracker.add_widget(label)
else:
self.trigger_tracker.add_widget(label)
self.boost_tracker.clear_widgets()
extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier
extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier
income_boost = ItemLabel(text="Extra Income: " + str(extra_income))
defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense))
self.boost_tracker.add_widget(income_boost)
self.boost_tracker.add_widget(defense_boost)
self.ui = WargrooveManager(self)
data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode()
Builder.load_string(data)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def update_commander_data(self):
if self.can_choose_commander:
faction_items = 0
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
for network_item in self.items_received:
if self.item_names[network_item.item] in faction_item_names:
faction_items += 1
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
# Must be an integer larger than 0
starting_groove = int(max(starting_groove, 0))
data = {
"commander": self.current_commander.internal_name,
"starting_groove": starting_groove
}
else:
data = {
"commander": "seed",
"starting_groove": 0
}
filename = 'commander.json'
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
json.dump(data, f)
if self.ui:
self.ui.update_tracker()
def set_commander(self, commander_name: str) -> bool:
"""Sets the current commander to the given one, if possible"""
if not self.can_choose_commander:
wg_logger.error("Cannot set commanders in this game mode.")
return
match_name = commander_name.lower()
for commander, unlocked in self.get_commanders():
if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name:
if unlocked:
self.current_commander = commander
self.syncing = True
wg_logger.info(f"Commander set to {commander.name}.")
self.update_commander_data()
return True
else:
wg_logger.error(f"Commander {commander.name} has not been unlocked.")
return False
else:
wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.")
def get_commanders(self) -> List[Tuple[CommanderData, bool]]:
"""Gets a list of commanders with their unlocked status"""
commanders = []
received_ids = [item.item for item in self.items_received]
for faction in faction_table.keys():
unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids
commanders += [(commander, unlocked) for commander in faction_table[faction]]
return commanders
async def game_watcher(ctx: WargrooveContext):
from worlds.wargroove.Locations import location_table
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def print_error_and_close(msg):
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
if __name__ == '__main__':
async def main(args):
ctx = WargrooveContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="WargrooveProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@ -139,6 +139,12 @@ pokemon_rb_options:
# True for operating system default program
# Alternatively, a path to a program to open the .gb file with
rom_start: true
wargroove_options:
# Locate the Wargroove root directory on your system.
# This is used by the Wargroove client, so it knows where to send communication files to
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
zillion_options:
# File name of the Zillion US rom
rom_file: "Zillion (UE) [!].sms"

104
worlds/wargroove/Items.py Normal file
View File

@ -0,0 +1,104 @@
import typing
from BaseClasses import Item, ItemClassification
from typing import Dict, List
PROGRESSION = ItemClassification.progression
PROGRESSION_SKIP_BALANCING = ItemClassification.progression_skip_balancing
USEFUL = ItemClassification.useful
FILLER = ItemClassification.filler
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
type: str
classification: ItemClassification = PROGRESSION
item_table: Dict[str, ItemData] = {
# Units
'Spearman': ItemData(52000, 'Unit'),
'Wagon': ItemData(52001, 'Unit', USEFUL),
'Mage': ItemData(52002, 'Unit'),
'Archer': ItemData(52003, 'Unit'),
'Knight': ItemData(52004, 'Unit'),
'Ballista': ItemData(52005, 'Unit'),
'Golem': ItemData(52006, 'Unit', USEFUL),
'Harpy': ItemData(52007, 'Unit'),
'Witch': ItemData(52008, 'Unit', USEFUL),
'Dragon': ItemData(52009, 'Unit'),
'Balloon': ItemData(52010, 'Unit', USEFUL),
'Barge': ItemData(52011, 'Unit'),
'Merfolk': ItemData(52012, 'Unit'),
'Turtle': ItemData(52013, 'Unit'),
'Harpoon Ship': ItemData(52014, 'Unit'),
'Warship': ItemData(52015, 'Unit'),
'Thief': ItemData(52016, 'Unit'),
'Rifleman': ItemData(52017, 'Unit'),
# Map Triggers
'Eastern Bridges': ItemData(52018, 'Trigger'),
'Southern Walls': ItemData(52019, 'Trigger'),
'Final Bridges': ItemData(52020, 'Trigger', PROGRESSION_SKIP_BALANCING),
'Final Walls': ItemData(52021, 'Trigger', PROGRESSION_SKIP_BALANCING),
'Final Sickle': ItemData(52022, 'Trigger', PROGRESSION_SKIP_BALANCING),
# Player Buffs
'Income Boost': ItemData(52023, 'Boost', FILLER),
'Commander Defense Boost': ItemData(52024, 'Boost', FILLER),
# Factions
'Cherrystone Commanders': ItemData(52025, 'Faction', USEFUL),
'Felheim Commanders': ItemData(52026, 'Faction', USEFUL),
'Floran Commanders': ItemData(52027, 'Faction', USEFUL),
'Heavensong Commanders': ItemData(52028, 'Faction', USEFUL),
'Requiem Commanders': ItemData(52029, 'Faction', USEFUL),
'Outlaw Commanders': ItemData(52030, 'Faction', USEFUL),
# Event Items
'Wargroove Victory': ItemData(None, 'Goal')
}
class CommanderData(typing.NamedTuple):
name: str
internal_name: str
alt_name: str = None
faction_table: Dict[str, List[CommanderData]] = {
'Starter': [
CommanderData('Mercival', 'commander_mercival')
],
'Cherrystone': [
CommanderData('Mercia', 'commander_mercia'),
CommanderData('Emeric', 'commander_emeric'),
CommanderData('Caesar', 'commander_caesar'),
],
'Felheim': [
CommanderData('Valder', 'commander_valder'),
CommanderData('Ragna', 'commander_ragna'),
CommanderData('Sigrid', 'commander_sigrid')
],
'Floran': [
CommanderData('Greenfinger', 'commander_greenfinger'),
CommanderData('Sedge', 'commander_sedge'),
CommanderData('Nuru', 'commander_nuru')
],
'Heavensong': [
CommanderData('Tenri', 'commander_tenri'),
CommanderData('Koji', 'commander_koji'),
CommanderData('Ryota', 'commander_ryota')
],
'Requiem': [
CommanderData('Elodie', 'commander_elodie'),
CommanderData('Dark Mercia', 'commander_darkmercia')
],
'Outlaw': [
CommanderData('Wulfar', 'commander_wulfar'),
CommanderData('Twins', 'commander_twins', 'Errol & Orla'),
CommanderData('Vesper', 'commander_vesper')
]
}

View File

@ -0,0 +1,41 @@
location_table = {
'Humble Beginnings: Caesar': 53001,
'Humble Beginnings: Chest 1': 53002,
'Humble Beginnings: Chest 2': 53003,
'Humble Beginnings: Victory': 53004,
'Best Friendssss: Find Sedge': 53005,
'Best Friendssss: Victory': 53006,
'A Knight\'s Folly: Caesar': 53007,
'A Knight\'s Folly: Victory': 53008,
'Denrunaway: Chest': 53009,
'Denrunaway: Victory': 53010,
'Dragon Freeway: Victory': 53011,
'Deep Thicket: Find Sedge': 53012,
'Deep Thicket: Victory': 53013,
'Corrupted Inlet: Victory': 53014,
'Mage Mayhem: Caesar': 53015,
'Mage Mayhem: Victory': 53016,
'Endless Knight: Victory': 53017,
'Ambushed in the Middle: Victory (Blue)': 53018,
'Ambushed in the Middle: Victory (Green)': 53019,
'The Churning Sea: Victory': 53020,
'Frigid Archery: Light the Torch': 53021,
'Frigid Archery: Victory': 53022,
'Archery Lessons: Chest': 53023,
'Archery Lessons: Victory': 53024,
'Surrounded: Caesar': 53025,
'Surrounded: Victory': 53026,
'Darkest Knight: Victory': 53027,
'Robbed: Victory': 53028,
'Open Season: Caesar': 53029,
'Open Season: Victory': 53030,
'Doggo Mountain: Find all the Dogs': 53031,
'Doggo Mountain: Victory': 53032,
'Tenri\'s Fall: Victory': 53033,
'Master of the Lake: Victory': 53034,
'A Ballista\'s Revenge: Victory': 53035,
'Rebel Village: Victory (Pink)': 53036,
'Rebel Village: Victory (Red)': 53037,
'Foolish Canal: Victory': 53038,
'Wargroove Finale: Victory': None,
}

View File

@ -0,0 +1,38 @@
import typing
from Options import Choice, Option, Range
class IncomeBoost(Range):
"""How much extra income the player gets per turn per boost received."""
display_name = "Income Boost"
range_start = 0
range_end = 100
default = 25
class CommanderDefenseBoost(Range):
"""How much extra defense the player's commander gets per boost received."""
display_name = "Commander Defense Boost"
range_start = 0
range_end = 8
default = 2
class CommanderChoice(Choice):
"""How the player's commander is selected for missions.
Locked Random: The player's commander is randomly predetermined for each level.
Unlockable Factions: The player starts with Mercival and can unlock playable factions.
Random Starting Faction: The player starts with a random starting faction and can unlock the rest.
When playing with unlockable factions, faction items are added to the pool.
Extra faction items after the first also reward starting Groove charge."""
display_name = "Commander Choice"
option_locked_random = 0
option_unlockable_factions = 1
option_random_starting_faction = 2
wargroove_options: typing.Dict[str, type(Option)] = {
"income_boost": IncomeBoost,
"commander_defense_boost": CommanderDefenseBoost,
"commander_choice": CommanderChoice
}

169
worlds/wargroove/Regions.py Normal file
View File

@ -0,0 +1,169 @@
def create_regions(world, player: int):
from . import create_region
from .Locations import location_table
world.regions += [
create_region(world, player, 'Menu', None, ['Humble Beginnings']),
# Level 1
create_region(world, player, 'Humble Beginnings', [
'Humble Beginnings: Caesar',
'Humble Beginnings: Chest 1',
'Humble Beginnings: Chest 2',
'Humble Beginnings: Victory',
], ['Best Friendssss', 'A Knight\'s Folly', 'Denrunaway', 'Wargroove Finale']),
# Levels 2A-2C
create_region(world, player, 'Best Friendssss', [
'Best Friendssss: Find Sedge',
'Best Friendssss: Victory'
], ['Dragon Freeway', 'Deep Thicket', 'Corrupted Inlet']),
create_region(world, player, 'A Knight\'s Folly', [
'A Knight\'s Folly: Caesar',
'A Knight\'s Folly: Victory'
], ['Mage Mayhem', 'Endless Knight', 'Ambushed in the Middle']),
create_region(world, player, 'Denrunaway', [
'Denrunaway: Chest',
'Denrunaway: Victory'
], ['The Churning Sea', 'Frigid Archery', 'Archery Lessons']),
# Levels 3AA-3AC
create_region(world, player, 'Dragon Freeway', [
'Dragon Freeway: Victory',
], ['Surrounded']),
create_region(world, player, 'Deep Thicket', [
'Deep Thicket: Find Sedge',
'Deep Thicket: Victory',
], ['Darkest Knight']),
create_region(world, player, 'Corrupted Inlet', [
'Corrupted Inlet: Victory',
], ['Robbed']),
# Levels 3BA-3BC
create_region(world, player, 'Mage Mayhem', [
'Mage Mayhem: Caesar',
'Mage Mayhem: Victory',
], ['Open Season', 'Foolish Canal: Mage Mayhem Entrance']),
create_region(world, player, 'Endless Knight', [
'Endless Knight: Victory',
], ['Doggo Mountain', 'Foolish Canal: Endless Knight Entrance']),
create_region(world, player, 'Ambushed in the Middle', [
'Ambushed in the Middle: Victory (Blue)',
'Ambushed in the Middle: Victory (Green)',
], ['Tenri\'s Fall']),
# Levels 3CA-3CC
create_region(world, player, 'The Churning Sea', [
'The Churning Sea: Victory',
], ['Rebel Village']),
create_region(world, player, 'Frigid Archery', [
'Frigid Archery: Light the Torch',
'Frigid Archery: Victory',
], ['A Ballista\'s Revenge']),
create_region(world, player, 'Archery Lessons', [
'Archery Lessons: Chest',
'Archery Lessons: Victory',
], ['Master of the Lake']),
# Levels 4AA-4AC
create_region(world, player, 'Surrounded', [
'Surrounded: Caesar',
'Surrounded: Victory',
]),
create_region(world, player, 'Darkest Knight', [
'Darkest Knight: Victory',
]),
create_region(world, player, 'Robbed', [
'Robbed: Victory',
]),
# Levels 4BAA-4BCA
create_region(world, player, 'Open Season', [
'Open Season: Caesar',
'Open Season: Victory',
]),
create_region(world, player, 'Doggo Mountain', [
'Doggo Mountain: Find all the Dogs',
'Doggo Mountain: Victory',
]),
create_region(world, player, 'Tenri\'s Fall', [
'Tenri\'s Fall: Victory',
]),
# Level 4BAB
create_region(world, player, 'Foolish Canal', [
'Foolish Canal: Victory',
]),
# Levels 4CA-4CC
create_region(world, player, 'Master of the Lake', [
'Master of the Lake: Victory',
]),
create_region(world, player, 'A Ballista\'s Revenge', [
'A Ballista\'s Revenge: Victory',
]),
create_region(world, player, 'Rebel Village', [
'Rebel Village: Victory (Pink)',
'Rebel Village: Victory (Red)',
]),
# Final Level
create_region(world, player, 'Wargroove Finale', [
'Wargroove Finale: Victory'
]),
]
# link up our regions with the entrances
world.get_entrance('Humble Beginnings', player).connect(world.get_region('Humble Beginnings', player))
world.get_entrance('Best Friendssss', player).connect(world.get_region('Best Friendssss', player))
world.get_entrance('A Knight\'s Folly', player).connect(world.get_region('A Knight\'s Folly', player))
world.get_entrance('Denrunaway', player).connect(world.get_region('Denrunaway', player))
world.get_entrance('Wargroove Finale', player).connect(world.get_region('Wargroove Finale', player))
world.get_entrance('Dragon Freeway', player).connect(world.get_region('Dragon Freeway', player))
world.get_entrance('Deep Thicket', player).connect(world.get_region('Deep Thicket', player))
world.get_entrance('Corrupted Inlet', player).connect(world.get_region('Corrupted Inlet', player))
world.get_entrance('Mage Mayhem', player).connect(world.get_region('Mage Mayhem', player))
world.get_entrance('Endless Knight', player).connect(world.get_region('Endless Knight', player))
world.get_entrance('Ambushed in the Middle', player).connect(world.get_region('Ambushed in the Middle', player))
world.get_entrance('The Churning Sea', player).connect(world.get_region('The Churning Sea', player))
world.get_entrance('Frigid Archery', player).connect(world.get_region('Frigid Archery', player))
world.get_entrance('Archery Lessons', player).connect(world.get_region('Archery Lessons', player))
world.get_entrance('Surrounded', player).connect(world.get_region('Surrounded', player))
world.get_entrance('Darkest Knight', player).connect(world.get_region('Darkest Knight', player))
world.get_entrance('Robbed', player).connect(world.get_region('Robbed', player))
world.get_entrance('Open Season', player).connect(world.get_region('Open Season', player))
world.get_entrance('Doggo Mountain', player).connect(world.get_region('Doggo Mountain', player))
world.get_entrance('Tenri\'s Fall', player).connect(world.get_region('Tenri\'s Fall', player))
world.get_entrance('Foolish Canal: Mage Mayhem Entrance', player).connect(world.get_region('Foolish Canal', player))
world.get_entrance('Foolish Canal: Endless Knight Entrance', player).connect(
world.get_region('Foolish Canal', player)
)
world.get_entrance('Master of the Lake', player).connect(world.get_region('Master of the Lake', player))
world.get_entrance('A Ballista\'s Revenge', player).connect(world.get_region('A Ballista\'s Revenge', player))
world.get_entrance('Rebel Village', player).connect(world.get_region('Rebel Village', player))

161
worlds/wargroove/Rules.py Normal file
View File

@ -0,0 +1,161 @@
from typing import List
from BaseClasses import MultiWorld, Region, Location
from ..AutoWorld import LogicMixin
from ..generic.Rules import set_rule
class WargrooveLogic(LogicMixin):
def _wargroove_has_item(self, player: int, item: str) -> bool:
return self.has(item, player)
def _wargroove_has_region(self, player: int, region: str) -> bool:
return self.can_reach(region, 'Region', player)
def _wargroove_has_item_and_region(self, player: int, item: str, region: str) -> bool:
return self.can_reach(region, 'Region', player) and self.has(item, player)
def set_rules(world: MultiWorld, player: int):
# Final Level
set_rule(world.get_location('Wargroove Finale: Victory', player),
lambda state: state._wargroove_has_item(player, "Final Bridges") and
state._wargroove_has_item(player, "Final Walls") and
state._wargroove_has_item(player, "Final Sickle"))
# Level 1
set_rule(world.get_location('Humble Beginnings: Caesar', player), lambda state: True)
set_rule(world.get_location('Humble Beginnings: Chest 1', player), lambda state: True)
set_rule(world.get_location('Humble Beginnings: Chest 2', player), lambda state: True)
set_rule(world.get_location('Humble Beginnings: Victory', player), lambda state: True)
set_region_exit_rules(world.get_region('Humble Beginnings', player),
[world.get_location('Humble Beginnings: Victory', player)])
# Levels 2A-2C
set_rule(world.get_location('Best Friendssss: Find Sedge', player), lambda state: True)
set_rule(world.get_location('Best Friendssss: Victory', player), lambda state: True)
set_region_exit_rules(world.get_region('Best Friendssss', player),
[world.get_location('Best Friendssss: Victory', player)])
set_rule(world.get_location('A Knight\'s Folly: Caesar', player), lambda state: True)
set_rule(world.get_location('A Knight\'s Folly: Victory', player), lambda state: True)
set_region_exit_rules(world.get_region('A Knight\'s Folly', player),
[world.get_location('A Knight\'s Folly: Victory', player)])
set_rule(world.get_location('Denrunaway: Chest', player), lambda state: True)
set_rule(world.get_location('Denrunaway: Victory', player), lambda state: True)
set_region_exit_rules(world.get_region('Denrunaway', player), [world.get_location('Denrunaway: Victory', player)])
# Levels 3AA-3AC
set_rule(world.get_location('Dragon Freeway: Victory', player),
lambda state: state._wargroove_has_item(player, 'Mage'))
set_region_exit_rules(world.get_region('Dragon Freeway', player),
[world.get_location('Dragon Freeway: Victory', player)])
set_rule(world.get_location('Deep Thicket: Find Sedge', player),
lambda state: state._wargroove_has_item(player, 'Mage'))
set_rule(world.get_location('Deep Thicket: Victory', player),
lambda state: state._wargroove_has_item(player, 'Mage'))
set_region_exit_rules(world.get_region('Deep Thicket', player),
[world.get_location('Deep Thicket: Victory', player)])
set_rule(world.get_location('Corrupted Inlet: Victory', player),
lambda state: state._wargroove_has_item(player, 'Barge') or
state._wargroove_has_item(player, 'Merfolk') or
state._wargroove_has_item(player, 'Warship'))
set_region_exit_rules(world.get_region('Corrupted Inlet', player),
[world.get_location('Corrupted Inlet: Victory', player)])
# Levels 3BA-3BC
set_rule(world.get_location('Mage Mayhem: Caesar', player),
lambda state: state._wargroove_has_item(player, 'Harpy') or state._wargroove_has_item(player, 'Dragon'))
set_rule(world.get_location('Mage Mayhem: Victory', player),
lambda state: state._wargroove_has_item(player, 'Harpy') or state._wargroove_has_item(player, 'Dragon'))
set_region_exit_rules(world.get_region('Mage Mayhem', player), [world.get_location('Mage Mayhem: Victory', player)])
set_rule(world.get_location('Endless Knight: Victory', player),
lambda state: state._wargroove_has_item(player, 'Eastern Bridges') and (
state._wargroove_has_item(player, 'Spearman') or
state._wargroove_has_item(player, 'Harpy') or
state._wargroove_has_item(player, 'Dragon')))
set_region_exit_rules(world.get_region('Endless Knight', player),
[world.get_location('Endless Knight: Victory', player)])
set_rule(world.get_location('Ambushed in the Middle: Victory (Blue)', player),
lambda state: state._wargroove_has_item(player, 'Spearman'))
set_rule(world.get_location('Ambushed in the Middle: Victory (Green)', player),
lambda state: state._wargroove_has_item(player, 'Spearman'))
set_region_exit_rules(world.get_region('Ambushed in the Middle', player),
[world.get_location('Ambushed in the Middle: Victory (Blue)', player),
world.get_location('Ambushed in the Middle: Victory (Green)', player)])
# Levels 3CA-3CC
set_rule(world.get_location('The Churning Sea: Victory', player),
lambda state: (state._wargroove_has_item(player, 'Merfolk') or state._wargroove_has_item(player, 'Turtle'))
and state._wargroove_has_item(player, 'Harpoon Ship'))
set_region_exit_rules(world.get_region('The Churning Sea', player),
[world.get_location('The Churning Sea: Victory', player)])
set_rule(world.get_location('Frigid Archery: Light the Torch', player),
lambda state: state._wargroove_has_item(player, 'Archer') and
state._wargroove_has_item(player, 'Southern Walls'))
set_rule(world.get_location('Frigid Archery: Victory', player),
lambda state: state._wargroove_has_item(player, 'Archer'))
set_region_exit_rules(world.get_region('Frigid Archery', player),
[world.get_location('Frigid Archery: Victory', player)])
set_rule(world.get_location('Archery Lessons: Chest', player),
lambda state: state._wargroove_has_item(player, 'Knight') and
state._wargroove_has_item(player, 'Southern Walls'))
set_rule(world.get_location('Archery Lessons: Victory', player),
lambda state: state._wargroove_has_item(player, 'Knight') and
state._wargroove_has_item(player, 'Southern Walls'))
set_region_exit_rules(world.get_region('Archery Lessons', player),
[world.get_location('Archery Lessons: Victory', player)])
# Levels 4AA-4AC
set_rule(world.get_location('Surrounded: Caesar', player),
lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Surrounded'))
set_rule(world.get_location('Surrounded: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Surrounded'))
set_rule(world.get_location('Darkest Knight: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Darkest Knight'))
set_rule(world.get_location('Robbed: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Thief', 'Robbed') and
state._wargroove_has_item(player, 'Rifleman'))
# Levels 4BA-4BC
set_rule(world.get_location('Open Season: Caesar', player),
lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Open Season') and
state._wargroove_has_item(player, 'Knight'))
set_rule(world.get_location('Open Season: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Open Season') and
state._wargroove_has_item(player, 'Knight'))
set_rule(world.get_location('Doggo Mountain: Find all the Dogs', player),
lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Doggo Mountain'))
set_rule(world.get_location('Doggo Mountain: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Knight', 'Doggo Mountain'))
set_rule(world.get_location('Tenri\'s Fall: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Tenri\'s Fall') and
state._wargroove_has_item(player, 'Thief'))
set_rule(world.get_location('Foolish Canal: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Mage', 'Foolish Canal') and
state._wargroove_has_item(player, 'Spearman'))
# Levels 4CA-4CC
set_rule(world.get_location('Master of the Lake: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Warship', 'Master of the Lake'))
set_rule(world.get_location('A Ballista\'s Revenge: Victory', player),
lambda state: state._wargroove_has_item_and_region(player, 'Ballista', 'A Ballista\'s Revenge'))
set_rule(world.get_location('Rebel Village: Victory (Pink)', player),
lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Rebel Village'))
set_rule(world.get_location('Rebel Village: Victory (Red)', player),
lambda state: state._wargroove_has_item_and_region(player, 'Spearman', 'Rebel Village'))
def set_region_exit_rules(region: Region, locations: List[Location], operator: str = "or"):
if operator == "or":
exit_rule = lambda state: any(location.access_rule(state) for location in locations)
else:
exit_rule = lambda state: all(location.access_rule(state) for location in locations)
for region_exit in region.exits:
region_exit.access_rule = exit_rule

View File

@ -0,0 +1,28 @@
<FactionBox>:
orientation: 'vertical'
padding: [10,5,10,5]
size_hint_y: 0.14
<CommanderGroup>:
orientation: 'horizontal'
<CommanderButton>:
text_size: self.size
size_hint: (None, 0.8)
width: 100
markup: True
halign: 'center'
valign: 'middle'
padding_x: 5
outline_width: 1
disabled: True
on_release: setattr(self, 'state', 'down')
<ItemTracker>:
orientation: 'horizontal'
padding_y: 5
<ItemLabel>:
size_hint_x: None
size: self.texture_size
pos_hint: {'left': 1}

View File

@ -0,0 +1,139 @@
import os
import string
import json
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
from .Items import item_table, faction_table
from .Locations import location_table
from .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
from .Options import wargroove_options
class WargrooveWeb(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Wargroove for Archipelago.",
"English",
"wargroove_en.md",
"wargroove/en",
["Fly Sniper"]
)]
class WargrooveWorld(World):
"""
Command an army, in this retro style turn based strategy game!
"""
option_definitions = wargroove_options
game = "Wargroove"
topology_present = True
data_version = 1
web = WargrooveWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
def _get_slot_data(self):
return {
'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)),
'income_boost': self.multiworld.income_boost[self.player],
'commander_defense_boost': self.multiworld.commander_defense_boost[self.player],
'can_choose_commander': self.multiworld.commander_choice[self.player] != 0,
'starting_groove_multiplier': 20 # Backwards compatibility in case this ever becomes an option
}
def generate_early(self):
# Selecting a random starting faction
if self.multiworld.commander_choice[self.player] == 2:
factions = [faction for faction in faction_table.keys() if faction != "Starter"]
starting_faction = WargrooveItem(self.multiworld.random.choice(factions) + ' Commanders', self.player)
self.multiworld.push_precollected(starting_faction)
def generate_basic(self):
# Fill out our pool with our items from the item table
pool = []
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
ignore_faction_items = self.multiworld.commander_choice[self.player] == 0
for name, data in item_table.items():
if data.code is not None and name not in precollected_item_names and not data.classification == ItemClassification.filler:
if name.endswith(' Commanders') and ignore_faction_items:
continue
item = WargrooveItem(name, self.player)
pool.append(item)
# Matching number of unfilled locations with filler items
locations_remaining = len(location_table) - 1 - len(pool)
while locations_remaining > 0:
# Filling the pool equally with both types of filler items
pool.append(WargrooveItem("Commander Defense Boost", self.player))
locations_remaining -= 1
if locations_remaining > 0:
pool.append(WargrooveItem("Income Boost", self.player))
locations_remaining -= 1
self.multiworld.itempool += pool
# Placing victory event at final location
victory = WargrooveItem("Wargroove Victory", self.player)
self.multiworld.get_location("Wargroove Finale: Victory", self.player).place_locked_item(victory)
self.multiworld.completion_condition[self.player] = lambda state: state.has("Wargroove Victory", self.player)
def set_rules(self):
set_rules(self.multiworld, self.player)
def create_item(self, name: str) -> Item:
return WargrooveItem(name, self.player)
def create_regions(self):
create_regions(self.multiworld, self.player)
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in wargroove_options:
option = getattr(self.multiworld, option_name)[self.player]
slot_data[option_name] = int(option.value)
return slot_data
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(["Commander Defense Boost", "Income Boost"])
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, player, world)
if locations:
for location in locations:
loc_id = location_table.get(location, 0)
location = WargrooveLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret
class WargrooveLocation(Location):
game: str = "Wargroove"
def __init__(self, player: int, name: str, address=None, parent=None):
super(WargrooveLocation, self).__init__(player, name, address, parent)
if address is None:
self.event = True
self.locked = True
class WargrooveItem(Item):
game = "Wargroove"
def __init__(self, name, player: int = None):
item_data = item_table[name]
super(WargrooveItem, self).__init__(
name,
item_data.classification,
item_data.code,
player
)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,34 @@
# Wargroove (Steam, Windows)
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
This randomizer shuffles units, map events, factions and boosts. It features a custom, non-linear campaign where the
final level and 3 branching paths are all available to the player from the start. The player cannot beat the final level
without specific items scattered throughout the branching paths. Certain levels on these paths may require
specific units or items in order to progress.
## What items and locations get shuffled?
1. Every buildable unit in the game (except for soldiers and dogs, which are free).
2. Commanders available to certain factions. If the player acquires the Floran Commanders, they can select any commander
from that faction.
3. Income and Commander Defense boosts that provide the player with extra income or extra commander defense.
4. Special map events like the Eastern Bridges or the Southern Walls, which unlock certain locations in certain levels.
## Which items can be in another player's world?
Any of the above items can be in another player's world.
## When the player receives an item, what happens?
When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action
is taken in game.
## What is the goal of this game when randomized?
The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`.

View File

@ -0,0 +1,83 @@
# Wargroove Setup Guide
## Required Files
- Wargroove with the Double Trouble DLC installed through Steam on Windows
- Only the Steam Windows version is supported. MAC, Switch, Xbox, and Playstation are not supported.
- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
## Backup playerProgress files
`playerProgress` and `playerProgress.bak` contain save data for all of your Wargroove campaigns. Backing up these files
is strongly recommended in case they become corrupted.
1. Type `%appdata%\Chucklefish\Wargroove\save` in the file browser and hit enter.
2. Copy the `playerProgress` and `playerProgress.bak` files and paste them into a backup directory.
## Update host.yaml to include the Wargroove root directory
1. Look for your Archipelago install files. By default, the installer puts them in `C:\ProgramData\Archipelago`.
2. Open the `host.yaml` file in your favorite text editor (Notepad will work).
3. Put your Wargroove root directory in the `root_directory:` under the `wargroove_options:` section.
- The Wargroove root directory can be found by going to
`Steam->Right Click Wargroove->Properties->Local Files->Browse Local Files` and copying the path in the address bar.
- Paste the path in between the quotes next to `root_directory:` in the `host.yaml`.
- You may have to replace all single \\ with \\\\.
4. Start the Wargroove client.
## Installing the Archipelago Wargroove Mod and Campaign files
1. Shut down Wargroove if it is open.
2. Start the ArchipelagoWargrooveClient.exe from the Archipelago installation.
This should install the mod and campaign for you.
3. Start Wargroove.
## Verify the campaign can be loaded
1. Start Wargroove from Steam.
2. Go to `Story->Campaign->Custom->Archipelago` and click play. You should see the first level.
## Starting a Multiworld game
1. Start the Wargroove Client and connect to the server. Enter your username from your
[settings file.](/games/Wargroove/player-settings)
2. Start Wargroove and play the Archipelago campaign by going to `Story->Campaign->Custom->Archipelago`.
## Ending a Multiworld game
It is strongly recommended that you delete your campaign progress after finishing a multiworld game.
This can be done by going to the level selection screen in the Archipelago campaign, hitting `ESC` and clicking the
`Delete Progress` button. The main menu should now be visible.
## Updating to a new version of the Wargroove mod or downloading new campaign files
First, delete your campaign progress by going to the level selection screen in the Archipelago campaign,
hitting `ESC` and clicking the `Delete Progress` button.
Follow the `Installing the Archipelago Wargroove Mod and Campaign files` steps again, but look for the latest version
to download. In addition, follow the steps outlined in `Wargroove crashes when trying to run the Archipelago campaign`
when attempting to update the campaign files and the mod.
## Troubleshooting
### The game is too hard
`Go to the campaign overview screen->Hit escape on the keyboard->Click adjust difficulty->Adjust the setttings`
### The mod doesn't load
Double-check the mod installation under `%appdata%\Chucklefish\Wargroove\mods`. There should be 3 `.dat` files in
`%appdata%\Chucklefish\Wargroove\mods\ArchipelagoMod`. Otherwise, follow
`Installing the Archipelago Wargroove Mod and Campaign files` steps once more.
### Wargroove crashes or there is a lua error
Wargroove is finicky, but there could be several causes for this. If it happens often or can be reproduced,
please submit a bug report in the tech-support channel on the [discord](https://discord.gg/archipelago).
### Wargroove crashes when trying to run the Archipelago campaign
This is caused by not deleting campaign progress before updating the mod and campaign files.
1. Go to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod.
2. Wargroove will give an error message.
3. Go back to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod again.
4. Wargroove crashes.
5. Go back to `Custom Content->Create->Campaign->Archipelago->Edit` and attempt to update the mod again.
6. In the edit menu, hit `ESC` and click `Delete Progress`.
7. If the above steps do not allow you to start the campaign from `Story->Campaign->Custom->Archipelago` replace
`playerProgress` and `playerProgress.bak` with your previously backed up files.
### Mod is out of date when trying to run the Archipelago campaign
Please follow the above steps in `Wargroove crashes when trying to run the Archipelago campaign`.