Skip to content

Commit

Permalink
feature: copy pack to data as a symbolic link
Browse files Browse the repository at this point in the history
  • Loading branch information
Shazbot committed Apr 17, 2023
1 parent 903f4da commit 269d8ab
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 103 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@
"@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5",
"@types/winreg": "^1.2.31",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@vercel/webpack-asset-relocator-loader": "^1.7.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.2.5",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1",
"electron": "19.0.4",
"electron-devtools-installer": "^3.2.0",
"eslint": "^8.27.0",
"eslint": "^8.38.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-storybook": "^0.5.12",
"fork-ts-checker-webpack-plugin": "^7.2.11",
Expand Down
1 change: 1 addition & 0 deletions src/ModRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const ModRow = memo(
<span
className={classNames("break-all", "flex", "items-center", {
["text-orange-500"]: mod.isInData,
["text-blue-400"]: mod.isSymbolicLink,
})}
>
{mod.isDeleted && (
Expand Down
60 changes: 60 additions & 0 deletions src/OptionsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
toggleIsScriptLoggingEnabled,
toggleIsSkipIntroMoviesEnabled,
toggleMakeUnitsGenerals,
dataModsToEnableByName,
} from "./appSlice";
import Drawer from "./Drawer";
import { useAppDispatch, useAppSelector } from "./hooks";
Expand All @@ -24,6 +25,10 @@ const cleanData = () => {
window.api?.cleanData();
};

const cleanSymbolicLinksInData = () => {
window.api?.cleanSymbolicLinksInData();
};

const exportModNamesToClipboard = (enabledMods: Mod[]) => {
window.api?.exportModNamesToClipboard(enabledMods);
};
Expand Down Expand Up @@ -97,6 +102,18 @@ const OptionsDrawer = memo(() => {
[enabledMods]
);

const copyToDataAsSymbolicLink = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (e.shiftKey) {
window.api?.copyToDataAsSymbolicLink();
} else {
window.api?.copyToDataAsSymbolicLink(enabledMods.map((mod) => mod.path));
dataModsToEnableByName.push(...enabledMods.map((mod) => mod.name));
}
},
[enabledMods]
);

return (
<div>
<GamePathsSetup
Expand Down Expand Up @@ -219,6 +236,7 @@ const OptionsDrawer = memo(() => {
As a modder this can overwrite your mod in data with an older version you have in
content!
</div>
<div>Mods that are in data will have a red name in the manager.</div>
</>
}
>
Expand All @@ -242,6 +260,48 @@ const OptionsDrawer = memo(() => {
</button>
</div>

<p className="mt-6 mb-4 text-sm text-gray-500 dark:text-gray-400">
You can also copy them as symbolic links (basically a shortcut) so they don't take up duplicate
space. They will also always be up-to-date with the content mod since they're just a shortcut to
the actual mod.
</p>

<div className="flex mt-2">
<button
className="make-tooltip-w-full inline-block px-6 py-2.5 bg-purple-600 text-white font-medium text-xs leading-tight rounded shadow-md hover:bg-purple-700 hover:shadow-lg focus:bg-purple-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-purple-800 active:shadow-lg transition duration-150 ease-in-out m-auto w-[70%]"
onClick={(e) => copyToDataAsSymbolicLink(e)}
>
<Tooltip
placement="top"
style="light"
content={
<>
<div>Will create Symbolics Links of currently enabled mods from content into data.</div>
<div>Hold Shift if you want to create links of all mods.</div>
<div>This won't create links of mods that already exist in data.</div>
<div>Mods that are symbolic links will have a blue name in the manager.</div>
</>
}
>
<span className="uppercase">Create symbolic links in data</span>
</Tooltip>
</button>
</div>
<div className="flex mt-2 w-full">
<button
className="make-tooltip-w-full inline-block px-6 py-2.5 bg-purple-600 text-white font-medium text-xs leading-tight rounded shadow-md hover:bg-purple-700 hover:shadow-lg focus:bg-purple-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-purple-800 active:shadow-lg transition duration-150 ease-in-out m-auto w-[70%]"
onClick={() => cleanSymbolicLinksInData()}
>
<Tooltip
placement="bottom"
style="light"
content="Will remove all symbolic links in data. Won't touch real mods that aren't symbolic links."
>
<span className="uppercase">Clean symbolic links in data</span>
</Tooltip>
</button>
</div>

<h6 className="mt-10">Hidden mods</h6>
<p className="mb-3 text-sm text-gray-500 dark:text-gray-400">
Unhide mods you've previously hidden:
Expand Down
2 changes: 2 additions & 0 deletions src/appData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface AppData {
currentlyReadingModPaths: string[];
dataPack?: Pack;
overwrittenDataPackedFiles: Record<string, string[]>;
enabledMods: Mod[];
}

export type AppFolderPaths = { gamePath: string; contentFolder: string };
Expand All @@ -31,4 +32,5 @@ export default {
compatData: { packTableCollisions: [], packFileCollisions: [] },
currentlyReadingModPaths: [],
overwrittenDataPackedFiles: {},
enabledMods: [],
} as AppData;
15 changes: 14 additions & 1 deletion src/appSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
} from "./modsHelpers";

// if a enabled mod was removed it's possible it was updated, re-enabled it then
const removedEnabledModPaths: string[] = [];
let removedEnabledModPaths: string[] = [];

// queue mods in data that should be enabled when they're added
// for use with copy to data so we re-enable mods
export const dataModsToEnableByName: string[] = [];

const appSlice = createSlice({
name: "app",
Expand Down Expand Up @@ -135,6 +139,15 @@ const appSlice = createSlice({

if (removedEnabledModPaths.find((path) => path === mod.path)) {
mod.isEnabled = true;
removedEnabledModPaths = removedEnabledModPaths.filter((pathOfRemoved) => pathOfRemoved != mod.path);
}

if (mod.isInData && dataModsToEnableByName.find((nameOfToEnable) => nameOfToEnable === mod.name)) {
mod.isEnabled = true;
dataModsToEnableByName.splice(
dataModsToEnableByName.findIndex((nameOfToEnable) => nameOfToEnable === mod.name),
1
);
}

if (state.dataFromConfig?.currentPreset.mods.find((iterMod) => iterMod.path == mod.path)?.isEnabled) {
Expand Down
2 changes: 2 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PackedFile, PackCollisions } from "./packFileTypes";
import { AppFolderPaths } from "./appData";
import { DBVersion } from "./schema";
import { api } from "./preload";
export {};

declare global {
Expand Down Expand Up @@ -35,6 +36,7 @@ declare global {
size: number;
mergedModsData?: MergedModsData[];
subbedTime?: number;
isSymbolicLink: boolean;
}

interface ModData {
Expand Down
51 changes: 49 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ if (!gotTheLock) {
isDeleted: false,
isMovie: false,
size: 0,
isSymbolicLink: false,
};
if (appData.packsData.every((iterPack) => iterPack.path != dataPackPath)) {
console.log("READING DATA PACK");
Expand Down Expand Up @@ -389,6 +390,7 @@ if (!gotTheLock) {
.watch([`${dataFolder}/*.pack`], {
ignoreInitial: true,
awaitWriteFinish: true,
followSymlinks: false,
ignored: /whmm_backups/,
})
.on("add", async (path) => {
Expand Down Expand Up @@ -638,6 +640,7 @@ if (!gotTheLock) {
});

ipcMain.on("copyToData", async (event, modPathsToCopy?: string[]) => {
console.log("copyToData: modPathsToCopy:", modPathsToCopy);
const mods = await getMods(log);
let withoutDataMods = mods.filter((mod) => !mod.isInData);
if (modPathsToCopy) {
Expand All @@ -655,7 +658,29 @@ if (!gotTheLock) {
});

await Promise.allSettled(copyPromises);
getAllMods();
// getAllMods();
});

ipcMain.on("copyToDataAsSymbolicLink", async (event, modPathsToCopy?: string[]) => {
console.log("copyToDataAsSymbolicLink modPathsToCopy:", modPathsToCopy);
const mods = await getMods(log);
let withoutDataMods = mods.filter((mod) => !mod.isInData);
if (modPathsToCopy) {
withoutDataMods = withoutDataMods.filter((mod) =>
modPathsToCopy.some((modPathToCopy) => modPathToCopy == mod.path)
);
}
const copyPromises = withoutDataMods.map((mod) => {
mainWindow?.webContents.send(
"handleLog",
`CREATING SYMLINK of ${mod.path} to ${appData.gamePath}\\data\\${mod.name}`
);

return fsExtra.symlink(mod.path, `${appData.gamePath}\\data\\${mod.name}`);
});

await Promise.allSettled(copyPromises);
// getAllMods();
});

ipcMain.on("cleanData", async () => {
Expand All @@ -673,13 +698,29 @@ if (!gotTheLock) {
});

await Promise.allSettled(deletePromises);
getAllMods();
// getAllMods();
});

ipcMain.on("cleanSymbolicLinksInData", async () => {
const mods = await getMods(log);
const symLinksToDelete = mods.filter((mod) => mod.isInData && mod.isSymbolicLink);
console.log("symLinksToDelete", symLinksToDelete);
const deletePromises = symLinksToDelete.map((mod) => {
mainWindow?.webContents.send("handleLog", `DELETING SYMLINK ${mod.path}`);

return fs.unlink(mod.path);
});

await Promise.allSettled(deletePromises);
// getAllMods();
});

ipcMain.on("saveConfig", (event, data: AppState) => {
console.log("saveConfig");
const enabledMods = data.currentPreset.mods.filter(
(iterMod) => iterMod.isEnabled || data.alwaysEnabledMods.find((mod) => mod.name === iterMod.name)
);
appData.enabledMods = enabledMods;
const hiddenAndEnabledMods = data.hiddenMods.filter((iterMod) =>
enabledMods.find((mod) => mod.name === iterMod.name)
);
Expand Down Expand Up @@ -834,6 +875,7 @@ if (!gotTheLock) {
}
console.log("ON requestOpenModInViewer", modPath);
viewerWindow?.webContents.send("openModInViewer", modPath);
viewerWindow?.setTitle(`WH3 Mod Manager v${version}: viewing ${nodePath.basename(modPath)}`);
getPackData(modPath);
if (viewerWindow) {
viewerWindow.focus();
Expand Down Expand Up @@ -1013,6 +1055,10 @@ if (!gotTheLock) {
console.log("SENDING QUEUED DATA TO VIEWER");
viewerWindow?.webContents.send("setPacksData", queuedViewerData);
viewerWindow?.webContents.send("openModInViewer", queuedViewerData[0]?.packPath);
if (queuedViewerData[0]?.packPath)
viewerWindow?.setTitle(
`WH3 Mod Manager v${version}: viewing ${nodePath.basename(queuedViewerData[0]?.packPath)}`
);
viewerWindow?.focus();
queuedViewerData = [];
};
Expand Down Expand Up @@ -1229,6 +1275,7 @@ if (!gotTheLock) {
isDeleted: false,
isMovie: false,
size: 0,
isSymbolicLink: false,
};

let extraEnabledMods = "";
Expand Down
21 changes: 15 additions & 6 deletions src/modFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,10 @@ export async function getDataMod(filePath: string, log: (msg: string) => void):

let lastChangedLocal = undefined;
let size = -1;
let isSymbolicLink = false;
try {
[lastChangedLocal, size] = await fsPromises.stat(filePath).then((stats) => {
return [stats.mtimeMs, stats.size];
[lastChangedLocal, size, isSymbolicLink] = await fsPromises.lstat(filePath).then((stats) => {
return [stats.mtimeMs, stats.size, stats.isSymbolicLink()] as [number, number, boolean];
});
} catch (err) {
log(`ERROR: ${err}`);
Expand Down Expand Up @@ -197,6 +198,7 @@ export async function getDataMod(filePath: string, log: (msg: string) => void):
isMovie: false,
size,
mergedModsData,
isSymbolicLink,
};
return mod;
}
Expand All @@ -220,7 +222,7 @@ const getDataMods = async (gameDir: string, log: (msg: string) => void): Promise
const dataModsPromises = files
.filter(
(file) =>
file.isFile() &&
!file.isDirectory() &&
file.name.endsWith(".pack") &&
!vanillaPacks.find((vanillaPack) => file.name === vanillaPack)
)
Expand Down Expand Up @@ -331,11 +333,12 @@ export async function getContentModInFolder(

let lastChangedLocal = undefined;
let size = -1;
let isSymbolicLink = false;
try {
[lastChangedLocal, size] = await fsPromises
.stat(nodePath.join(contentSubfolder, pack.name))
[lastChangedLocal, size, isSymbolicLink] = await fsPromises
.lstat(nodePath.join(contentSubfolder, pack.name))
.then((stats) => {
return [stats.mtimeMs, stats.size];
return [stats.mtimeMs, stats.size, stats.isSymbolicLink()] as [number, number, boolean];
});
} catch (err) {
log(`ERROR: ${err}`);
Expand All @@ -360,6 +363,7 @@ export async function getContentModInFolder(
isMovie: false,
subbedTime: (subbedTime != -1 && subbedTime) || lastChangedLocal,
size,
isSymbolicLink,
};
return mod;
}
Expand All @@ -377,6 +381,11 @@ export async function getMods(log: (msg: string) => void): Promise<Mod[]> {
const dataMods = await getDataMods(appData.gamePath, log);
mods.push(...dataMods);

console.log(
"DATa MODS THAT ARE SIMLINKS:",
dataMods.filter((mod) => mod.isSymbolicLink)
);

const files = await fsPromises.readdir(contentFolder, { withFileTypes: true });
const newMods = files
.filter((file) => file.isDirectory())
Expand Down
10 changes: 10 additions & 0 deletions src/packFileSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,17 @@ const createBattlePermissionsData = (packsData: Pack[], pack_files: PackedFile[]
)
.reduce((previous, packedFile) => previous.concat(packedFile), []);

const dataPack = packsData.find((packData) => packData.name === "data.pack");
if (!dataPack) return;
const vanillaBattlePersmission = dataPack.packedFiles.find((pf) =>
pf.name.startsWith("db\\units_custom_battle_permissions_tables\\")
);
if (!vanillaBattlePersmission) return;

const vanillaBattlePersmissionVersion = vanillaBattlePersmission.version;

const battlePermissionsSchemaFields = battlePermissions.reduce((previous, packedFile) => {
if (packedFile.version != vanillaBattlePersmissionVersion) return previous;
return (packedFile.schemaFields && previous.concat(packedFile.schemaFields)) || previous;
}, [] as SchemaField[]);
pack_files.push({
Expand Down
3 changes: 3 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const api = {
ipcRenderer.on("openModInViewer", callback),
readAppConfig: () => ipcRenderer.send("readAppConfig"),
copyToData: (modPathsToCopy?: string[]) => ipcRenderer.send("copyToData", modPathsToCopy),
copyToDataAsSymbolicLink: (modPathsToCopy?: string[]) =>
ipcRenderer.send("copyToDataAsSymbolicLink", modPathsToCopy),
cleanData: () => ipcRenderer.send("cleanData"),
cleanSymbolicLinksInData: () => ipcRenderer.send("cleanSymbolicLinksInData"),
getPackData: (packPath: string, table?: DBTable) => ipcRenderer.send("getPackData", packPath, table),
saveConfig: (appState: AppState) => ipcRenderer.send("saveConfig", appState),
readMods: (mods: Mod[], skipCollisionCheck = true) =>
Expand Down
Loading

0 comments on commit 269d8ab

Please sign in to comment.