Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/components/views/OnlineModView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
</button>
</div>
</div>
<div class="input-group">
<input type="date" v-model="endDate" />
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -97,6 +100,15 @@ export default class OnlineModView extends Vue {
sortingStyleModel = SortingStyle.DEFAULT;
thunderstoreSearchFilter = "";

get endDate(): string {
return this.$store.state.profile.endDate;
}

set endDate(value: string) {
this.$store.commit("profile/setEndDate", value);
this.$store.dispatch("tsMods/updateMods");
}

get localModList(): ManifestV2[] {
return this.$store.state.profile.modList;
}
Expand Down
25 changes: 16 additions & 9 deletions src/model/ThunderstoreMod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export default class ThunderstoreMod extends ThunderstoreVersion implements Reac
private rating: number = 0;
private owner: string = '';
private packageUrl: string = '';
private dateCreated: string = '';
private dateUpdated: string = '';
private uuid4: string = '';
private pinned: boolean = false;
Expand Down Expand Up @@ -48,6 +47,22 @@ export default class ThunderstoreMod extends ThunderstoreVersion implements Reac
return mod;
}

// Imitate the order where mods are returned from Thunderstore package listing API.
public static defaultOrderComparer(a: ThunderstoreMod, b: ThunderstoreMod): number {
// Pinned mods first.
if (a.isPinned() !== b.isPinned()) {
return a.isPinned() ? -1 : 1;
}

// Deprecated mods last.
if (a.isDeprecated() !== b.isDeprecated()) {
return a.isDeprecated() ? 1 : -1;
}

// Sort mods with same boolean flags by update date.
return a.getDateUpdated() >= b.getDateUpdated() ? -1 : 1;
}

public fromReactive(reactive: any): ThunderstoreMod {
this.setName(reactive.name);
this.setFullName(reactive.fullName);
Expand Down Expand Up @@ -104,14 +119,6 @@ export default class ThunderstoreMod extends ThunderstoreVersion implements Reac
this.packageUrl = url;
}

public getDateCreated(): string {
return this.dateCreated;
}

public setDateCreated(date: string) {
this.dateCreated = date;
}

public getDateUpdated(): string {
return this.dateUpdated;
}
Expand Down
11 changes: 11 additions & 0 deletions src/model/ThunderstoreVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class ThunderstoreVersion implements ReactiveObjectConverterInter
private enabled: boolean = true;
private downloads: number = 0;
private downloadUrl: string = '';
private dateCreated: string = '';

public make(version: any): ThunderstoreVersion {
this.setName(version.name);
Expand All @@ -23,6 +24,7 @@ export default class ThunderstoreVersion implements ReactiveObjectConverterInter
this.setIcon(version.icon);
this.setDownloadCount(version.downloads);
this.setDownloadUrl(version.download_url);
this.setDateCreated(version.date_created);
return this;
}

Expand All @@ -36,6 +38,7 @@ export default class ThunderstoreVersion implements ReactiveObjectConverterInter
this.enabled = reactive.enabled;
this.setDownloadCount(reactive.downloadCount);
this.setDownloadUrl(reactive.downloadUrl);
this.setDateCreated(reactive.dateCreated);
return this;
}

Expand Down Expand Up @@ -114,4 +117,12 @@ export default class ThunderstoreVersion implements ReactiveObjectConverterInter
public setDownloadUrl(url: string) {
this.downloadUrl = url;
}

public getDateCreated(): string {
return this.dateCreated;
}

public setDateCreated(date: string) {
this.dateCreated = date;
}
}
10 changes: 10 additions & 0 deletions src/store/modules/ProfileModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface State {
disabledPosition?: SortLocalDisabledMods;
searchQuery: string;
dismissedUpdateAll: boolean;
endDate: string|undefined;
}

/**
Expand All @@ -43,6 +44,7 @@ export default {
disabledPosition: undefined,
searchQuery: '',
dismissedUpdateAll: false,
endDate: undefined,
}),

getters: <GetterTree<State, RootState>>{
Expand Down Expand Up @@ -116,6 +118,10 @@ export default {
&& state.disabledPosition === SortLocalDisabledMods.CUSTOM
&& state.searchQuery.length === 0;
},

endDate(state) {
return state.endDate;
}
},

mutations: {
Expand Down Expand Up @@ -161,6 +167,10 @@ export default {
setSearchQuery(state: State, value: string) {
state.searchQuery = value.trim();
},

setEndDate(state: State, value: string) {
state.endDate = value.trim();
},
},

actions: <ActionTree<State, RootState>>{
Expand Down
17 changes: 15 additions & 2 deletions src/store/modules/TsModsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ThunderstoreMod from '../../model/ThunderstoreMod';
import ConnectionProvider from '../../providers/generic/connection/ConnectionProvider';
import * as PackageDb from '../../r2mm/manager/PackageDexieStore';
import { Deprecations } from '../../utils/Deprecations';
import { filterModVersions } from '../../utils/ManagerUtils';
import { filterModVersions, filterModVersionsByDate } from '../../utils/ManagerUtils';

interface CachedMod {
tsMod: ThunderstoreMod | undefined;
Expand Down Expand Up @@ -162,13 +162,26 @@ export const TsModsModule = {
commit('setExclusions', exclusions);
},

async updateMods({commit, rootState}) {
async updateMods({commit, rootGetters, rootState}) {
const modList = await PackageDb.getPackagesAsThunderstoreMods(rootState.activeGame.internalFolderName);

if (rootState.activeGameModLoaderTarget) {
filterModVersions(modList, rootState.activeGameModLoaderTarget);
}

const startDate = undefined; // Not supported initially.
const endDate = rootGetters['profile/endDate'];
console.time("Filter execution time");
filterModVersionsByDate(modList, startDate, endDate);
console.timeEnd("Filter execution time");

let totalVersions = 0;
for (let i = 0; i < modList.length; i++) {
totalVersions += modList[i].getVersions().length;
}
console.log(`${totalVersions} versions remaining`);
console.log('----------------------------------------------');

const updated = await PackageDb.getLastPackageListUpdateTime(rootState.activeGame.internalFolderName);
commit('setMods', modList);
commit('setModsLastUpdated', updated);
Expand Down
40 changes: 40 additions & 0 deletions src/utils/ManagerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,52 @@ export const filterModVersions = (modList: ThunderstoreMod[], dependencyString:

if (filtered.length) {
modList[target].setVersions(filtered);
modList[target].setDateUpdated(filtered[filtered.length - 1].getDateCreated());
} else {
modList.splice(target, 1);
}
}
}

/**
* Filters out versions that are created outside the time bubble defined by the
* date arguments. If all versions are filtered out, removes the whole package.
*
* MUTATES THE LIST IN PLACE. Theoretically there's no upper limit to how large
* the modList can be, so avoid making a copy unnecessarily.
*
* This can be used to improve the changes of creating a mod profile compatible
* with a specific version of a game. If a game update breaks all mods, user
* can choose to stay on an older game version and filter out newer mods, or to
* use the new game version and filter out mods that haven't been updated yet.
*/
export const filterModVersionsByDate = (modList: ThunderstoreMod[], startDate?: string, endDate?: string) => {
if (startDate && endDate && startDate > endDate) {
throw new Error("Argument startDate can't be greater than endDate")
}
if (!startDate && !endDate) {
return;
}

for (let i = modList.length - 1; i >= 0; i--) {
const filtered = modList[i].getVersions().filter(
(v) => (!startDate || v.getDateCreated() >= startDate) && (!endDate || v.getDateCreated() <= endDate)
);

if (filtered.length) {
modList[i].setVersions(filtered);
modList[i].setDateUpdated(filtered[filtered.length - 1].getDateCreated());
} else {
modList.splice(i, 1);
}
}

// TODO(?): this takes around 20ms with current LC mod list. It's only needed when
// the default ordering is used so it could be done conditionally, but that would
// require refactoring the sorting mode out of the OnlineModView component.
modList.sort(ThunderstoreMod.defaultOrderComparer);
}

/**
* Return default game selection needed to skip the game selection screen.
*/
Expand Down
71 changes: 71 additions & 0 deletions test/jest/__tests__/classes/ThunderstoreMod.ts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import ThunderstoreMod from "src/model/ThunderstoreMod";

const getMockPackage = (pinned: boolean, deprecated: boolean, updated: string) => {
const mod = new ThunderstoreMod();
mod.setPinnedStatus(pinned);
mod.setDeprecatedStatus(deprecated);
mod.setDateUpdated(updated);
return mod;
};

// Fisher-Yates shuffle algorithm. Just calling array.sort(Math.random() - 0.5)
// seemed to result in slightly less random distribution of items.
const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};

describe("ThunderstoreMod.defaultOrderComparer", () => {
it("Sorts mods list to match Thunderstore API's ordering", () => {
const mods = [
getMockPackage(true, true, "2024-09-01"),
getMockPackage(true, true, "2024-08-01"),
getMockPackage(true, false, "2024-09-01"),
getMockPackage(true, false, "2024-08-01"),
getMockPackage(false, true, "2024-09-01"),
getMockPackage(false, true, "2024-08-01"),
getMockPackage(false, false, "2024-09-01"),
getMockPackage(false, false, "2024-08-01"),
]

for (let i = 0; i < 5; i++) {
shuffleArray(mods);

mods.sort(ThunderstoreMod.defaultOrderComparer);

// Pinned, non-deprecated by date
expect(mods[0].isPinned()).toStrictEqual(true);
expect(mods[0].isDeprecated()).toStrictEqual(false);
expect(mods[0].getDateUpdated()).toStrictEqual("2024-09-01");
expect(mods[1].isPinned()).toStrictEqual(true);
expect(mods[1].isDeprecated()).toStrictEqual(false);
expect(mods[1].getDateUpdated()).toStrictEqual("2024-08-01");

// Pinned, deprecated by date
expect(mods[2].isPinned()).toStrictEqual(true);
expect(mods[2].isDeprecated()).toStrictEqual(true);
expect(mods[2].getDateUpdated()).toStrictEqual("2024-09-01");
expect(mods[3].isPinned()).toStrictEqual(true);
expect(mods[3].isDeprecated()).toStrictEqual(true);
expect(mods[3].getDateUpdated()).toStrictEqual("2024-08-01");

// Non-pinned, non-deprecated by date
expect(mods[4].isPinned()).toStrictEqual(false);
expect(mods[4].isDeprecated()).toStrictEqual(false);
expect(mods[4].getDateUpdated()).toStrictEqual("2024-09-01");
expect(mods[5].isPinned()).toStrictEqual(false);
expect(mods[5].isDeprecated()).toStrictEqual(false);
expect(mods[5].getDateUpdated()).toStrictEqual("2024-08-01");

// Non-pinned, deprecated by date
expect(mods[6].isPinned()).toStrictEqual(false);
expect(mods[6].isDeprecated()).toStrictEqual(true);
expect(mods[6].getDateUpdated()).toStrictEqual("2024-09-01");
expect(mods[7].isPinned()).toStrictEqual(false);
expect(mods[7].isDeprecated()).toStrictEqual(true);
expect(mods[7].getDateUpdated()).toStrictEqual("2024-08-01");
}
});
});
Loading
Loading