Skip to content

Commit

Permalink
new feature: mod customization
Browse files Browse the repository at this point in the history
  • Loading branch information
Shazbot committed Nov 9, 2023
1 parent 374e338 commit 5def6d6
Show file tree
Hide file tree
Showing 35 changed files with 2,149 additions and 303 deletions.
23 changes: 22 additions & 1 deletion locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,26 @@
"mergeModsHelp2": "The merged mod won't have the same file names as the merged mods which can affect load order priority, so skip merging mods that require manual load order fiddling. That said, those kind of mods should be incredibly rare and as a rule you should never manually touch load order anyway!",
"mergeModsHelp3": "When mods get updated the merged pack will have the old outdated mod inside it. You should get a warning in red (it'll be above the Play button) warning you about this and you can then right click the merged pack and use the Update (Re-merge) option which will update the merged pack. The warning can appear when you start the app but disappears once we get newer info from the workshop, you don't have to update it in that case.",
"mergeModsHelp4": "You can leave the mods that have been merged enabled in the mod manager, the manager will automatically skip them if they're already present in a merged mod you have enabled. This is reliant on the .json file, if it's missing you'll have to disable those mods or the game will crash since it doesn't like duplicate files in mods.",
"language": "Language:"
"language": "Language:",
"customizeMod": "Customize Mod",
"groupingsUnitPermissions": "Units, Per Group",
"buildingsPermissions": "Buildings",
"unit": "Unit",
"unitKeySortTableOrder": "(Default Order)",
"owner": "Owner",
"group": "Group",
"allowed": "Allowed",
"factionsUnitPermissions": "Units, Per Faction",
"uniqueAgentsPermissions": "Legendary Lords And Heroes",
"agentSubtype": "Agent Subtype",
"campaignGroup": "Campaign Group",
"agentsPermissions": "Lords And Heroes",
"agentSubtypeAndType": "Agent Subtype (Agent Type)",
"faction": "Faction",
"Building": "Building",
"cultureSubcultureFaction": "Culture, Subculture, Faction (All 3 Optional)",
"modCustomizationHelp1": "This panel allows you to customize mods, currently you can disable units, buildings and agents (lords and heroes).",
"modCustomizationHelp2": "The way the game handles unit permissions, a unit can have a group permission (which can affect multiple factions) or a per-faction permission, or both. For game stability reasons only group permissions can be disabled. Note this won't affect custom battles since that uses a different system!",
"modCustomizationHelp3": "Generic agents (lords and heroes) are tied to campaign groups and you can disable them that way, except for legendary agents which have their own tab.",
"modCustomizationHelp4": "The manager customizes a mod by creating a copy of the pack, with the necessary changes, inside a new 'whmm_overwrites' subfolder inside your WH3 folder each time you start the game. Note those copies are currently never deleted or cleaned up. I'm not sure how this affects multiplayer but it's likely you'll get 'different versions' warnings since the game thinks your packs are different even when they're not, but I'm not sure. Removing the customization for a mod will also give you a warning when loading a save that the pack is missing since we're now using the original mod pack, not the customized version of it, but you can ignore that."
}
23 changes: 22 additions & 1 deletion locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,26 @@
"mergeModsHelp2": "合并的模组不会与被合并模组同名,因为文件名会影响加载顺序,因此跳过那些需要手动调整排序的模组。虽然这样的模组比较罕见,但通常情况下,你不应该手动修改模组的加载顺序!",
"mergeModsHelp3": "当模组有新的更新时,合并模组包中将会存在过期的模组。你会在启动游戏按钮上得到红色的警告,你可以通过右键点击合并模组包,并且使用更新(重新合并)选项,这将会更新合并包。这个警告可能会在你启动app时出现,同时也会随着软件更新创意工坊内容后消失,这种情况下你不必执行更新。",
"mergeModsHelp4": "你可以在管理器中保持那些已被合并过的(单个)模组的启用状态,管理器如果发现已启用的模组被包含在一个合并包中,会自动跳过他们。这个功能依赖于.json文件,如果json文件缺失,那你就必须得手动禁用那些模组,否则游戏将可能由于重复文件而崩溃。",
"language": "语言:"
"language": "语言:",
"customizeMod": "Customize Mod",
"groupingsUnitPermissions": "Units, Per Group",
"buildingsPermissions": "Buildings",
"unit": "Unit",
"unitKeySortTableOrder": "(Default Order)",
"owner": "Owner",
"group": "Group",
"allowed": "Allowed",
"factionsUnitPermissions": "Units, Per Faction",
"uniqueAgentsPermissions": "Legendary Lords And Heroes",
"agentSubtype": "Agent Subtype",
"campaignGroup": "Campaign Group",
"agentsPermissions": "Lords And Heroes",
"agentSubtypeAndType": "Agent Subtype (Agent Type)",
"faction": "Faction",
"Building": "Building",
"cultureSubcultureFaction": "Culture, Subculture, Faction (All 3 Optional)",
"modCustomizationHelp1": "This panel allows you to customize mods, currently you can disable units, buildings and agents (lords and heroes).",
"modCustomizationHelp2": "The way the game handles unit permissions, a unit can have a group permission (which can affect multiple factions) or a per-faction permission, or both. For game stability reasons only group permissions can be disabled. Note this won't affect custom battles since that uses a different system!",
"modCustomizationHelp3": "Generic agents (lords and heroes) are tied to campaign groups and you can disable them that way, except for legendary agents which have their own tab.",
"modCustomizationHelp4": "The manager customizes a mod by creating a copy of the pack, with the necessary changes, inside a new 'whmm_overwrites' subfolder inside your WH3 folder each time you start the game. Note those copies are currently never deleted or cleaned up. I'm not sure how this affects multiplayer but it's likely you'll get 'different versions' warnings since the game thinks your packs are different even when they're not, but I'm not sure. Removing the customization for a mod will also give you a warning when loading a save that the pack is missing since we're now using the original mod pack, not the customized version of it, but you can ignore that."
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/react": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"@types/clone-deep": "^4.0.3",
"@types/fs-extra": "^9.0.13",
"@types/nightmare": "^2.10.6",
"@types/object-hash": "^3.0.2",
Expand Down Expand Up @@ -147,6 +148,7 @@
"binary-file": "^0.2.3",
"chokidar": "^3.5.3",
"classnames": "^2.3.2",
"clone-deep": "^4.0.1",
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7",
"electron-fetch": "^1.9.1",
Expand Down
1 change: 1 addition & 0 deletions src/appConfigFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const appStateToConfigAppState = (appState: AppState): AppStateToWrite => {
categories: appState.categories,
modRowsSortingType: appState.modRowsSortingType,
currentLanguage: appState.currentLanguage,
packDataOverwrites: appState.packDataOverwrites,
};
};

Expand Down
129 changes: 77 additions & 52 deletions src/appSlice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AppFolderPaths } from "./appData";
import { PackCollisions } from "./packFileTypes";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import hash from "object-hash";
import {
adjustDuplicates,
findAlwaysEnabledMods,
Expand All @@ -9,7 +10,7 @@ import {
} from "./modsHelpers";
import { SortingType } from "./utility/modRowSorting";
import { compareModNames, sortAsInPreset, sortByNameAndLoadOrder } from "./modSortingHelpers";
import { index } from "handsontable/helpers/dom";
import initialState from "./initialAppState";

const addCategoryByPayload = (state: AppState, payload: AddCategoryPayload) => {
const { mods, category } = payload;
Expand Down Expand Up @@ -48,53 +49,7 @@ const removeCategoryByPayload = (state: AppState, payload: RemoveCategoryPayload

const appSlice = createSlice({
name: "app",
initialState: {
currentPreset: {
mods: [],
name: "",
},
categories: [],
lastSelectedPreset: null,
presets: [],
filter: "",
alwaysEnabledMods: [],
hiddenMods: [],
saves: [],
isOnboardingToRun: false,
wasOnboardingEverRun: false,
isDev: false,
isAdmin: false,
areThumbnailsEnabled: false,
isClosedOnPlay: false,
isAuthorEnabled: false,
isMakeUnitsGeneralsEnabled: false,
isScriptLoggingEnabled: false,
isSkipIntroMoviesEnabled: false,
isAutoStartCustomBattleEnabled: false,
allMods: [],
packsData: {},
packCollisions: { packTableCollisions: [], packFileCollisions: [] },
newMergedPacks: [],
pathsOfReadPacks: [],
appFolderPaths: { gamePath: "", contentFolder: "" },
isSetAppFolderPathsDone: false,
overwrittenDataPackedFiles: {},
outdatedPackFiles: {},
startArgs: [],
currentTab: "mods",
isCreateSteamCollectionOpen: false,
isWH3Running: false,
toasts: [],
removedModsCategories: {},
// before we make symbolic links in data queue those mods to be re-enabled
dataModsToEnableByName: [],
// if a enabled mod was removed it's possible it was updated, re-enabled it then
removedModsData: [],
modRowsSortingType: SortingType.Ordered,
importedMods: [],
availableLanguages: ["en"],
currentLanguage: "en",
} as AppState,
initialState: initialState,
reducers: {
// when mutating mods make sure you get the same mod from state.currentPreset.mods and don't change the mod that's from the payload
setModRowsSortingType: (state: AppState, action: PayloadAction<SortingType>) => {
Expand Down Expand Up @@ -388,8 +343,17 @@ const appSlice = createSlice({
const packsData = action.payload;

for (const packData of packsData) {
state.packsData[packData.packPath] = packData;
if (!state.packsData[packData.packPath]) {
state.packsData[packData.packPath] = packData;
} else if (packData.packedFiles) {
state.packsData[packData.packPath].packedFiles =
state.packsData[packData.packPath].packedFiles || {};
for (const [packedFilePath, packedFile] of Object.entries(packData.packedFiles))
state.packsData[packData.packPath].packedFiles[packedFilePath] = packedFile;
}
}

console.log("APPSLICE set setPacksData:", packsData);
},
setPacksDataRead: (state: AppState, action: PayloadAction<string[]>) => {
const packPaths = action.payload;
Expand Down Expand Up @@ -456,6 +420,7 @@ const appSlice = createSlice({
state.isAutoStartCustomBattleEnabled = fromConfigAppState.isAutoStartCustomBattleEnabled;
state.modRowsSortingType = fromConfigAppState.modRowsSortingType || state.modRowsSortingType;
state.currentLanguage = fromConfigAppState.currentLanguage || "en";
state.packDataOverwrites = fromConfigAppState.packDataOverwrites || {};

const categoriesFromMods = new Set(state.currentPreset.mods.map((mod) => mod.categories ?? []).flat());
if (fromConfigAppState.categories) {
Expand Down Expand Up @@ -605,10 +570,17 @@ const appSlice = createSlice({

if (!modToChange || !modRelativeTo) return;

state.currentPreset.mods.splice(state.currentPreset.mods.indexOf(modToChange), 1);
const newIndex = state.currentPreset.mods.indexOf(modRelativeTo);
state.currentPreset.mods.splice(newIndex, 0, modToChange);
console.log("mod to change:", modToChange.name);
console.log("mod relative to:", modRelativeTo.name);

let newIndex = state.currentPreset.mods.indexOf(modToChange);
if (modToChange != modRelativeTo) {
state.currentPreset.mods.splice(state.currentPreset.mods.indexOf(modToChange), 1);
newIndex = state.currentPreset.mods.indexOf(modRelativeTo);
state.currentPreset.mods.splice(newIndex, 0, modToChange);
}

console.log("new load order for:", modToChange.name, newIndex);
modToChange.loadOrder = newIndex;
},
resetModLoadOrderAll: (state: AppState) => {
Expand Down Expand Up @@ -767,7 +739,44 @@ const appSlice = createSlice({
setAvailableLanguages: (state: AppState, action: PayloadAction<string[]>) => {
state.availableLanguages = action.payload;
},
setPackDataOverwrites: (state: AppState, action: PayloadAction<PackDataOverwritePayload>) => {
const overwrite = action.payload;
state.packDataOverwrites[overwrite.packName] = state.packDataOverwrites[overwrite.packName] || [];
state.packDataOverwrites[overwrite.packName] = state.packDataOverwrites[overwrite.packName].filter(
(iterOverwrite) =>
iterOverwrite.packFilePath != overwrite.packFilePath ||
iterOverwrite.columnsId != overwrite.columnsId
);
state.packDataOverwrites[overwrite.packName].push({
packFilePath: overwrite.packFilePath,
columnsId: overwrite.columnsId,
operation: overwrite.operation,
overwriteData: overwrite.overwriteData,
overwriteIndex: overwrite.overwriteIndex,
columnIndices: overwrite.columnIndices,
columnValues: overwrite.columnValues,
});
},
removePackDataOverwrite: (state: AppState, action: PayloadAction<PackDataOverwritePayload>) => {
const overwrite = action.payload;
state.packDataOverwrites[overwrite.packName] = state.packDataOverwrites[overwrite.packName] || [];
state.packDataOverwrites[overwrite.packName] = state.packDataOverwrites[overwrite.packName].filter(
(iterOverwrite) =>
iterOverwrite.packFilePath != overwrite.packFilePath ||
iterOverwrite.columnsId != overwrite.columnsId
);
if (state.packDataOverwrites[overwrite.packName].length == 0)
delete state.packDataOverwrites[overwrite.packName];
},
removeAllPackDataOverwrites: (state: AppState, action: PayloadAction<string>) => {
const packName = action.payload;
delete state.packDataOverwrites[packName];
},
selectDBTable: (state: AppState, action: PayloadAction<DBTableSelection>) => {
if (state.currentDBTableSelection == action.payload) {
console.log("selectDBTable for same selection, not updating app state");
return;
}
state.currentDBTableSelection = action.payload;
},
setCurrentTab: (state: AppState, action: PayloadAction<MainWindowTab>) => {
Expand All @@ -783,6 +792,17 @@ const appSlice = createSlice({
addToast: (state: AppState, action: PayloadAction<Toast>) => {
state.toasts.push(action.payload);
},
setModBeingCustomized: (state: AppState, action: PayloadAction<Mod | undefined>) => {
state.modBeingCustomized = action.payload;
},
setCustomizableMods: (state: AppState, action: PayloadAction<Record<string, string[]>>) => {
if (hash(state.customizableMods) == hash(action.payload)) {
console.log("setCustomizableMods for same mods, not updating app state");
return;
}
state.customizableMods = action.payload;
console.log("setCustomizableMods:", state.customizableMods);
},
selectCategory: (state: AppState, action: PayloadAction<CategorySelectionPayload>) => {
const { mods, category, selectOperation } = action.payload;

Expand Down Expand Up @@ -871,6 +891,11 @@ export const {
removeCategory,
setModRowsSortingType,
setAvailableLanguages,
setPackDataOverwrites,
removePackDataOverwrite,
removeAllPackDataOverwrites,
setModBeingCustomized,
setCustomizableMods,
} = appSlice.actions;

export default appSlice.reducer;
1 change: 1 addition & 0 deletions src/components/Categories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ const Categories = React.memo(() => {
<ModDropdownOptions
isOpen={isContextMenuOpen}
mod={currentlySelectedMods[0]}
mods={mods}
></ModDropdownOptions>
)}
</div>
Expand Down
Loading

0 comments on commit 5def6d6

Please sign in to comment.