Skip to content

Commit

Permalink
feat: added the optional version property to the mod configuration. c…
Browse files Browse the repository at this point in the history
…loses #436
  • Loading branch information
meza committed Dec 21, 2023
1 parent 891d371 commit 938bb8e
Show file tree
Hide file tree
Showing 23 changed files with 388 additions and 82 deletions.
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,50 @@ Adding a mod also downloads the corresponding jar file.
You can optionally specify the `--allow-version-fallback` flag to allow the tool to attempt to download the mod for
previous versions of Minecraft if the mod doesn't support the current version.

#### Installing specific versions

If you want to install a specific version of a mod, you can use the `--version` flag to specify the version.

`mmm add modrinth FOIvwGKz --version 1.3.1`

The version of the mod has to exist for the given Minecraft version.

:warning: **Modrinth and Curseforge handle versions differently**

##### Modrinth

When using Modrinth, you need to specify the version number as it is listed on the website.

![](/doc/images/versions-modrinth.png)

In the example above, `1.3.1` and `1.2.2` are the version numbers. Notice how in the filename itself it says `v1.3.1` but
the version number communicated by Modrinth is actually `1.3.1`. You always want to use the version number that's
communicated by Modrinth.

##### Curseforge

Curseforge on the other hand has no core concept of the individual mods' versions so we have to rely on the actual file names.


Once you find the mod you want to install, you need to click on the "Files" tab and then find the version you want to
install.

![](/doc/images/versions-curseforge-1.png)

You then need to click on the specific version you want to install and copy the file name.

![](/doc/images/versions-curseforge-2.png)

> This will change in the future as soon as Curseforge adds support for proper versioning.


#### Command Line Arguments

| Short | Long | Description | Value |
|-------|--------------------------|-----------------------------------|--------------------------------------------------------------|
| -f | --allow-version-fallback | Whether to allow version fallback | No value needed. <br/>When it is supplied, `true` is assumed |
| Short | Long | Description | Value |
|-------|--------------------------|-----------------------------------|------------------------------------------------------------------------------|
| -f | --allow-version-fallback | Whether to allow version fallback | No value needed. <br/>When it is supplied, `true` is assumed |
| -v | --version | The version of the mod to add | A valid version string for Modrinth or the version's filename for Curseforge |

#### Platforms

Expand Down Expand Up @@ -275,7 +314,8 @@ use.
The install command works off of the `modlist-lock.json` file which contains the exact version information for any given
mod.

If a `modlist-lock.json` does not exist, the install command will download the latest version of every mod. This is a
If a `modlist-lock.json` does not exist, the install command will download the latest version of every mod unless you've set
a specific `version` with the [add command](#installing-specific-versions). This is a
limitation of the Minecraft modding ecosystem and the lack of enforced versioning.

> If you are in charge of Modrinth or Curseforge, please mandate the use of semver!
Expand All @@ -293,7 +333,8 @@ everyone has the exact same versions of everything.
`mmm update` or `mmm u`

This will try and find newer versions of every mod defined in the `modlist.json` file that matches the given game
version and loader. If a new mod is found, it will be downloaded and the old one will be removed. If the download fails,
version, loader and doesn't have a fixed `version` configuration.
If a new mod is found, it will be downloaded and the old one will be removed. If the download fails,
the old one will be kept.

You would run this command when you want to make sure that you're using the newest versions of the mods.
Expand Down Expand Up @@ -445,7 +486,13 @@ This is how it looks like if you followed the examples in the [`add`](#add) sect
{
"type": "modrinth",
"id": "AANobbMI",
"name": "Sodium"
"name": "Sodium",
"version": "0.5.5"
},
{
"type": "modrinth",
"id": "YL57xq9U",
"name": "Iris Shaders"
}
]
}
Expand Down Expand Up @@ -522,6 +569,14 @@ they forget to list the supported Minecraft versions correctly.

This setting will be overridable on an individual mod basis in the next release. Currently, it's a global setting.

#### version _optional_

For every mod you can specify a version. This is useful if you want to install a specific version of a mod and want to
keep it that way regardless of any updates to the mod.

There are subtle differences between how this works for Modrinth and Curseforge. To learn more about this, please read
the [installing specific versions](#installing-specific-versions) section of the [add](#add) command.

### Ignore File

Ignoring files works pretty much the same way as it does with [.gitignore](https://git-scm.com/docs/gitignore).
Expand Down
Binary file added doc/images/versions-curseforge-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/versions-curseforge-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/versions-modrinth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion src/__snapshots__/mmm.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,9 @@ Command {
"_enablePositionalOptions": false,
"_events": {
"option:allow-version-fallback": [Function],
"option:version": [Function],
},
"_eventsCount": 1,
"_eventsCount": 2,
"_executableDir": null,
"_executableFile": null,
"_executableHandler": false,
Expand Down Expand Up @@ -321,6 +322,26 @@ Command {
"args": [],
"commands": [],
"options": [
Option {
"argChoices": undefined,
"conflictsWith": [],
"defaultValue": undefined,
"defaultValueDescription": undefined,
"description": "The version of the mod to add. If not specified, the latest version will be used",
"envVar": undefined,
"flags": "-v, --version <version>",
"hidden": false,
"implied": undefined,
"long": "--version",
"mandatory": false,
"negate": false,
"optional": false,
"parseArg": undefined,
"presetArg": undefined,
"required": true,
"short": "-v",
"variadic": false,
},
Option {
"argChoices": undefined,
"conflictsWith": [],
Expand Down
17 changes: 14 additions & 3 deletions src/actions/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ describe('The add module', async () => {
const randomPlatform = Platform.CURSEFORGE;
const randomModId = chance.word();
const randomAllowVersion = chance.bool();
let randomVersion: string | undefined;

if (chance.bool()) {
randomVersion = chance.word();
}

vi.mocked(fetchModDetails).mockReset();
vi.mocked(fetchModDetails).mockRejectedValueOnce(new NoRemoteFileFound(randomModId, randomPlatform));
vi.mocked(fetchModDetails).mockRejectedValueOnce(new Error('test-error'));
Expand All @@ -203,14 +209,19 @@ describe('The add module', async () => {
platform: Platform.MODRINTH
});

await expect(add(randomPlatform, randomModId, { config: 'config.json', allowVersionFallback: randomAllowVersion }, logger)).rejects.toThrow(new Error('process.exit'));
await expect(add(randomPlatform, randomModId, { config: 'config.json', allowVersionFallback: randomAllowVersion, version: randomVersion }, logger)).rejects.toThrow(new Error('process.exit'));

expect(fetchModDetails).toHaveBeenNthCalledWith(2, Platform.MODRINTH, 'another-mod-id',
randomConfiguration.expected.defaultAllowedReleaseTypes,
randomConfiguration.expected.gameVersion,
randomConfiguration.expected.loader,
randomAllowVersion);
expect(noRemoteFileFound).toHaveBeenCalledWith(randomModId, randomPlatform, randomConfiguration.expected, logger, { config: 'config.json', allowVersionFallback: randomAllowVersion });
randomAllowVersion,
randomVersion);
expect(noRemoteFileFound).toHaveBeenCalledWith(randomModId, randomPlatform, randomConfiguration.expected, logger, {
config: 'config.json',
allowVersionFallback: randomAllowVersion,
version: randomVersion
});
expect(logger.error).toHaveBeenCalledWith('test-error', 2);
});

Expand Down
5 changes: 4 additions & 1 deletion src/actions/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { DownloadFailedException } from '../errors/DownloadFailedException.js';

export interface AddOptions extends DefaultOptions {
allowVersionFallback?: boolean;
version?: string | undefined;
}

const handleUnknownPlatformException = async (error: UnknownPlatformException, id: string, options: AddOptions, logger: Logger) => {
Expand Down Expand Up @@ -68,7 +69,9 @@ export const add = async (platform: Platform, id: string, options: AddOptions, l
configuration.defaultAllowedReleaseTypes,
configuration.gameVersion,
configuration.loader,
!!options.allowVersionFallback);
!!options.allowVersionFallback,
options.version
);

await downloadFile(modData.downloadUrl, path.resolve(configuration.modsFolder, modData.fileName));

Expand Down
21 changes: 19 additions & 2 deletions src/actions/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,34 @@ describe('The install module', () => {
verifyBasics();
});

it<LocalTestContext>('Sets the appropriate debug messages', async ({ options, logger }) => {
it<LocalTestContext>('Sets the appropriate debug messages for latest', async ({ options, logger }) => {
const { randomInstalledMod, randomInstallation, randomConfiguration } = setupOneInstalledMod();

vi.mocked(ensureConfiguration).mockResolvedValueOnce(randomConfiguration);
vi.mocked(readLockFile).mockResolvedValueOnce([randomInstallation]);
vi.mocked(getHash).mockResolvedValueOnce(randomInstallation.hash);

options.debug = true;
randomInstalledMod.version = undefined;
await install(options, logger);

expect(logger.debug).toHaveBeenCalledWith(`Checking ${randomInstalledMod.name} for ${randomInstalledMod.type}`);
expect(logger.debug).toHaveBeenCalledWith(`Checking ${randomInstalledMod.name}@latest for ${randomInstalledMod.type}`);

});

it<LocalTestContext>('Sets the appropriate debug messages for specific version', async ({ options, logger }) => {
const { randomInstalledMod, randomInstallation, randomConfiguration } = setupOneInstalledMod();

randomInstalledMod.version = '1.1.0';

vi.mocked(ensureConfiguration).mockResolvedValueOnce(randomConfiguration);
vi.mocked(readLockFile).mockResolvedValueOnce([randomInstallation]);
vi.mocked(getHash).mockResolvedValueOnce(randomInstallation.hash);

options.debug = true;
await install(options, logger);

expect(logger.debug).toHaveBeenCalledWith(`Checking ${randomInstalledMod.name}@1.1.0 for ${randomInstalledMod.type}`);

});

Expand Down
6 changes: 4 additions & 2 deletions src/actions/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export const install = async (options: DefaultOptions, logger: Logger) => {
const mods = configuration.mods;

const processMod = async (mod: Mod, index: number) => {
const canonVersion = mod.version || 'latest';
try {
logger.debug(`Checking ${mod.name} for ${mod.type}`);
logger.debug(`Checking ${mod.name}@${canonVersion} for ${mod.type}`);

if (hasInstallation(mod, installations)) {
const installedModIndex = getInstallation(mod, installedMods);
Expand Down Expand Up @@ -81,7 +82,8 @@ export const install = async (options: DefaultOptions, logger: Logger) => {
mod.allowedReleaseTypes || configuration.defaultAllowedReleaseTypes,
configuration.gameVersion,
configuration.loader,
!!mod.allowVersionFallback
!!mod.allowVersionFallback,
mod.version
);

mods[index].name = modData.name;
Expand Down
3 changes: 2 additions & 1 deletion src/actions/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const update = async (options: DefaultOptions, logger: Logger) => {
mod.allowedReleaseTypes || configuration.defaultAllowedReleaseTypes,
configuration.gameVersion,
configuration.loader,
!!mod.allowVersionFallback
!!mod.allowVersionFallback,
mod.version
);
mods[index].name = modData.name;

Expand Down
3 changes: 2 additions & 1 deletion src/lib/modlist.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export interface Mod {
id: string,
allowedReleaseTypes?: ReleaseType[]
name: string,
allowVersionFallback?: boolean
allowVersionFallback?: boolean,
version?: string | undefined
}

export interface ModsJson {
Expand Down
1 change: 1 addition & 0 deletions src/mmm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ commands.push(
program.command('add')
.argument('<type>', 'curseforge or modrinth')
.argument('<id>', 'Curseforge or Modrinth Project Id')
.option('-v, --version <version>', 'The version of the mod to add. If not specified, the latest version will be used')
.option('-f, --allow-version-fallback', 'Should we try to download the mod for previous Minecraft versions if they do not exists for your Minecraft Version?', false)
.action(async (type: Platform, id: string, _options, cmd) => {
await add(type, id, cmd.optsWithGlobals(), logger);
Expand Down
79 changes: 76 additions & 3 deletions src/repositories/curseforge/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ describe('The Curseforge repository', () => {
});
});

describe('when version fallback is allowed and the available version is one lower version', () => {
describe('when game version fallback is allowed and the available game version is one lower game version', () => {
beforeEach<RepositoryTestContext>((context) => {
context.allowFallback = true;
});
Expand Down Expand Up @@ -331,7 +331,7 @@ describe('The Curseforge repository', () => {
});
});

describe('when version fallback is allowed and the available version is the previous major version', () => {
describe('when game version fallback is allowed and the available game version is the previous major game version', () => {
beforeEach<RepositoryTestContext>((context) => {
context.allowFallback = true;
});
Expand Down Expand Up @@ -412,7 +412,7 @@ describe('The Curseforge repository', () => {

});

it<RepositoryTestContext>('returns the most recent file for a given version', async (context) => {
it<RepositoryTestContext>('returns the most recent file for a given game version', async (context) => {
const randomName = chance.word();
const randomFile1 = generateCurseforgeModFile({
isAvailable: true,
Expand Down Expand Up @@ -508,4 +508,77 @@ describe('The Curseforge repository', () => {

});

describe('when a specific mod version is requested', () => {
it<RepositoryTestContext>('returns the correct version', async (context) => {
const randomName = chance.word();
const randomFile1 = generateCurseforgeModFile({
isAvailable: true,
fileStatus: releasedStatus,
fileDate: '2019-08-24T14:15:22Z',
releaseType: Release.RELEASE,
sortableGameVersions: [{
gameVersionName: context.loader,
gameVersion: context.gameVersion
}],
fileName: '1.0.0'
});
const randomFile2 = generateCurseforgeModFile({
isAvailable: true,
fileStatus: releasedStatus,
fileDate: '2020-08-24T14:15:22Z',
releaseType: Release.RELEASE,
sortableGameVersions: [{
gameVersionName: context.loader,
gameVersion: context.gameVersion
}],
fileName: '1.1.0'
});
const randomFile3 = generateCurseforgeModFile({
isAvailable: true,
fileStatus: releasedStatus,
fileDate: '2018-08-24T14:15:22Z',
releaseType: Release.RELEASE,
sortableGameVersions: [{
gameVersionName: context.loader,
gameVersion: context.gameVersion
}],
fileName: '1.2.0'
});
const randomFile4 = generateCurseforgeModFile({
isAvailable: true,
fileStatus: releasedStatus,
fileDate: '2018-08-24T14:15:22Z',
releaseType: Release.RELEASE,
sortableGameVersions: [{
gameVersionName: context.loader,
gameVersion: context.gameVersion
}],
fileName: '1.3.0'
});
assumeSuccessfulModFetch(randomName, [
randomFile1.generated,
randomFile2.generated,
randomFile3.generated,
randomFile4.generated
]);

const actual = await getMod(
context.id,
[ReleaseType.RELEASE],
context.gameVersion,
context.loader,
context.allowFallback,
'1.2.0'
);

expect(actual).toEqual({
name: randomName,
fileName: randomFile3.generated.fileName,
releaseDate: randomFile3.generated.fileDate,
hash: randomFile3.generated.hashes.find((hash) => hash.algo === HashFunctions.sha1)?.value,
downloadUrl: randomFile3.generated.downloadUrl
});
});
});

});
Loading

0 comments on commit 938bb8e

Please sign in to comment.