├── .eslintignore
├── .eslintrc.js
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── SECURITY.md
├── dependabot.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── pre-merge.yml
│ ├── rebase.yml
│ ├── release.yml
│ └── version.yml
├── .gitignore
├── .mocharc.json
├── .nycrc
├── .prettierignore
├── .prettierrc.json
├── .stylelintrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── build
└── installerHeaderIcon.ico
├── dev-app-update.yml.example
├── lint-staged.config.js
├── package-lock.json
├── package.json
├── public
├── images
│ ├── default-background.png
│ └── logos
│ │ ├── discord.svg
│ │ ├── faq.svg
│ │ ├── new-player-guide.svg
│ │ ├── patreon.svg
│ │ ├── reddit.svg
│ │ ├── twitch.svg
│ │ ├── website.svg
│ │ ├── wildlander-full-light.svg
│ │ ├── wildlander-icon-dark.png
│ │ ├── wildlander-icon-light.png
│ │ └── youtube.svg
└── index.html
├── src
├── __tests__
│ └── unit
│ │ ├── controllers
│ │ └── graphics.controller.test.ts
│ │ ├── services
│ │ ├── blacklist.service.test.ts
│ │ ├── config.service.test.ts
│ │ ├── game.service.test.ts
│ │ ├── graphics.service.test.ts
│ │ ├── migration.service.test.ts
│ │ ├── modOrganizer.service.test.ts
│ │ ├── profile.service.test.ts
│ │ ├── startup.service.test.ts
│ │ ├── system.service.test.ts
│ │ └── wabbajack.service.test.ts
│ │ └── setup
│ │ └── global-setup.ts
├── additional-instructions.json
├── assets
│ ├── fonts
│ │ ├── averta-black.ttf
│ │ ├── averta-blackitalic.ttf
│ │ ├── averta-bold.ttf
│ │ ├── averta-bolditalic.ttf
│ │ ├── averta-extrabold.ttf
│ │ ├── averta-extrabolditalic.ttf
│ │ ├── averta-extrathin.ttf
│ │ ├── averta-extrathinitalic.ttf
│ │ ├── averta-light.ttf
│ │ ├── averta-lightitalic.ttf
│ │ ├── averta-regular.ttf
│ │ ├── averta-regularitalic.ttf
│ │ ├── averta-semibold.ttf
│ │ ├── averta-semibolditalic.ttf
│ │ ├── averta-thin.ttf
│ │ └── averta-thinitalic.ttf
│ ├── icons
│ │ └── MaterialIcons-Regular.woff2
│ ├── scss
│ │ ├── index.scss
│ │ ├── layout.scss
│ │ ├── popper.scss
│ │ ├── settings
│ │ │ ├── colours.scss
│ │ │ ├── font-face.scss
│ │ │ ├── font.scss
│ │ │ └── sizes.scss
│ │ └── utility.scss
│ └── tools
│ │ └── QRes.exe
├── main.ts
├── main
│ ├── application.ts
│ ├── controllers
│ │ ├── config
│ │ │ ├── config.controller.ts
│ │ │ └── config.events.ts
│ │ ├── dialog
│ │ │ ├── dialog.controller.ts
│ │ │ └── dialog.events.ts
│ │ ├── enb
│ │ │ ├── enb.controller.ts
│ │ │ └── enb.events.ts
│ │ ├── graphics
│ │ │ ├── graphics.controller.ts
│ │ │ └── graphics.events.ts
│ │ ├── launcher
│ │ │ ├── launcher.controller.ts
│ │ │ └── launcher.events.ts
│ │ ├── modOrganizer
│ │ │ ├── modOrganizer.controller.ts
│ │ │ └── modOrganizer.events.ts
│ │ ├── modpack
│ │ │ ├── modpack.controller.ts
│ │ │ └── mopack.events.ts
│ │ ├── profile
│ │ │ ├── profile.controller.ts
│ │ │ └── profile.events.ts
│ │ ├── resolution
│ │ │ ├── resolution.controller.ts
│ │ │ └── resolution.events.ts
│ │ ├── system
│ │ │ ├── system.controller.ts
│ │ │ └── system.events.ts
│ │ ├── update
│ │ │ └── update.events.ts
│ │ ├── wabbajack
│ │ │ ├── wabbajack.controller.ts
│ │ │ └── wabbajack.events.ts
│ │ └── window
│ │ │ ├── window.controller.ts
│ │ │ └── window.events.ts
│ ├── decorators
│ │ └── controller.decorator.ts
│ ├── logger.ts
│ ├── preload.ts
│ └── services
│ │ ├── blacklist.service.ts
│ │ ├── config.service.ts
│ │ ├── enb.service.ts
│ │ ├── error.service.ts
│ │ ├── game.service.ts
│ │ ├── graphics.service.ts
│ │ ├── instruction.service.ts
│ │ ├── launcher.service.ts
│ │ ├── migration.service.ts
│ │ ├── modOrganizer.service.ts
│ │ ├── modpack.service.ts
│ │ ├── profile.service.ts
│ │ ├── resolution.service.ts
│ │ ├── startup.service.ts
│ │ ├── system.service.ts
│ │ ├── update.service.ts
│ │ ├── wabbajack.service.ts
│ │ └── window.service.ts
├── modpack.json
├── renderer
│ ├── App.vue
│ ├── components
│ │ ├── AppDropdownFileSelect.vue
│ │ ├── AppModal.vue
│ │ ├── AppPage.vue
│ │ ├── AppPageContent.vue
│ │ ├── BaseButton.vue
│ │ ├── BaseDropdown.vue
│ │ ├── BaseImage.vue
│ │ ├── BaseInput.vue
│ │ ├── BaseLabel.vue
│ │ ├── BaseLink.vue
│ │ ├── BaseList.vue
│ │ ├── Community.vue
│ │ ├── ENB.vue
│ │ ├── GraphicsSelection.vue
│ │ ├── ImageWithText.vue
│ │ ├── LauncherVersion.vue
│ │ ├── MO2RunningModal.vue
│ │ ├── ModDirectory.vue
│ │ ├── NavigationItem.vue
│ │ ├── News.vue
│ │ ├── Patrons.vue
│ │ ├── ProfileSelection.vue
│ │ ├── Resolution.vue
│ │ ├── TheHeader.vue
│ │ ├── TheNavigation.vue
│ │ └── TheTitleBar.vue
│ ├── index.ts
│ ├── router
│ │ └── index.ts
│ ├── services
│ │ ├── cache.service.ts
│ │ ├── event.service.ts
│ │ ├── ipc.service.ts
│ │ ├── message.service.ts
│ │ ├── modal.service.ts
│ │ ├── modpack.service.ts
│ │ ├── patreon.service.ts
│ │ ├── posts.service.ts
│ │ └── service-container.ts
│ └── views
│ │ ├── AutoUpdate.vue
│ │ ├── ModDirectory.vue
│ │ ├── ViewAdvanced.vue
│ │ ├── ViewCommunity.vue
│ │ └── ViewHome.vue
├── shared
│ ├── enums
│ │ └── userPreferenceKeys.ts
│ └── util
│ │ └── asyncFilter.ts
└── types
│ ├── ModOrganizer.ini.d.ts
│ ├── Resolution.d.ts
│ ├── additional-instructions.d.ts
│ ├── fetch-installed-software.d.ts
│ ├── modpack-metadata.d.ts
│ ├── shims-vue.d.ts
│ ├── vu3-popper.d.ts
│ └── wabbajack.d.ts
├── tsconfig.json
├── vue.config.js
└── wallaby.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | .eslintrc.js
2 | .github
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 |
4 | env: {
5 | node: true,
6 | },
7 |
8 | 'extends': [
9 | 'plugin:vue/vue3-essential',
10 | 'eslint:recommended',
11 | '@vue/typescript/recommended',
12 | '@vue/prettier',
13 | '@vue/prettier/@typescript-eslint'
14 | ],
15 |
16 | parserOptions: {
17 | parser: '@typescript-eslint/parser',
18 | },
19 |
20 | rules: {
21 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
23 | 'prettier/prettier': ['error', { 'endOfLine': 'auto' }]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Install dependencies
4 |
5 | ```
6 | npm install
7 | ```
8 |
9 | ## Running
10 |
11 | Start the application
12 | ```
13 | npm run start
14 | //or for automatic reloading of the main process with nodemon
15 | npm run start:dev
16 | ```
17 |
18 | ## Building
19 |
20 | The application will be built and published automatically when merged to the main branch.
21 | However, if you want to build locally then you can run
22 | ```
23 | npm run build
24 | ```
25 |
26 | ## Linting
27 |
28 | All files will be automatically linted when committing. However, if you want to manually lint you can run
29 |
30 | ```
31 | npm run lint
32 | ```
33 |
34 | ## Commit validation
35 |
36 | This repository uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All commits must follow this format.
37 |
38 | ## Architecture
39 |
40 | Key technology:
41 |
42 | - [Electron](https://www.electronjs.org/) - The core of the application is built using this.
43 | - [Node.js](https://nodejs.org/en/) - Language used to build the electron `main` process (the backend of the application).
44 | - [Vue 3](https://vuejs.org/) - Frontend framework
45 | - [Loopback Context](https://loopback.io/doc/en/lb4/Context.html) - Dependency injection and IoC container used in the `main` process to simplify setup
46 |
47 | ### Main process
48 |
49 | The code for the main process is mostly broken into controllers and services, along with the application bootstrapping.
50 |
51 | - `controllers` - [Handle](https://www.electronjs.org/docs/latest/api/ipc-main#ipcmainhandlechannel-listener) ipc events from the `renderer`. These can be registered with the `@controller` decorator and ipc handlers with `@handle`.
52 | - `services` - Responsible for the bulk of the logic. Controllers delegate to services to perform actual business logic.
53 | - `application.ts` - Bootstraps and initialise the application, including registering the controller and service artefacts.
54 |
55 | #### Example Controller
56 |
57 | The following will register a controller with 2 ipc handlers.
58 | It is recommended that the channel names are stored in a `const enum`.
59 | ```typescript
60 | // `main/controllers/example/example.controller.ts`
61 |
62 | import { controller, handle } from "@/main/decorators/controller.decorator";
63 | import { EXAMPLE_EVENTS } from "@/main/controllers/example/example.events";
64 | import { service } from "@loopback/core";
65 | import { ExampleService } from "@/main/services/example.service";
66 |
67 | @controller
68 | export class ExampleController {
69 | constructor(@service(ExampleService) private exampleService: ExampleService) {
70 | }
71 |
72 | @handle('something else')
73 | async somethingElse(...args) {
74 | await this.exampleService.doSomethingElse(...args);
75 | }
76 |
77 | @handle(EXAMPLE_EVENTS.DO_SOMETHING)
78 | async doSomething(...args) {
79 | await this.exampleService.doSomething(...args);
80 | }
81 | }
82 | ```
83 |
84 | ### Renderer process
85 |
86 | Only the main process can access node events. The renderer process cannot directly access node events. To access "
87 | backend" events, the renderer process must `invoke` events from the ipc service.
88 | Because of this, importing anything with any node code into the renderer process will cause errors.
89 | So, anything that must be shared between both must not contain node code. It is recommended that shared `const enum`s are put in their own file.
90 |
91 | #### Example ipc invoke
92 |
93 | ```typescript
94 |
95 | export default class Example extends Vue {
96 | private ipcService = injectStrict(SERVICE_BINDINGS.IPC_SERVICE);
97 |
98 | async created() {
99 | await this.ipcService.invoke(EXAMPLE_EVENTS.DO_SOMETHING)
100 | }
101 | }
102 |
103 | ```
104 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug, outstanding questions
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
22 | **Desktop (please complete the following information):**
23 | - OS: [e.g. Windows version]
24 | - Version [e.g. 22]
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: feature, outstanding questions
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 1.x.x | :white_check_mark: |
8 |
9 | ## Reporting a Vulnerability
10 |
11 | To report a vulnerability, create an issue in this repository and label it with "security".
12 | Or, visit the Discord.
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 | assignees:
13 | - "MattLish"
14 | labels:
15 | - "dependencies"
16 | reviewers:
17 | - "MattLish"
18 | allow:
19 | - dependency-type: "production"
20 |
21 | - package-ecosystem: "github-actions"
22 | directory: "/"
23 | schedule:
24 | # Check for updates to GitHub Actions every weekday
25 | interval: "weekly"
26 | assignees:
27 | - "MattLish"
28 | labels:
29 | - "dependencies"
30 | reviewers:
31 | - "MattLish"
32 | allow:
33 | - dependency-name: "*"
34 | dependency-type: "production"
35 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | schedule:
18 | # ┌───────────── minute (0 - 59)
19 | # │ ┌───────────── hour (0 - 23)
20 | # │ │ ┌───────────── day of the month (1 - 31)
21 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
22 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
23 | # │ │ │ │ │
24 | # │ │ │ │ │
25 | # │ │ │ │ │
26 | # * * * * *
27 | - cron: '30 1 * * 0'
28 |
29 |
30 | jobs:
31 | analyze:
32 | name: Analyze
33 | runs-on: ubuntu-latest
34 | permissions:
35 | actions: read
36 | contents: read
37 | security-events: write
38 |
39 | strategy:
40 | fail-fast: false
41 | matrix:
42 | language: [ 'javascript' ]
43 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
44 | # Learn more:
45 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
46 |
47 | steps:
48 | - name: Checkout repository
49 | uses: actions/checkout@v3
50 |
51 | # Initializes the CodeQL tools for scanning.
52 | - name: Initialize CodeQL
53 | uses: github/codeql-action/init@v2
54 | with:
55 | languages: ${{ matrix.language }}
56 | # If you wish to specify custom queries, you can do so here or in a config file.
57 | # By default, queries listed here will override any specified in a config file.
58 | # Prefix the list here with "+" to use these queries and those in the config file.
59 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
60 |
61 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
62 | # If this step fails, then you should remove it and run the build manually (see below)
63 | - name: Autobuild
64 | uses: github/codeql-action/autobuild@v2
65 |
66 | # ℹ️ Command-line programs to run using the OS shell.
67 | # 📚 https://git.io/JvXDl
68 |
69 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
70 | # and modify them (or add more) to build your code if your project
71 | # uses a compiled language
72 |
73 | #- run: |
74 | # make bootstrap
75 | # make release
76 |
77 | - name: Perform CodeQL Analysis
78 | uses: github/codeql-action/analyze@v2
79 |
--------------------------------------------------------------------------------
/.github/workflows/pre-merge.yml:
--------------------------------------------------------------------------------
1 | name: Pre-merge
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'release-temp/**'
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: '16'
22 |
23 | - run: npm ci --legacy-peer-deps
24 | - run: npm run lint -- --no-fix
25 |
26 | test:
27 | runs-on: ubuntu-latest
28 |
29 | steps:
30 | - uses: actions/checkout@v3
31 |
32 | - name: Use Node.js ${{ matrix.node-version }}
33 | uses: actions/setup-node@v3
34 | with:
35 | node-version: '16'
36 |
37 | - run: npm ci --legacy-peer-deps
38 | - run: npm run test:coverage
39 |
40 | # Validate that commits are following the conventional commit format
41 | commit-validate:
42 | # Don't run for dependabot updates
43 | if: contains(github.head_ref, 'dependabot/') == false
44 | runs-on: ubuntu-latest
45 | steps:
46 | - uses: actions/checkout@v3
47 | with:
48 | fetch-depth: 0
49 |
50 | - name: Use Node.js ${{ matrix.node-version }}
51 | uses: actions/setup-node@v3
52 | with:
53 | node-version: '16'
54 |
55 | - name: install dependencies
56 | run: npm ci --legacy-peer-deps
57 |
58 | - name: lint commits
59 | run: npx commitlint --from origin/main
60 |
61 | version-dry-run:
62 | runs-on: ubuntu-latest
63 | steps:
64 | - uses: actions/checkout@v3
65 |
66 | - name: Use Node.js
67 | uses: actions/setup-node@v3
68 | with:
69 | node-version: '16'
70 |
71 | - name: bump version dry run
72 | run: npx standard-version --dry-run
73 |
--------------------------------------------------------------------------------
/.github/workflows/rebase.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Rebase
2 | on:
3 | issue_comment:
4 | types: [ created ]
5 | jobs:
6 | rebase:
7 | name: Rebase
8 | if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout the latest code
12 | uses: actions/checkout@v3
13 | with:
14 | token: ${{ secrets.ACCESS_TOKEN }}
15 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
16 | - name: Automatic Rebase
17 | uses: cirrus-actions/rebase@1.7
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
20 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_run:
5 | workflows: [ "Version" ]
6 | types:
7 | - completed
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | create-release:
17 | runs-on: windows-latest
18 |
19 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 |
24 | - name: Use Node.js
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: '16'
28 |
29 | - name: Install dependencies
30 | run: npm ci --legacy-peer-deps
31 |
32 | # When testing or working with forks, electron builder will try and publish to the main repo, not the fork.
33 | # This allows it to publish to the forked repo for testing
34 | - name: Replace package.json repository
35 | shell: bash
36 | run: |
37 | contents="$(jq --arg repo $GITHUB_SERVER_URL/$GITHUB_REPOSITORY '.repository = $repo' package.json)"
38 | echo $contents > package.json
39 |
40 | - name: Build app
41 | run: npm run build -- --publish always
42 | env:
43 | CI: true
44 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/version.yml:
--------------------------------------------------------------------------------
1 | name: Version
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | # Allows you to run this workflow manually from the Actions tab
9 | workflow_dispatch:
10 |
11 | jobs:
12 | version-bump:
13 | runs-on: ubuntu-latest
14 | # Prevent the versioning getting stuck in a loop when it pushes back to the main branch
15 | if: "!contains(toJSON(github.event.commits.*.message), 'chore(release)')"
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | token: ${{ secrets.ACCESS_TOKEN }}
21 |
22 | - uses: fregante/setup-git-user@v1
23 |
24 | - name: Use Node.js
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: '16'
28 |
29 | - name: bump version
30 | run: npx standard-version --no-verify
31 |
32 | # Inspired by https://github.com/CasperWA/push-protected but unable to get it working with this repository.
33 | # Instead, we just wait a set amount of time and hope the status checks complete.
34 | # Eventually this will need revisiting to ensure status checks pass before continuing
35 | - name: publish changes
36 | run: |
37 | TEMP_BRANCH=release-temp/$(cat /proc/sys/kernel/random/uuid)
38 | git checkout -b $TEMP_BRANCH
39 | git push -f origin $TEMP_BRANCH
40 |
41 | sleep 200
42 |
43 | git checkout main
44 | git reset --hard $TEMP_BRANCH
45 | git push origin main
46 | git push --tags
47 | git push -d origin $TEMP_BRANCH
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /coverage
5 | .nyc_output
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | pnpm-debug.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | dev-app-update.yml
27 |
28 | # Mock directory to replicate %APPDATA%
29 | local
30 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "recursive": true,
3 | "require": [
4 | "source-map-support/register",
5 | "tsconfig-paths/register"
6 | ],
7 | "file": [
8 | "./dist/__tests__/unit/setup/global-setup.js"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["dist"],
3 | "exclude": ["dist/__tests__/"],
4 | "extension": [".js", ".ts"],
5 | "reporter": ["text", "html"],
6 | "exclude-after-remap": false,
7 | "check-coverage": true,
8 | "per-file": true,
9 | "all": true,
10 | "branches": 0,
11 | "lines": 0,
12 | "functions": 0,
13 | "statements": 0
14 | }
15 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist_electron
3 | .eslintrc.js
4 | .github
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-recommended-scss",
3 | "rules": {
4 | "selector-pseudo-class-no-unknown": [
5 | true,
6 | {
7 | "ignorePseudoClasses": [
8 | "deep"
9 | ]
10 | }
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Thomas Blasquez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wildlander launcher
2 |
3 | A launcher for the [Wildlander](https://www.wildlandermod.com/) modpack.
4 |
5 | The intention of the launcher is to ease the installation process for modpacks that utilise [Wabbajack](https://www.wabbajack.org/#/) but require extra steps.
6 | This includes copying game files, installing prerequisites, various support options, etc.
7 | The idea is to attempt to make the installation of a modpack more of a one click procedure.
8 |
9 | [Install the latest release from the releases page](https://github.com/Wildlander-mod/Launcher/releases/latest).
10 |
11 | # Contributing
12 |
13 | [CONTRIBUTING.md](.github/CONTRIBUTING.md)
14 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@vue/cli-plugin-babel/preset"],
3 | };
4 |
--------------------------------------------------------------------------------
/build/installerHeaderIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/build/installerHeaderIcon.ico
--------------------------------------------------------------------------------
/dev-app-update.yml.example:
--------------------------------------------------------------------------------
1 | owner: wildlander-mod
2 | repo: launcher
3 | provider: github
4 | releaseType: "release"
5 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "*.{js,jsx,vue,ts,tsx}": ["vue-cli-service lint", () => "npm run test"],
3 | "*.scss": ["npm run lint:styles"],
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wildlander-launcher",
3 | "version": "2.19.0",
4 | "description": "A launcher for the Wabbajack modpacks",
5 | "author": "Wildlander",
6 | "scripts": {
7 | "clean": "lb-clean dist",
8 | "precompile": "npm run clean",
9 | "compile": "lb-tsc",
10 | "postcompile": "tsc-alias",
11 | "prestart": "npm run compile",
12 | "start": "vue-cli-service electron:serve",
13 | "start:dev": "nodemon",
14 | "prebuild": "npm run compile",
15 | "build": "vue-cli-service electron:build",
16 | "lint": "npm run lint:typescript && npm run lint:eslint && npm run lint:styles",
17 | "lint:fix": "npm run lint:eslint:fix",
18 | "electron:debug:main": "electron --remote-debugging-port=9223 ./dist_electron",
19 | "electron:debug:renderer": "npm run start -- --debug",
20 | "lint:eslint": "vue-cli-service lint",
21 | "lint:eslint:fix": "vue-cli-service lint --fix",
22 | "lint:styles": "stylelint **/*.scss",
23 | "lint:typescript": "tsc --noEmit",
24 | "postinstall": "electron-builder install-app-deps",
25 | "postuninstall": "electron-builder install-app-deps",
26 | "pretest": "npm run compile",
27 | "test": "npm run test:unit",
28 | "test:coverage": "lb-nyc npm run test",
29 | "test:unit": "lb-mocha \"dist/__tests__/unit\""
30 | },
31 | "nodemonConfig": {
32 | "watch": [
33 | "src/**"
34 | ],
35 | "ignore": [
36 | "src/renderer/**"
37 | ],
38 | "exec": "npm run start",
39 | "ext": "*"
40 | },
41 | "main": "background.js",
42 | "dependencies": {
43 | "@loopback/boot": "^5.0.2",
44 | "@loopback/context": "^5.0.1",
45 | "@loopback/core": "^4.0.2",
46 | "@vueform/toggle": "^2.1.1",
47 | "electron-context-menu": "^3.4.0",
48 | "electron-log": "^4.4.8",
49 | "electron-shutdown-command": "^2.0.1",
50 | "electron-store": "^8.1.0",
51 | "electron-updater": "^4.6.5",
52 | "fetch-installed-software": "^0.0.7",
53 | "fs-extra": "^10.1.0",
54 | "js-ini": "^1.5.1",
55 | "junk": "^3.1.0",
56 | "mitt": "^3.0.0",
57 | "node-fetch": "^2.6.7",
58 | "ps-list": "^7.2.0",
59 | "tslib": "^2.5.0",
60 | "vue": "^3.2.31",
61 | "vue-class-component": "^8.0.0-0",
62 | "vue-final-modal": "^3.4.4",
63 | "vue-router": "^4.1.3",
64 | "vue3-click-away": "^1.2.4",
65 | "vue3-popper": "^1.5.0"
66 | },
67 | "devDependencies": {
68 | "@babel/plugin-proposal-class-properties": "^7.16.7",
69 | "@babel/preset-typescript": "^7.16.5",
70 | "@commitlint/config-conventional": "^15.0.0",
71 | "@loopback/build": "^9.0.2",
72 | "@loopback/testlab": "^5.0.2",
73 | "@types/electron-devtools-installer": "^2.2.0",
74 | "@types/fs-extra": "^9.0.13",
75 | "@types/ini": "^1.3.31",
76 | "@types/mocha": "^9.1.1",
77 | "@types/mock-fs": "^4.13.1",
78 | "@types/mock-require": "^2.0.1",
79 | "@types/ncp": "^2.0.5",
80 | "@types/node-fetch": "^2.6.1",
81 | "@typescript-eslint/eslint-plugin": "^2.33.0",
82 | "@typescript-eslint/parser": "^2.33.0",
83 | "@vue/cli-plugin-babel": "~4.5.15",
84 | "@vue/cli-plugin-eslint": "^4.5.15",
85 | "@vue/cli-plugin-router": "~4.5.15",
86 | "@vue/cli-plugin-typescript": "^4.5.15",
87 | "@vue/cli-service": "~4.5.15",
88 | "@vue/compiler-sfc": "^3.2.26",
89 | "@vue/eslint-config-prettier": "^6.0.0",
90 | "@vue/eslint-config-standard": "^5.1.2",
91 | "@vue/eslint-config-typescript": "^5.0.2",
92 | "babel-eslint": "^10.1.0",
93 | "commitlint": "^15.0.0",
94 | "electron": "^16.0.5",
95 | "electron-devtools-installer": "^3.1.0",
96 | "eslint": "^6.7.2",
97 | "eslint-plugin-import": "^2.25.3",
98 | "eslint-plugin-node": "^11.1.0",
99 | "eslint-plugin-prettier": "^3.4.1",
100 | "eslint-plugin-promise": "^6.0.0",
101 | "eslint-plugin-standard": "^4.0.0",
102 | "eslint-plugin-vue": "^8.2.0",
103 | "lint-staged": "^12.1.3",
104 | "mocha": "^10.0.0",
105 | "mock-fs": "^5.1.3",
106 | "mock-require": "^3.0.3",
107 | "node-sass": "^6.0.1",
108 | "nodemon": "^2.0.15",
109 | "prettier": "^2.5.1",
110 | "reflect-metadata": "^0.1.13",
111 | "sass-loader": "^10.2.0",
112 | "stylelint": "^13.12.0",
113 | "stylelint-config-recommended-scss": "^4.3.0",
114 | "stylelint-scss": "^3.21.0",
115 | "tsc-alias": "^1.6.4",
116 | "tsconfig-paths": "^4.0.0",
117 | "typescript": "^4.5.4",
118 | "vue-cli-plugin-electron-builder": "~2.1.1",
119 | "vue-property-decorator": "^10.0.0-rc.3"
120 | },
121 | "commitlint": {
122 | "extends": [
123 | "@commitlint/config-conventional"
124 | ],
125 | "rules": {
126 | "footer-max-line-length": [
127 | 0,
128 | "always"
129 | ]
130 | },
131 | "parserPreset": {
132 | "parserOpts": {
133 | "noteKeywords": [
134 | "docs:"
135 | ]
136 | }
137 | }
138 | },
139 | "gitHooks": {
140 | "pre-commit": "lint-staged",
141 | "commit-msg": "commitlint -e -V "
142 | },
143 | "repository": "https://github.com/Wildlander-mod/Launcher"
144 | }
145 |
--------------------------------------------------------------------------------
/public/images/default-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/public/images/default-background.png
--------------------------------------------------------------------------------
/public/images/logos/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
15 |
19 |
23 |
24 |
--------------------------------------------------------------------------------
/public/images/logos/faq.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
15 |
19 |
23 |
27 |
31 |
35 |
36 |
--------------------------------------------------------------------------------
/public/images/logos/new-player-guide.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
15 |
19 |
23 |
27 |
31 |
35 |
39 |
43 |
44 |
--------------------------------------------------------------------------------
/public/images/logos/patreon.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
37 |
38 |
39 |
40 |
46 |
47 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/public/images/logos/reddit.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/public/images/logos/twitch.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/images/logos/wildlander-icon-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/public/images/logos/wildlander-icon-dark.png
--------------------------------------------------------------------------------
/public/images/logos/wildlander-icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/public/images/logos/wildlander-icon-light.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/__tests__/unit/controllers/graphics.controller.test.ts:
--------------------------------------------------------------------------------
1 | import { GraphicsController } from "@/main/controllers/graphics/graphics.controller";
2 | import { GraphicsService } from "@/main/services/graphics.service";
3 | import {
4 | createStubInstance,
5 | sinon,
6 | StubbedInstanceWithSinonAccessor,
7 | } from "@loopback/testlab";
8 |
9 | describe("Graphics controller", () => {
10 | let graphicsService: StubbedInstanceWithSinonAccessor;
11 | let graphicsController: GraphicsController;
12 |
13 | beforeEach(() => {
14 | graphicsService = createStubInstance(GraphicsService);
15 | graphicsController = new GraphicsController(graphicsService);
16 | });
17 |
18 | it("should get graphics preferences", () => {
19 | graphicsController.getGraphicsPreference();
20 | sinon.assert.called(graphicsService.stubs.getGraphicsPreference);
21 | });
22 |
23 | it("should get the graphics settings", () => {
24 | graphicsController.getGraphics();
25 | sinon.assert.called(graphicsService.stubs.getGraphics);
26 | });
27 |
28 | it("should set the graphics preference", () => {
29 | graphicsController.setGraphics("mock graphics");
30 | sinon.assert.calledWith(graphicsService.stubs.setGraphics, "mock graphics");
31 | });
32 |
33 | it("should restore the graphics preference", () => {
34 | graphicsController.restoreGraphics();
35 | sinon.assert.called(graphicsService.stubs.restoreGraphics);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/blacklist.service.test.ts:
--------------------------------------------------------------------------------
1 | import { createStubInstance, expect } from "@loopback/testlab";
2 | import { SystemService } from "@/main/services/system.service";
3 | import { BlacklistService } from "@/main/services/blacklist.service";
4 |
5 | describe("Blacklist service", () => {
6 | it("should return a list of blacklisted processes running", async () => {
7 | const mockSystemService = createStubInstance(SystemService);
8 |
9 | mockSystemService.stubs.isProcessRunning.resolves(false);
10 | mockSystemService.stubs.isProcessRunning
11 | .withArgs("WRCoreService.x64.exe")
12 | .resolves(true);
13 |
14 | const blacklistService = new BlacklistService(mockSystemService);
15 |
16 | expect(await blacklistService.blacklistedProcessesRunning()).to.eql([
17 | {
18 | name: "WebRoot Antivirus",
19 | processName: "WRCoreService.x64.exe",
20 | running: true,
21 | },
22 | ]);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/config.service.test.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService, UserPreferences } from "@/main/services/config.service";
2 | import { expect } from "@loopback/testlab";
3 | import mockFs from "mock-fs";
4 | import Store from "electron-store";
5 |
6 | describe("Config Service", () => {
7 | let mockStore: Store;
8 | let mockModDirectory: string;
9 |
10 | beforeEach(() => {
11 | const mockUserConfig = "/mock/config";
12 | const preferenceFile = "userPreferences";
13 | mockModDirectory = "mock/mod/directory";
14 | mockFs({
15 | [`${mockUserConfig}/${preferenceFile}.json`]: JSON.stringify({
16 | MOD_DIRECTORY: mockModDirectory,
17 | }),
18 | });
19 | mockStore = new Store({
20 | name: preferenceFile,
21 | cwd: mockUserConfig,
22 | });
23 | });
24 |
25 | afterEach(() => {
26 | mockFs.restore();
27 | });
28 |
29 | it("should get the mod directory", () => {
30 | const configService = new ConfigService(mockStore);
31 | expect(configService.modDirectory()).to.eql("mock/mod/directory");
32 | });
33 |
34 | it("should return the modlist launcher directory", () => {
35 | const configService = new ConfigService(mockStore);
36 | expect(configService.launcherDirectory()).to.eql(
37 | `${mockModDirectory}/launcher`
38 | );
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/game.service.test.ts:
--------------------------------------------------------------------------------
1 | import mockFs from "mock-fs";
2 | import {
3 | createStubInstance,
4 | expect,
5 | StubbedInstanceWithSinonAccessor,
6 | } from "@loopback/testlab";
7 | import { GameService } from "@/main/services/game.service";
8 | import { ConfigService } from "@/main/services/config.service";
9 | import fs from "fs";
10 |
11 | describe("GameService", () => {
12 | let mockConfigService: StubbedInstanceWithSinonAccessor;
13 | let gameService: GameService;
14 |
15 | let logPath: string;
16 |
17 | beforeEach(() => {
18 | mockConfigService = createStubInstance(ConfigService);
19 | gameService = new GameService(mockConfigService);
20 |
21 | logPath = "mock/log/path";
22 | });
23 |
24 | afterEach(() => {
25 | mockFs.restore();
26 | });
27 |
28 | it("should copy Skyrim logs if they exist", async () => {
29 | mockConfigService.stubs.skyrimDirectory.returns("mock/path");
30 | mockConfigService.stubs.getLogDirectory.returns(logPath);
31 |
32 | mockFs({
33 | "mock/path/d3dx9_42.log": "mock log file",
34 | [logPath]: {},
35 | });
36 |
37 | await gameService.copySkyrimLaunchLogs();
38 |
39 | expect(fs.existsSync(`${logPath}/skyrim-launch-logs.log`)).to.true();
40 | });
41 |
42 | it("should not copy Skyrim logs if they don't exist", async () => {
43 | mockConfigService.stubs.skyrimDirectory.returns("mock/path");
44 | mockConfigService.stubs.getLogDirectory.returns(logPath);
45 |
46 | mockFs({
47 | [logPath]: {},
48 | });
49 |
50 | await gameService.copySkyrimLaunchLogs();
51 |
52 | expect(fs.existsSync(`${logPath}/skyrim-launch-logs.log`)).to.false();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/modOrganizer.service.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MO2Names,
3 | ModOrganizerService,
4 | } from "@/main/services/modOrganizer.service";
5 | import {
6 | createStubInstance,
7 | expect,
8 | StubbedInstanceWithSinonAccessor,
9 | } from "@loopback/testlab";
10 | import { EnbService } from "@/main/services/enb.service";
11 | import { ErrorService } from "@/main/services/error.service";
12 | import { ConfigService } from "@/main/services/config.service";
13 | import { ResolutionService } from "@/main/services/resolution.service";
14 | import { GameService } from "@/main/services/game.service";
15 | import { ProfileService } from "@/main/services/profile.service";
16 | import { SystemService } from "@/main/services/system.service";
17 | import { GraphicsService } from "@/main/services/graphics.service";
18 | import mockFs from "mock-fs";
19 |
20 | describe("ModOrganizer service", () => {
21 | let mockEnbService: StubbedInstanceWithSinonAccessor;
22 | let mockErrorService: StubbedInstanceWithSinonAccessor;
23 | let mockConfigService: StubbedInstanceWithSinonAccessor;
24 | let mockResolutionService: StubbedInstanceWithSinonAccessor;
25 | let mockGameService: StubbedInstanceWithSinonAccessor;
26 | let mockProfileService: StubbedInstanceWithSinonAccessor;
27 | let mockSystemService: StubbedInstanceWithSinonAccessor;
28 | let mockGraphicsService: StubbedInstanceWithSinonAccessor;
29 |
30 | let modOrganizerService: ModOrganizerService;
31 |
32 | beforeEach(() => {
33 | mockEnbService = createStubInstance(EnbService);
34 | mockErrorService = createStubInstance(ErrorService);
35 | mockConfigService = createStubInstance(ConfigService);
36 | mockResolutionService = createStubInstance(ResolutionService);
37 | mockGameService = createStubInstance(GameService);
38 | mockProfileService = createStubInstance(ProfileService);
39 | mockSystemService = createStubInstance(SystemService);
40 | mockGraphicsService = createStubInstance(GraphicsService);
41 | });
42 |
43 | afterEach(() => {
44 | mockFs.restore();
45 | });
46 |
47 | it("should determine if Mod Organizer is running", async () => {
48 | mockSystemService.stubs.isProcessRunning
49 | .withArgs(MO2Names.MO2EXE)
50 | .resolves(true);
51 |
52 | modOrganizerService = new ModOrganizerService(
53 | mockEnbService,
54 | mockErrorService,
55 | mockConfigService,
56 | mockResolutionService,
57 | mockGameService,
58 | mockProfileService,
59 | mockSystemService,
60 | mockGraphicsService
61 | );
62 |
63 | expect(await modOrganizerService.isRunning()).to.eql(true);
64 | });
65 |
66 | it("should get the first binary", async () => {
67 | const mockModDirectory = "mock/mod/directory";
68 |
69 | mockFs({
70 | [mockModDirectory]: {
71 | [MO2Names.MO2Settings]: `
72 | [customExecutables]
73 | size=1
74 | 1\\binary=mock/first/custom/executable.exe
75 | 1\\title=SKSE
76 | `,
77 | },
78 | });
79 |
80 | mockConfigService.stubs.modDirectory.returns(mockModDirectory);
81 |
82 | modOrganizerService = new ModOrganizerService(
83 | mockEnbService,
84 | mockErrorService,
85 | mockConfigService,
86 | mockResolutionService,
87 | mockGameService,
88 | mockProfileService,
89 | mockSystemService,
90 | mockGraphicsService
91 | );
92 |
93 | expect(await modOrganizerService.getFirstCustomExecutableTitle()).to.eql(
94 | "SKSE"
95 | );
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/profile.service.test.ts:
--------------------------------------------------------------------------------
1 | import mockFs from "mock-fs";
2 | import { FriendlyDirectoryMap } from "@/modpack-metadata";
3 | import {
4 | createStubInstance,
5 | expect,
6 | sinon,
7 | StubbedInstanceWithSinonAccessor,
8 | } from "@loopback/testlab";
9 | import { ConfigService } from "@/main/services/config.service";
10 | import { ProfileService } from "@/main/services/profile.service";
11 | import { USER_PREFERENCE_KEYS } from "@/shared/enums/userPreferenceKeys";
12 |
13 | describe("Profile service", () => {
14 | let mockConfigService: StubbedInstanceWithSinonAccessor;
15 | let profileService: ProfileService;
16 |
17 | beforeEach(() => {
18 | mockConfigService = createStubInstance(ConfigService);
19 | profileService = new ProfileService(mockConfigService);
20 | });
21 |
22 | afterEach(() => {
23 | mockFs.restore();
24 | });
25 |
26 | it("should get if the user is showing hidden profiles", () => {
27 | mockConfigService.stubs.getPreference
28 | .withArgs(USER_PREFERENCE_KEYS.SHOW_HIDDEN_PROFILE)
29 | .returns(true);
30 |
31 | expect(profileService.getShowHiddenProfiles()).to.eql(true);
32 | });
33 |
34 | it("should default showing hidden profiles to false if the key doesn't exist", () => {
35 | mockConfigService.stubs.getPreference
36 | .withArgs(USER_PREFERENCE_KEYS.SHOW_HIDDEN_PROFILE)
37 | .returns(undefined);
38 |
39 | expect(profileService.getShowHiddenProfiles()).to.eql(false);
40 | });
41 |
42 | it("should set if the user is showing hidden profiles", () => {
43 | mockConfigService.stubs.getPreference
44 | .withArgs(USER_PREFERENCE_KEYS.SHOW_HIDDEN_PROFILE)
45 | .returns(true);
46 | profileService.setShowHiddenProfiles(false);
47 |
48 | mockConfigService.stubs.setPreference.calledWith(sinon.match.string, false);
49 | });
50 |
51 | it("should get the profiles", async () => {
52 | const modDirectory = "mod/directory";
53 | const launcherDirectory = `${modDirectory}/launcher`;
54 |
55 | mockConfigService.stubs.modDirectory.returns(modDirectory);
56 | mockConfigService.stubs.launcherDirectory.returns(launcherDirectory);
57 |
58 | mockFs({
59 | [`${launcherDirectory}/namesMO2.json`]: JSON.stringify([
60 | {
61 | real: "mock-real-name",
62 | friendly: "mock friendly name",
63 | },
64 | ] as FriendlyDirectoryMap[]),
65 | [`${modDirectory}/profiles/mock-real-name`]: {},
66 | [`${modDirectory}/profiles/unmapped`]: {},
67 | });
68 |
69 | const profiles = await profileService.getProfiles();
70 | const expected: FriendlyDirectoryMap[] = [
71 | {
72 | real: "mock-real-name",
73 | friendly: "mock friendly name",
74 | },
75 | {
76 | real: "unmapped",
77 | friendly: "unmapped",
78 | },
79 | ];
80 |
81 | expect(profiles).to.eql(expected);
82 | });
83 |
84 | it("should get the profile's directories", async () => {
85 | const modDirectory = "mod/directory";
86 | const launcherDirectory = `${modDirectory}/launcher`;
87 |
88 | mockConfigService.stubs.modDirectory.returns(modDirectory);
89 | mockConfigService.stubs.launcherDirectory.returns(launcherDirectory);
90 |
91 | mockFs({
92 | [`${launcherDirectory}/namesMO2.json`]: JSON.stringify([
93 | {
94 | real: "mock-real-name",
95 | friendly: "mock friendly name",
96 | },
97 | ] as FriendlyDirectoryMap[]),
98 | [`${modDirectory}/profiles/unmapped`]: {},
99 | [`${modDirectory}/profiles/mock-real-name`]: {},
100 | });
101 |
102 | const profileDirectories = await profileService.getProfileDirectories();
103 | const expected = [
104 | `${modDirectory}/profiles/mock-real-name`,
105 | `${modDirectory}/profiles/unmapped`,
106 | ];
107 |
108 | expect(profileDirectories).to.eql(expected);
109 | });
110 |
111 | it("should prepend the profile directory", () => {
112 | const modDirectory = "mod/directory";
113 |
114 | mockConfigService.stubs.modDirectory.returns(modDirectory);
115 |
116 | expect(profileService.prependProfileDirectory("mock-file.json")).to.eql(
117 | "mod/directory/profiles/mock-file.json"
118 | );
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/startup.service.test.ts:
--------------------------------------------------------------------------------
1 | import mock from "mock-require";
2 | // electron-is-dev throws an error if not running in electron
3 | mock("electron-is-dev", () => {
4 | return true;
5 | });
6 |
7 | import { StartupService } from "@/main/services/startup.service";
8 | import {
9 | createStubInstance,
10 | sinon,
11 | StubbedInstanceWithSinonAccessor,
12 | } from "@loopback/testlab";
13 | import { ModpackService } from "@/main/services/modpack.service";
14 | import { LauncherService } from "@/main/services/launcher.service";
15 | import { ConfigService } from "@/main/services/config.service";
16 | import { WabbajackService } from "@/main/services/wabbajack.service";
17 | import { ResolutionService } from "@/main/services/resolution.service";
18 | import { UpdateService } from "@/main/services/update.service";
19 | import { BlacklistService } from "@/main/services/blacklist.service";
20 | import { ErrorService } from "@/main/services/error.service";
21 | import { WindowService } from "@/main/services/window.service";
22 |
23 | describe("Startup service", () => {
24 | let mockModpackService: StubbedInstanceWithSinonAccessor;
25 | let mockLauncherService: StubbedInstanceWithSinonAccessor;
26 | let mockConfigService: StubbedInstanceWithSinonAccessor;
27 | let mockWabbajackService: StubbedInstanceWithSinonAccessor;
28 | let mockResolutionService: StubbedInstanceWithSinonAccessor;
29 | let mockUpdateService: StubbedInstanceWithSinonAccessor;
30 | let mockBlacklistService: StubbedInstanceWithSinonAccessor;
31 | let mockErrorService: StubbedInstanceWithSinonAccessor;
32 | let mockWindowService: StubbedInstanceWithSinonAccessor;
33 |
34 | let startupService: StartupService;
35 |
36 | beforeEach(() => {
37 | mockModpackService = createStubInstance(ModpackService);
38 | mockLauncherService = createStubInstance(LauncherService);
39 | mockConfigService = createStubInstance(ConfigService);
40 | mockWabbajackService = createStubInstance(WabbajackService);
41 | mockResolutionService = createStubInstance(ResolutionService);
42 | mockUpdateService = createStubInstance(UpdateService);
43 | mockBlacklistService = createStubInstance(BlacklistService);
44 | mockErrorService = createStubInstance(ErrorService);
45 | mockWindowService = createStubInstance(WindowService);
46 |
47 | startupService = new StartupService(
48 | mockModpackService,
49 | mockLauncherService,
50 | mockConfigService,
51 | mockWabbajackService,
52 | mockResolutionService,
53 | mockUpdateService,
54 | mockBlacklistService,
55 | mockErrorService,
56 | mockWindowService
57 | );
58 | });
59 |
60 | after(() => {
61 | mock.stopAll();
62 | });
63 |
64 | it("should quit if a blacklisted process is running", async () => {
65 | mockBlacklistService.stubs.blacklistedProcessesRunning.resolves([
66 | { name: "mock name", processName: "mockProcess.exe", running: true },
67 | ]);
68 |
69 | startupService.registerStartupCommands("processBlacklist");
70 | await startupService.runStartup();
71 |
72 | sinon.assert.called(mockWindowService.stubs.quit);
73 | });
74 |
75 | it("should not quit if there are no blacklisted processes", () => {
76 | startupService.registerStartupCommands(
77 | 'Check if blacklisted process is running"'
78 | );
79 | startupService.runStartup();
80 |
81 | sinon.assert.notCalled(mockWindowService.stubs.quit);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/__tests__/unit/services/system.service.test.ts:
--------------------------------------------------------------------------------
1 | import { SystemService } from "@/main/services/system.service";
2 | import {
3 | createStubInstance,
4 | expect,
5 | StubbedInstanceWithSinonAccessor,
6 | } from "@loopback/testlab";
7 | import { ConfigService } from "@/main/services/config.service";
8 | import { ErrorService } from "@/main/services/error.service";
9 | import psList from "ps-list";
10 |
11 | describe("System service", () => {
12 | let mockConfigService: StubbedInstanceWithSinonAccessor;
13 | let mockErrorService: StubbedInstanceWithSinonAccessor;
14 |
15 | let systemService: SystemService;
16 |
17 | beforeEach(() => {
18 | mockConfigService = createStubInstance(ConfigService);
19 | mockErrorService = createStubInstance(ErrorService);
20 | });
21 |
22 | it("should return true if a process is running", async () => {
23 | const mockListProcess = () =>
24 | new Promise((resolve) =>
25 | resolve([{ name: "mockname.exe", pid: 1234, ppid: 1234 }])
26 | );
27 | systemService = new SystemService(
28 | mockConfigService,
29 | mockErrorService,
30 | mockListProcess
31 | );
32 |
33 | expect(await systemService.isProcessRunning("mockname.exe")).to.eql(true);
34 | });
35 |
36 | it("should return false if a process is not", async () => {
37 | const mockListProcess = () =>
38 | new Promise((resolve) =>
39 | resolve([{ name: "mockname.exe", pid: 1234, ppid: 1234 }])
40 | );
41 | systemService = new SystemService(
42 | mockConfigService,
43 | mockErrorService,
44 | mockListProcess
45 | );
46 |
47 | expect(await systemService.isProcessRunning("notrunning.exe")).to.eql(
48 | false
49 | );
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/__tests__/unit/setup/global-setup.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@/main/logger";
2 |
3 | // Disable logging
4 | logger.transports.console.level = false;
5 | logger.transports.file.level = false;
6 |
--------------------------------------------------------------------------------
/src/additional-instructions.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "action": "disable-plugin",
4 | "type": "enb",
5 | "target": "noEnb",
6 | "plugin": "NightEyeENBFix_PredatorVision.esp"
7 | },
8 | {
9 | "action": "disable-ultra-widescreen",
10 | "version": "1.0.0"
11 | },
12 | {
13 | "action": "enable-mod",
14 | "type": "resolution-ratio",
15 | "target": "21:9",
16 | "mod": "Wildlander 21-9 Resolution Support"
17 | },
18 | {
19 | "action": "enable-mod",
20 | "type": "resolution-ratio",
21 | "target": "32:9",
22 | "mod": "Wildlander 32-9 Resolution Support"
23 | },
24 | {
25 | "action": "enable-plugin",
26 | "type": "resolution-ratio",
27 | "target": [
28 | "21:9",
29 | "32:9"
30 | ],
31 | "plugin": "widescreen_skyui_fix.esp"
32 | }
33 | ]
34 |
--------------------------------------------------------------------------------
/src/assets/fonts/averta-black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-black.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-blackitalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-blackitalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-bolditalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-bolditalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-extrabold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-extrabold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-extrabolditalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-extrabolditalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-extrathin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-extrathin.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-extrathinitalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-extrathinitalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-light.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-lightitalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-lightitalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-regularitalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-regularitalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-semibold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-semibolditalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-semibolditalic.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-thin.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/averta-thinitalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/fonts/averta-thinitalic.ttf
--------------------------------------------------------------------------------
/src/assets/icons/MaterialIcons-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/icons/MaterialIcons-Regular.woff2
--------------------------------------------------------------------------------
/src/assets/scss/index.scss:
--------------------------------------------------------------------------------
1 | // The order of these imports is important.
2 | // It is roughly following itcss style
3 |
4 | @import "./settings/colours";
5 | @import "./settings/sizes";
6 | @import "./settings/font";
7 | @import "./settings/font-face";
8 |
9 | @import "./layout";
10 |
11 | @import "./utility";
12 |
13 | @import "./popper";
14 |
--------------------------------------------------------------------------------
/src/assets/scss/layout.scss:
--------------------------------------------------------------------------------
1 | @import "~@/assets/scss/settings/sizes.scss";
2 |
3 | // All layout items are prefaced with `l-` to separate them from others
4 |
5 | .l-flex {
6 | display: flex;
7 | flex: 1;
8 | }
9 |
10 | .l-no-flex-grow {
11 | flex: 0;
12 | }
13 |
14 | .l-column {
15 | @extend .l-flex;
16 | flex-direction: column;
17 | }
18 |
19 | .l-center {
20 | @extend .l-flex;
21 |
22 | justify-content: center;
23 | align-items: center;
24 | }
25 |
26 | .l-center-vertically {
27 | @extend .l-flex;
28 |
29 | flex-direction: row;
30 | align-items: center;
31 | }
32 |
33 | .l-row {
34 | @extend .l-flex;
35 |
36 | flex-direction: row;
37 |
38 | &--bottom {
39 | align-items: flex-end;
40 | }
41 | }
42 |
43 | .l-center-text {
44 | text-align: center;
45 | }
46 |
47 | .l-start {
48 | @extend .l-flex;
49 |
50 | justify-content: flex-start;
51 | }
52 |
53 | .l-end {
54 | @extend .l-flex;
55 |
56 | justify-content: flex-end;
57 | }
58 |
59 | .l-flex-grow {
60 | flex-grow: 1;
61 | }
62 |
63 | .l-space-around {
64 | @extend .l-flex;
65 |
66 | justify-content: space-around;
67 | }
68 |
69 | .l-space-between {
70 | @extend .l-flex;
71 |
72 | justify-content: space-between;
73 | }
74 |
75 | .l-spacing-right {
76 | margin-right: $size-spacing;
77 | }
78 |
--------------------------------------------------------------------------------
/src/assets/scss/popper.scss:
--------------------------------------------------------------------------------
1 | @import "./layout";
2 | @import "./settings/colours";
3 |
4 | // All popper classes need important because once built, the order of the css is not consistent
5 | :deep(.popper) {
6 | padding: $size-spacing !important;
7 | color: white;
8 | }
9 |
10 | :deep(.popper),
11 | :deep(.popper:hover),
12 | :deep(.popper #arrow::before),
13 | :deep(.popper:hover > #arrow::before) {
14 | background-color: $colour-background--darker-solid !important;
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/scss/settings/colours.scss:
--------------------------------------------------------------------------------
1 | $colour-primary: #6f00c6;
2 | $colour-primary--light: #8f00ff;
3 | $colour-secondary: #e1e1e1;
4 | $colour-alternate: #00bf6f;
5 | $colour-text: #ffffff;
6 | $colour-text--dark: darken($colour-text, 60%);
7 | $colour-text--secondary: #ffffff66;
8 | $color-warning: #980000;
9 | $color-warning--light: #fd1717;
10 | $colour-background: #e1e1e1;
11 | $colour-background-secondary: #000000;
12 | $colour-background--light: rgba(255, 255, 255, 0.2);
13 | $colour-background--light-solid: rgb(118, 118, 118);
14 | $colour-background--transparent: transparentize($colour-background, 0.8);
15 | $colour-background--dark: rgba(132, 132, 132, 0.2);
16 | $colour-background--darker: transparentize(#1f1f1f, 0.1);
17 | $colour-background--darker-solid: rgb(31, 31, 31);
18 | $colour-background-secondary--transparent: transparentize(
19 | $colour-background-secondary,
20 | 0.8
21 | );
22 |
23 | $colour-star: #ffce00;
24 |
25 | $background-blur: blur(10px);
26 | $background-blur--more: blur(20px);
27 |
--------------------------------------------------------------------------------
/src/assets/scss/settings/font-face.scss:
--------------------------------------------------------------------------------
1 | .material-icons {
2 | font-family: "Material Icons", serif;
3 | font-weight: normal;
4 | font-style: normal;
5 | font-size: 24px; /* Preferred icon size */
6 | display: inline-block;
7 | line-height: 1;
8 | text-transform: none;
9 | letter-spacing: normal;
10 | word-wrap: normal;
11 | white-space: nowrap;
12 | direction: ltr;
13 |
14 | /* Support for all WebKit browsers. */
15 | -webkit-font-smoothing: antialiased;
16 | }
17 |
18 | @font-face {
19 | font-family: "Material Icons";
20 | font-style: normal;
21 | font-weight: 400;
22 | src: url(~@/assets/icons/MaterialIcons-Regular.woff2);
23 | }
24 |
25 | html {
26 | font-family: "averta", serif;
27 | }
28 |
29 | @font-face {
30 | font-family: "averta";
31 | font-style: normal;
32 | font-weight: 100 199;
33 | src: url(~@/assets/fonts/averta-extrathin.ttf);
34 | }
35 |
36 | @font-face {
37 | font-family: "averta";
38 | font-style: italic;
39 | font-weight: 100 199;
40 | src: url(~@/assets/fonts/averta-extrathinitalic.ttf);
41 | }
42 |
43 | @font-face {
44 | font-family: "averta";
45 | font-style: normal;
46 | font-weight: 200 299;
47 | src: url(~@/assets/fonts/averta-thin.ttf);
48 | }
49 |
50 | @font-face {
51 | font-family: "averta";
52 | font-style: italic;
53 | font-weight: 200 299;
54 | src: url(~@/assets/fonts/averta-thinitalic.ttf);
55 | }
56 |
57 | @font-face {
58 | font-family: "averta";
59 | font-style: normal;
60 | font-weight: 300 399;
61 | src: url(~@/assets/fonts/averta-light.ttf);
62 | }
63 |
64 | @font-face {
65 | font-family: "averta";
66 | font-style: italic;
67 | font-weight: 300 399;
68 | src: url(~@/assets/fonts/averta-lightitalic.ttf);
69 | }
70 |
71 | @font-face {
72 | font-family: "averta";
73 | font-style: normal;
74 | font-weight: 400 599;
75 | src: url(~@/assets/fonts/averta-regular.ttf);
76 | }
77 |
78 | @font-face {
79 | font-family: "averta";
80 | font-style: italic;
81 | font-weight: 400 599;
82 | src: url(~@/assets/fonts/averta-regularitalic.ttf);
83 | }
84 |
85 | @font-face {
86 | font-family: "averta";
87 | font-style: normal;
88 | font-weight: 600 699;
89 | src: url(~@/assets/fonts/averta-semibold.ttf);
90 | }
91 |
92 | @font-face {
93 | font-family: "averta";
94 | font-style: italic;
95 | font-weight: 600 699;
96 | src: url(~@/assets/fonts/averta-semibolditalic.ttf);
97 | }
98 |
99 | @font-face {
100 | font-family: "averta";
101 | font-style: normal;
102 | font-weight: 700 799;
103 | src: url(~@/assets/fonts/averta-bold.ttf);
104 | }
105 |
106 | @font-face {
107 | font-family: "averta";
108 | font-style: italic;
109 | font-weight: 700 799;
110 | src: url(~@/assets/fonts/averta-bolditalic.ttf);
111 | }
112 |
113 | @font-face {
114 | font-family: "averta";
115 | font-style: normal;
116 | font-weight: 800 899;
117 | src: url(~@/assets/fonts/averta-extrabold.ttf);
118 | }
119 |
120 | @font-face {
121 | font-family: "averta";
122 | font-style: italic;
123 | font-weight: 800 899;
124 | src: url(~@/assets/fonts/averta-extrabolditalic.ttf);
125 | }
126 |
127 | @font-face {
128 | font-family: "averta";
129 | font-style: normal;
130 | font-weight: 900 1000;
131 | src: url(~@/assets/fonts/averta-black.ttf);
132 | }
133 |
134 | @font-face {
135 | font-family: "averta";
136 | font-style: italic;
137 | font-weight: 900 1000;
138 | src: url(~@/assets/fonts/averta-blackitalic.ttf);
139 | }
140 |
--------------------------------------------------------------------------------
/src/assets/scss/settings/font.scss:
--------------------------------------------------------------------------------
1 | $font-size--x-small: 6px;
2 | $font-size--small: 10px;
3 | $font-size: 12px;
4 | $font-size--large: 14px;
5 | $font-size--x-large: 18px;
6 |
7 | $font-weight--small: 300;
8 | $font-weight: 400;
9 | $font-weight--large: 700;
10 | $font-weight--x-large: 700;
11 |
12 | $line-height--small: 12px;
13 | $line-height: 20px;
14 | $line-height--large: 22px;
15 | $line-height--x-large: 30px;
16 |
17 | $line-height__body: $line-height;
18 | $font-size__body: $font-size;
19 |
20 | $font-size-subtitle-alt: $font-size--x-large;
21 | $lint-height-subtitle-alt: $line-height--large;
22 |
--------------------------------------------------------------------------------
/src/assets/scss/settings/sizes.scss:
--------------------------------------------------------------------------------
1 | $size-window-height: 580px;
2 | $size-window-width: 1000px;
3 |
4 | $size-spacing--x-large: 30px;
5 | $size-spacing--large: 15px;
6 | $size-spacing: 10px;
7 | $size-spacing--small: 7px;
8 | $size-spacing--x-small: 5px;
9 | $size-spacing--titlebar: 24px;
10 |
11 | $size-action-height: 30px;
12 |
--------------------------------------------------------------------------------
/src/assets/scss/utility.scss:
--------------------------------------------------------------------------------
1 | @import "./settings/colours";
2 | @import "./settings/sizes";
3 | @import "./settings/font";
4 |
5 | // Utility classes not tied to a component
6 |
7 | .u-spacing {
8 | padding: $size-spacing;
9 | }
10 |
11 | .u-list--bare {
12 | list-style: none;
13 | margin: 0;
14 | padding: 0;
15 | }
16 |
17 | .u-scroll-x {
18 | overflow-x: scroll;
19 | }
20 |
21 | .u-scroll-y {
22 | overflow-y: scroll;
23 | }
24 |
25 | .u-scroll-y-auto {
26 | overflow-y: auto;
27 | }
28 |
29 | .u-small-scrollbar {
30 | &::-webkit-scrollbar {
31 | width: 2px;
32 | background-color: transparent;
33 | }
34 |
35 | &::-webkit-scrollbar-thumb {
36 | background-color: $colour-text--secondary;
37 | border-radius: 1px;
38 | }
39 |
40 | /* Buttons */
41 | &::-webkit-scrollbar-button:single-button {
42 | height: 0;
43 | width: 0;
44 | }
45 | }
46 |
47 | .u-text {
48 | box-sizing: border-box;
49 | font-weight: $font-weight--small;
50 | size: $font-size--large;
51 | margin: 0;
52 | }
53 |
54 | .u-disable-click-events {
55 | pointer-events: none;
56 | user-select: none;
57 | }
58 |
--------------------------------------------------------------------------------
/src/assets/tools/QRes.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wildlander-mod/Launcher/049f2bf2530826f7a62162f41b3149418fd4e259/src/assets/tools/QRes.exe
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { app, protocol } from "electron";
2 | import { isDevelopment } from "./main/services/config.service";
3 | import { autoUpdater } from "electron-updater";
4 | import { LauncherApplication } from "@/main/application";
5 | import { logger } from "@/main/logger";
6 | import { ErrorService } from "@/main/services/error.service";
7 | import { WindowService } from "@/main/services/window.service";
8 |
9 | const isSingleInstance = app.requestSingleInstanceLock();
10 | if (!isSingleInstance) {
11 | app.quit();
12 | }
13 |
14 | // Ensure it's easy to tell where the logs for this application start
15 | const initialLog = `| ${new Date().toLocaleString()} |`;
16 | logger.debug("-".repeat(initialLog.length));
17 | logger.debug(initialLog);
18 | logger.debug("-".repeat(initialLog.length));
19 | autoUpdater.logger = require("electron-log");
20 |
21 | // Scheme must be registered before the app is ready
22 | protocol.registerSchemesAsPrivileged([
23 | { scheme: "app", privileges: { secure: true, standard: true } },
24 | ]);
25 |
26 | // Exit cleanly on request from parent process in development mode.
27 | if (isDevelopment) {
28 | if (process.platform === "win32") {
29 | process.on("message", (data) => {
30 | if (data === "graceful-exit") {
31 | app.quit();
32 | }
33 | });
34 | } else {
35 | process.on("SIGTERM", () => {
36 | app.quit();
37 | });
38 | }
39 | }
40 |
41 | const start = async () => {
42 | const launcherApplication = new LauncherApplication();
43 | await launcherApplication.boot();
44 | await launcherApplication.start();
45 |
46 | app.on("second-instance", async () => {
47 | const windowService = await launcherApplication.getServiceByClass(
48 | WindowService
49 | );
50 | // Someone tried to run a second instance, so focus the original window.
51 | windowService.focusWindow();
52 | });
53 | };
54 |
55 | // This method will be called when Electron has finished
56 | // initialization and is ready to create browser windows.
57 | // Some APIs can only be used after this event occurs.
58 | app.on("ready", async () => {
59 | try {
60 | await start();
61 | } catch (error) {
62 | const errorService = new ErrorService();
63 | await errorService.handleError(
64 | "Failed to start application",
65 | (error as Error).message
66 | );
67 | process.exit(1);
68 | }
69 |
70 | logger.debug("App started");
71 | });
72 |
--------------------------------------------------------------------------------
/src/main/application.ts:
--------------------------------------------------------------------------------
1 | import { StartupService } from "@/main/services/startup.service";
2 | import { Constructor } from "@loopback/context";
3 | import { WindowService } from "@/main/services/window.service";
4 | import { logger } from "@/main/logger";
5 | import { BootMixin } from "@loopback/boot";
6 | import { Application } from "@loopback/core";
7 | import { Controller } from "@/main/decorators/controller.decorator";
8 | import { ErrorService } from "@/main/services/error.service";
9 |
10 | const serviceNamespace = "services";
11 |
12 | export class LauncherApplication extends BootMixin(Application) {
13 | constructor() {
14 | super();
15 |
16 | this.projectRoot = __dirname;
17 |
18 | this.onStart(async () => {
19 | try {
20 | await this.registerHandlers();
21 |
22 | const startupService = await this.getServiceByClass(StartupService);
23 | startupService.registerStartupCommands();
24 | await startupService.runStartup();
25 |
26 | await this.startBrowser();
27 | } catch (error) {
28 | const errorService = await this.getServiceByClass(ErrorService);
29 | await errorService.handleError(
30 | "Failed to start application",
31 | (error as Error).message
32 | );
33 | process.exit(1);
34 | }
35 | });
36 | }
37 |
38 | public getServiceByClass(cls: Constructor): Promise {
39 | return this.get(`${serviceNamespace}.${cls.name}`);
40 | }
41 |
42 | private async registerHandlers() {
43 | logger.silly("Registering handlers");
44 |
45 | for (const controllerBinding of this.findByTag("controller")) {
46 | (await this.get(controllerBinding.key)).registerHandlers();
47 | }
48 |
49 | logger.silly("Registered handlers");
50 | }
51 |
52 | private async startBrowser() {
53 | const renderService = await this.getServiceByClass(WindowService);
54 | await renderService.createBrowserWindow();
55 | await renderService.load("/");
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/controllers/config/config.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { service } from "@loopback/core";
3 | import { CONFIG_EVENTS } from "@/main/controllers/config/config.events";
4 | import { ConfigService } from "@/main/services/config.service";
5 |
6 | @controller
7 | export class ConfigController {
8 | constructor(@service(ConfigService) private configService: ConfigService) {}
9 |
10 | @handle(CONFIG_EVENTS.EDIT_CONFIG)
11 | editConfig() {
12 | this.configService.editPreferences();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/controllers/config/config.events.ts:
--------------------------------------------------------------------------------
1 | export const enum CONFIG_EVENTS {
2 | EDIT_CONFIG = "EDIT_CONFIG",
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/controllers/dialog/dialog.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { DIALOG_EVENTS } from "@/main/controllers/dialog/dialog.events";
3 | import { service } from "@loopback/core";
4 | import { ErrorService } from "@/main/services/error.service";
5 | import { dialog } from "electron";
6 |
7 | @controller
8 | export class DialogController {
9 | constructor(@service(ErrorService) private errorService: ErrorService) {}
10 |
11 | @handle(DIALOG_EVENTS.ERROR)
12 | async error({ title, error }: { title: string; error: string }) {
13 | await this.errorService.handleError(title, error);
14 | }
15 |
16 | @handle(DIALOG_EVENTS.CONFIRMATION)
17 | async confirmation({
18 | message,
19 | buttons,
20 | }: {
21 | message: string;
22 | buttons: string[];
23 | }) {
24 | return await dialog.showMessageBox({ message, buttons });
25 | }
26 |
27 | @handle(DIALOG_EVENTS.DIRECTORY_SELECT)
28 | directorySelect() {
29 | return dialog.showOpenDialog({ properties: ["openDirectory"] });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/controllers/dialog/dialog.events.ts:
--------------------------------------------------------------------------------
1 | export const enum DIALOG_EVENTS {
2 | ERROR = "ERROR",
3 | CONFIRMATION = "CONFIRMATION",
4 | DIRECTORY_SELECT = "DIRECTORY_SELECT",
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/controllers/enb/enb.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { ENB_EVENTS } from "@/main/controllers/enb/enb.events";
3 | import { EnbService } from "@/main/services/enb.service";
4 | import { service } from "@loopback/core";
5 | import { FriendlyDirectoryMap } from "@/modpack-metadata";
6 |
7 | @controller
8 | export class EnbController {
9 | constructor(@service(EnbService) private enbService: EnbService) {}
10 |
11 | @handle(ENB_EVENTS.GET_ENB_PRESETS)
12 | getEnbPresets(): Promise {
13 | return this.enbService.getENBPresets();
14 | }
15 |
16 | @handle(ENB_EVENTS.RESTORE_ENB_PRESETS)
17 | async restoreEnbPresets(): Promise {
18 | await this.enbService.restoreENBPresets();
19 | }
20 |
21 | @handle(ENB_EVENTS.GET_ENB_PREFERENCE)
22 | async getEnbPreference(): Promise {
23 | return await this.enbService.getEnbPreference();
24 | }
25 |
26 | @handle(ENB_EVENTS.SET_ENB_PREFERENCE)
27 | async setEnbPreference(enb: unknown): Promise {
28 | if (typeof enb !== "string") {
29 | throw new Error(`ENB is not a string: ${enb}`);
30 | }
31 | await this.enbService.setEnb(enb);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/controllers/enb/enb.events.ts:
--------------------------------------------------------------------------------
1 | export const enum ENB_EVENTS {
2 | GET_ENB_PRESETS = "GET_ENB_PRESETS",
3 | RESTORE_ENB_PRESETS = "RESTORE_ENB_PRESETS",
4 |
5 | GET_ENB_PREFERENCE = "GET_ENB_PREFERENCE",
6 | SET_ENB_PREFERENCE = "SET_ENB_PREFERENCE",
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/controllers/graphics/graphics.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { GRAPHICS_EVENTS } from "@/main/controllers/graphics/graphics.events";
3 | import { GraphicsService } from "@/main/services/graphics.service";
4 | import { service } from "@loopback/core";
5 |
6 | @controller
7 | export class GraphicsController {
8 | constructor(
9 | @service(GraphicsService) private graphicsService: GraphicsService
10 | ) {}
11 |
12 | @handle(GRAPHICS_EVENTS.GET_GRAPHICS_PREFERENCE)
13 | getGraphicsPreference() {
14 | return this.graphicsService.getGraphicsPreference();
15 | }
16 |
17 | @handle(GRAPHICS_EVENTS.GET_GRAPHICS)
18 | getGraphics() {
19 | return this.graphicsService.getGraphics();
20 | }
21 |
22 | @handle(GRAPHICS_EVENTS.SET_GRAPHICS)
23 | setGraphics(graphics: string) {
24 | return this.graphicsService.setGraphics(graphics);
25 | }
26 |
27 | @handle(GRAPHICS_EVENTS.RESTORE_GRAPHICS)
28 | restoreGraphics() {
29 | return this.graphicsService.restoreGraphics();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/controllers/graphics/graphics.events.ts:
--------------------------------------------------------------------------------
1 | export const enum GRAPHICS_EVENTS {
2 | GET_GRAPHICS_PREFERENCE = "GET_GRAPHICS_PREFERENCE",
3 | GET_GRAPHICS = "GET_GRAPHICS",
4 | SET_GRAPHICS = "SET_GRAPHICS",
5 | RESTORE_GRAPHICS = "RESTORE_GRAPHICS",
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/controllers/launcher/launcher.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { LAUNCHER_EVENTS } from "@/main/controllers/launcher/launcher.events";
3 | import { service } from "@loopback/core";
4 | import { LauncherService } from "@/main/services/launcher.service";
5 |
6 | @controller
7 | export class LauncherController {
8 | constructor(
9 | @service(LauncherService) private launcherService: LauncherService
10 | ) {}
11 |
12 | @handle(LAUNCHER_EVENTS.GET_VERSION)
13 | getVersion() {
14 | return this.launcherService.getVersion();
15 | }
16 |
17 | @handle(LAUNCHER_EVENTS.SET_CHECK_PREREQUISITES)
18 | setCheckPrerequisites(value: boolean) {
19 | return this.launcherService.setCheckPrerequisites(value);
20 | }
21 |
22 | @handle(LAUNCHER_EVENTS.GET_CHECK_PREREQUISITES)
23 | getCheckPrerequisites() {
24 | return this.launcherService.getCheckPrerequisites();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/controllers/launcher/launcher.events.ts:
--------------------------------------------------------------------------------
1 | export const enum LAUNCHER_EVENTS {
2 | GET_VERSION = " GET_VERSION",
3 |
4 | SET_CHECK_PREREQUISITES = "SET_CHECK_PREREQUISITES",
5 |
6 | GET_CHECK_PREREQUISITES = "GET_CHECK_PREREQUISITES",
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/controllers/modOrganizer/modOrganizer.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { MOD_ORGANIZER_EVENTS } from "@/main/controllers/modOrganizer/modOrganizer.events";
3 | import { ModOrganizerService } from "@/main/services/modOrganizer.service";
4 | import { service } from "@loopback/core";
5 |
6 | @controller
7 | export class ModOrganizerController {
8 | constructor(
9 | @service(ModOrganizerService)
10 | private modOrganizerService: ModOrganizerService
11 | ) {}
12 |
13 | @handle(MOD_ORGANIZER_EVENTS.LAUNCH_MO2)
14 | async launchMO2() {
15 | await this.modOrganizerService.launchMO2();
16 | }
17 |
18 | @handle(MOD_ORGANIZER_EVENTS.LAUNCH_GAME)
19 | async launchGame() {
20 | await this.modOrganizerService.launchGame();
21 | }
22 |
23 | @handle(MOD_ORGANIZER_EVENTS.CLOSE_MO2)
24 | async closeMO2() {
25 | await this.modOrganizerService.closeMO2();
26 | }
27 |
28 | @handle(MOD_ORGANIZER_EVENTS.IS_MO2_RUNNING)
29 | async isMO2Running(): Promise {
30 | return this.modOrganizerService.isRunning();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/controllers/modOrganizer/modOrganizer.events.ts:
--------------------------------------------------------------------------------
1 | export const enum MOD_ORGANIZER_EVENTS {
2 | LAUNCH_GAME = "LAUNCH_GAME",
3 | LAUNCH_MO2 = "LAUNCH_MO2",
4 | CLOSE_MO2 = "CLOSE_MO2",
5 |
6 | IS_MO2_RUNNING = "IS_MO2_RUNNING",
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/controllers/modpack/modpack.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import {
3 | IsModpackValidResponse,
4 | MODPACK_EVENTS,
5 | } from "@/main/controllers/modpack/mopack.events";
6 | import { service } from "@loopback/core";
7 | import { ModpackService } from "@/main/services/modpack.service";
8 | import { logger } from "@/main/logger";
9 | import { LauncherService } from "@/main/services/launcher.service";
10 |
11 | @controller
12 | export class ModpackController {
13 | constructor(
14 | @service(ModpackService) private modpackService: ModpackService,
15 | @service(LauncherService) private launcherService: LauncherService
16 | ) {}
17 |
18 | @handle(MODPACK_EVENTS.IS_MODPACK_SET)
19 | isModpackSet() {
20 | return this.modpackService.isModpackSet();
21 | }
22 |
23 | @handle(MODPACK_EVENTS.IS_MODPACK_DIRECTORY_VALID)
24 | isValidModDirectory(filepath: string): IsModpackValidResponse {
25 | if (!filepath) {
26 | logger.warn("No filepath specified when checking mod directory");
27 | return { ok: false };
28 | }
29 | return this.modpackService.checkModpackPathIsValid(filepath);
30 | }
31 |
32 | @handle(MODPACK_EVENTS.SET_MODPACK)
33 | setModpack(filepath: string) {
34 | if (!filepath) {
35 | logger.error("No filepath specified when setting mod directory");
36 | }
37 | return this.launcherService.setModpack(filepath);
38 | }
39 |
40 | @handle(MODPACK_EVENTS.GET_MODPACK)
41 | getModpack() {
42 | return this.modpackService.getModpackDirectory();
43 | }
44 |
45 | @handle(MODPACK_EVENTS.GET_MODPACK_METADATA)
46 | getModpackMetadata() {
47 | return this.modpackService.getModpackMetadata();
48 | }
49 |
50 | @handle(MODPACK_EVENTS.DELETE_MODPACK_DIRECTORY)
51 | deleteModpackDirectory() {
52 | return this.modpackService.deleteModpackDirectory();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/controllers/modpack/mopack.events.ts:
--------------------------------------------------------------------------------
1 | export const enum MODPACK_EVENTS {
2 | IS_MODPACK_SET = "IS_MODPACK_SET",
3 | IS_MODPACK_DIRECTORY_VALID = "IS_MODPACK_DIRECTORY_VALID",
4 | SET_MODPACK = "SET_MODPACK",
5 | GET_MODPACK = "GET_MODPACK",
6 | GET_MODPACK_METADATA = "GET_MODPACK_METADATA",
7 | DELETE_MODPACK_DIRECTORY = "DELETE_MODPACK_DIRECTORY",
8 | }
9 |
10 | export interface IsModpackValidResponse {
11 | ok: boolean;
12 | missingPaths?: string[];
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/controllers/profile/profile.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { ProfileService } from "@/main/services/profile.service";
3 | import { service } from "@loopback/core";
4 | import { PROFILE_EVENTS } from "@/main/controllers/profile/profile.events";
5 | import { LauncherService } from "@/main/services/launcher.service";
6 |
7 | @controller
8 | export class ProfileController {
9 | constructor(
10 | @service(ProfileService) private profileService: ProfileService,
11 | @service(LauncherService) private launcherService: LauncherService
12 | ) {}
13 |
14 | @handle(PROFILE_EVENTS.GET_PROFILES)
15 | getProfiles() {
16 | return this.profileService.getProfiles();
17 | }
18 |
19 | @handle(PROFILE_EVENTS.GET_PROFILE_PREFERENCE)
20 | getProfilePreference() {
21 | return this.profileService.getProfilePreference();
22 | }
23 |
24 | @handle(PROFILE_EVENTS.SET_PROFILE_PREFERENCE)
25 | setProfilePreference(profile: string) {
26 | return this.profileService.setProfilePreference(profile);
27 | }
28 |
29 | @handle(PROFILE_EVENTS.RESTORE_PROFILES)
30 | async restoreProfiles() {
31 | await this.profileService.restoreProfiles();
32 | await this.launcherService.refreshModpack();
33 | }
34 |
35 | @handle(PROFILE_EVENTS.GET_SHOW_HIDDEN_PROFILES)
36 | async getShowHiddenProfiles() {
37 | return this.profileService.getShowHiddenProfiles();
38 | }
39 |
40 | @handle(PROFILE_EVENTS.SET_SHOW_HIDDEN_PROFILES)
41 | async setShowHiddenProfiles(show: boolean) {
42 | return this.profileService.setShowHiddenProfiles(show);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/controllers/profile/profile.events.ts:
--------------------------------------------------------------------------------
1 | export const enum PROFILE_EVENTS {
2 | GET_PROFILES = "GET_PROFILES",
3 | GET_PROFILE_PREFERENCE = "GET_PROFILE_PREFERENCE",
4 | SET_PROFILE_PREFERENCE = "SET_PROFILE_PREFERENCE",
5 | RESTORE_PROFILES = "RESTORE_PROFILES",
6 | GET_SHOW_HIDDEN_PROFILES = "GET_SHOW_HIDDEN_PROFILES",
7 | SET_SHOW_HIDDEN_PROFILES = "SET_SHOW_HIDDEN_PROFILES",
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/controllers/resolution/resolution.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { RESOLUTION_EVENTS } from "@/main/controllers/resolution/resolution.events";
3 | import { ResolutionService } from "@/main/services/resolution.service";
4 | import { service } from "@loopback/core";
5 | import { Resolution } from "@/Resolution";
6 |
7 | @controller
8 | export class ResolutionController {
9 | constructor(
10 | @service(ResolutionService) private resolutionService: ResolutionService
11 | ) {}
12 |
13 | @handle(RESOLUTION_EVENTS.GET_RESOLUTION_PREFERENCE)
14 | getResolutionPreference() {
15 | return this.resolutionService.getResolutionPreference();
16 | }
17 |
18 | @handle(RESOLUTION_EVENTS.SET_RESOLUTION_PREFERENCE)
19 | setResolutionPreference(resolution: Resolution) {
20 | return this.resolutionService.setResolution(resolution);
21 | }
22 |
23 | @handle(RESOLUTION_EVENTS.GET_RESOLUTIONS)
24 | getResolutions() {
25 | return this.resolutionService.getResolutions();
26 | }
27 |
28 | @handle(RESOLUTION_EVENTS.IS_UNSUPPORTED_RESOLUTION)
29 | isUnsupportedResolution(resolution: Resolution) {
30 | return this.resolutionService.isUnsupportedResolution(resolution);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/controllers/resolution/resolution.events.ts:
--------------------------------------------------------------------------------
1 | export const enum RESOLUTION_EVENTS {
2 | GET_RESOLUTIONS = "GET_RESOLUTIONS",
3 | IS_UNSUPPORTED_RESOLUTION = "IS_UNSUPPORTED_RESOLUTION",
4 | GET_RESOLUTION_PREFERENCE = "GET_RESOLUTION_PREFERENCE",
5 | SET_RESOLUTION_PREFERENCE = "SET_RESOLUTION_PREFERENCE",
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/controllers/system/system.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { service } from "@loopback/core";
3 | import { SystemService } from "@/main/services/system.service";
4 | import { SYSTEM_EVENTS } from "@/main/controllers/system/system.events";
5 | import { shell } from "electron";
6 | import { ErrorService } from "@/main/services/error.service";
7 |
8 | @controller
9 | export class SystemController {
10 | constructor(
11 | @service(SystemService) private systemService: SystemService,
12 | @service(ErrorService) private errorService: ErrorService
13 | ) {}
14 |
15 | @handle(SYSTEM_EVENTS.OPEN_CRASH_LOGS)
16 | async openCrashLogs() {
17 | await this.systemService.openCrashLogs();
18 | }
19 |
20 | @handle(SYSTEM_EVENTS.OPEN_APPLICATION_LOGS)
21 | async openApplicationLogs() {
22 | await this.systemService.openApplicationLogsPath();
23 | }
24 |
25 | @handle(SYSTEM_EVENTS.OPEN_LINK_IN_BROWSER)
26 | async openLinkInBrowser(link: string) {
27 | try {
28 | await shell.openExternal(link);
29 | } catch (error) {
30 | await this.errorService.handleError(
31 | "Error opening link",
32 | (error as Error).message
33 | );
34 | }
35 | }
36 |
37 | @handle(SYSTEM_EVENTS.CLEAR_APP_LOGS)
38 | public async clearLogFiles() {
39 | await this.systemService.clearApplicationLogs();
40 | }
41 |
42 | @handle(SYSTEM_EVENTS.CHECK_PREREQUISITES)
43 | async checkPrerequisite() {
44 | return await this.systemService.checkPrerequisitesInstalled();
45 | }
46 |
47 | @handle(SYSTEM_EVENTS.INSTALL_PREREQUISITES)
48 | async installPrerequisites() {
49 | return await this.systemService.installPrerequisites();
50 | }
51 |
52 | @handle(SYSTEM_EVENTS.REBOOT)
53 | reboot() {
54 | this.systemService.reboot();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/controllers/system/system.events.ts:
--------------------------------------------------------------------------------
1 | export const enum SYSTEM_EVENTS {
2 | OPEN_CRASH_LOGS = "OPEN_CRASH_LOGS",
3 | OPEN_APPLICATION_LOGS = "OPEN_APPLICATION_LOGS",
4 | OPEN_LINK_IN_BROWSER = "OPEN_LINK_IN_BROWSER",
5 | CLEAR_APP_LOGS = "CLEAR_APP_LOGS",
6 | CHECK_PREREQUISITES = "CHECK_PREREQUISITES",
7 | INSTALL_PREREQUISITES = "INSTALL_PREREQUISITES",
8 | REBOOT = "REBOOT",
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/controllers/update/update.events.ts:
--------------------------------------------------------------------------------
1 | export const enum UPDATE_EVENTS {
2 | UPDATE_AVAILABLE = "update-available",
3 | UPDATE_NOT_AVAILABLE = "update-not-available",
4 | UPDATE_DOWNLOADED = "update-downloaded",
5 | DOWNLOAD_PROGRESS = "download-progress",
6 | ERROR = "error",
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/controllers/wabbajack/wabbajack.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { WABBAJACK_EVENTS } from "@/main/controllers/wabbajack/wabbajack.events";
3 | import { service } from "@loopback/core";
4 | import { WabbajackService } from "@/main/services/wabbajack.service";
5 |
6 | @controller
7 | export class WabbajackController {
8 | constructor(
9 | @service(WabbajackService) private wabbajackService: WabbajackService
10 | ) {}
11 |
12 | @handle(WABBAJACK_EVENTS.GET_INSTALLED_MODPACKS)
13 | async getInstalledModpacks() {
14 | return this.wabbajackService.getInstalledCurrentModpackPaths();
15 | }
16 |
17 | @handle(WABBAJACK_EVENTS.GET_MODPACK_VERSION)
18 | async getVersion() {
19 | return this.wabbajackService.getModpackVersion();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/controllers/wabbajack/wabbajack.events.ts:
--------------------------------------------------------------------------------
1 | export const enum WABBAJACK_EVENTS {
2 | GET_INSTALLED_MODPACKS = "GET_INSTALLED_MODPACKS",
3 | GET_MODPACK_VERSION = "GET_MODPACK_VERSION",
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/controllers/window/window.controller.ts:
--------------------------------------------------------------------------------
1 | import { controller, handle } from "@/main/decorators/controller.decorator";
2 | import { WindowService } from "@/main/services/window.service";
3 | import { service } from "@loopback/core";
4 | import { WindowEvents } from "@/main/controllers/window/window.events";
5 | import { ConfigService } from "@/main/services/config.service";
6 |
7 | @controller
8 | export class WindowController {
9 | constructor(
10 | @service(WindowService) private renderService: WindowService,
11 | @service(ConfigService) private configService: ConfigService
12 | ) {}
13 |
14 | @handle(WindowEvents.CLOSE)
15 | quit() {
16 | this.renderService.quit();
17 | }
18 |
19 | @handle(WindowEvents.RELOAD)
20 | reload() {
21 | this.renderService.reload();
22 | }
23 |
24 | @handle(WindowEvents.MINIMIZE)
25 | minimize() {
26 | this.renderService.minimize();
27 | }
28 |
29 | @handle(WindowEvents.OPEN_LOG_PATH)
30 | async openLogPath() {
31 | await this.configService;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/controllers/window/window.events.ts:
--------------------------------------------------------------------------------
1 | export const enum WindowEvents {
2 | CLOSE = "CLOSE",
3 | RELOAD = "RELOAD",
4 | MINIMIZE = "MINIMIZE",
5 | OPEN_LOG_PATH = "OPEN_LOG_PATH",
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/decorators/controller.decorator.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain, IpcMainInvokeEvent } from "electron";
2 | import { logger } from "@/main/logger";
3 | import { Constructor } from "@loopback/context";
4 |
5 | export interface Controller {
6 | registerHandlers: () => void;
7 | }
8 |
9 | const Handlers = "Handlers";
10 |
11 | /**
12 | Add a method to controllers that can be called to register all IPC handlers.
13 | This is then automatically be called by the booter.
14 | e.g.
15 | @controller
16 | class ExampleController {
17 |
18 | @handler("channelName")
19 | method(){
20 | //Do something when 'channelName' event is invoked...
21 | }
22 |
23 | }
24 | */
25 | export function controller>(Base: T) {
26 | return {
27 | [Base.name]: class extends Base implements Controller {
28 | public registerHandlers() {
29 | const handlers = Base.prototype[Handlers];
30 | handlers.forEach(
31 | (method: (...args: unknown[]) => unknown, channel: string) => {
32 | logger.silly(`Registered handler "${channel}"`);
33 | ipcMain.handle(
34 | channel,
35 | (event: IpcMainInvokeEvent, ...args: unknown[]) =>
36 | method.apply(this, [...args, event])
37 | );
38 | }
39 | );
40 | }
41 | },
42 | }[Base.name];
43 | }
44 |
45 | export function handle(channel: string) {
46 | return function (
47 | target: Record,
48 | propertyKey: string,
49 | propertyDescriptor: PropertyDescriptor
50 | ) {
51 | target[Handlers] = target[Handlers] || new Map();
52 | target[Handlers].set(channel, propertyDescriptor.value);
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/logger.ts:
--------------------------------------------------------------------------------
1 | import log from "electron-log";
2 |
3 | // Export from here in case we want to write any wrappers around the methods or change logger
4 | export const logger = log;
5 |
--------------------------------------------------------------------------------
/src/main/preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge, ipcRenderer } from "electron";
2 | import log from "electron-log";
3 |
4 | // Expose protected methods that allow the renderer process to use
5 | // the ipcRenderer without exposing the entire object
6 | contextBridge.exposeInMainWorld("ipcRenderer", {
7 | invoke: (channel: string, data: unknown) => {
8 | return ipcRenderer.invoke(channel, data);
9 | },
10 | on: (channel: string, callback: Function) => {
11 | // Deliberately strip event as it includes `sender`
12 | ipcRenderer.on(channel, (event, ...args) => callback(...args));
13 | },
14 | });
15 |
16 | contextBridge.exposeInMainWorld("logger", log.functions);
17 |
--------------------------------------------------------------------------------
/src/main/services/blacklist.service.ts:
--------------------------------------------------------------------------------
1 | import { BindingScope, injectable } from "@loopback/context";
2 | import { service } from "@loopback/core";
3 | import { SystemService } from "@/main/services/system.service";
4 |
5 | export interface BlacklistedProgram {
6 | name: string;
7 | processName: string;
8 | }
9 |
10 | @injectable({
11 | scope: BindingScope.SINGLETON,
12 | })
13 | export class BlacklistService {
14 | constructor(@service(SystemService) private systemService: SystemService) {}
15 |
16 | getBlacklistedPrograms(): BlacklistedProgram[] {
17 | return [
18 | // Webroot and Norton cause issues because the move some modpack files
19 | {
20 | name: "WebRoot Antivirus",
21 | processName: "WRCoreService.x64.exe",
22 | },
23 | {
24 | name: "WebRoot Antivirus",
25 | processName: "WRCoreService.x86.exe",
26 | },
27 | ];
28 | }
29 |
30 | async blacklistedProcessesRunning() {
31 | return (
32 | await Promise.all(
33 | this.getBlacklistedPrograms().map(async (process) => ({
34 | ...process,
35 | running: await this.systemService.isProcessRunning(
36 | process.processName
37 | ),
38 | }))
39 | )
40 | ).filter(({ running }) => running);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/services/config.service.ts:
--------------------------------------------------------------------------------
1 | import Store from "electron-store";
2 | import { USER_PREFERENCE_KEYS } from "@/shared/enums/userPreferenceKeys";
3 | import { Resolution } from "@/Resolution";
4 | import path from "path";
5 | import { BindingScope, injectable } from "@loopback/context";
6 | import { logger } from "@/main/logger";
7 | import fs from "fs";
8 |
9 | export const appRoot = path.resolve(`${__dirname}/../../`);
10 | export const isDevelopment = path.extname(appRoot) !== ".asar";
11 |
12 | export interface UserPreferences {
13 | [USER_PREFERENCE_KEYS.MOD_DIRECTORY]: string;
14 | [USER_PREFERENCE_KEYS.PRESET]: string;
15 | [USER_PREFERENCE_KEYS.GRAPHICS]: string;
16 | [USER_PREFERENCE_KEYS.ENB_PROFILE]: string;
17 | [USER_PREFERENCE_KEYS.PREVIOUS_ENB_PROFILE]: string;
18 | [USER_PREFERENCE_KEYS.RESOLUTION]: Resolution;
19 | [USER_PREFERENCE_KEYS.SHOW_HIDDEN_PROFILE]: boolean;
20 | [USER_PREFERENCE_KEYS.CHECK_PREREQUISITES]: boolean;
21 | }
22 |
23 | type PreferenceWithValidator = {
24 | [key in keyof UserPreferences]?: {
25 | value: UserPreferences[keyof UserPreferences];
26 | validate?: (...args: unknown[]) => boolean | Promise;
27 | };
28 | };
29 |
30 | export const userPreferences = new Store({
31 | name: "userPreferences",
32 | });
33 |
34 | @injectable({
35 | scope: BindingScope.SINGLETON,
36 | })
37 | export class ConfigService {
38 | constructor(private config = userPreferences) {}
39 |
40 | skyrimDirectory() {
41 | return `${this.modDirectory()}/Stock Game`;
42 | }
43 |
44 | getLogDirectory() {
45 | return path.dirname(logger.transports?.file.getFile().path);
46 | }
47 |
48 | modDirectory() {
49 | return this.config.get(USER_PREFERENCE_KEYS.MOD_DIRECTORY);
50 | }
51 |
52 | backupDirectory() {
53 | return `${this.modDirectory()}/launcher/_backups`;
54 | }
55 |
56 | backupsExist() {
57 | return fs.existsSync(this.backupDirectory());
58 | }
59 |
60 | getPreference(
61 | key: keyof UserPreferences
62 | ): T {
63 | return this.config.get(key) as unknown as T;
64 | }
65 |
66 | launcherDirectory() {
67 | return `${this.modDirectory()}/launcher`;
68 | }
69 |
70 | hasPreference(key: keyof UserPreferences) {
71 | return this.config.has(key);
72 | }
73 |
74 | deletePreference(key: keyof UserPreferences) {
75 | logger.debug(`Deleting preference: ${key}`);
76 | return this.config.delete(key);
77 | }
78 |
79 | setPreference(key: keyof UserPreferences | string, value: unknown) {
80 | if (typeof value === "object") {
81 | logger.debug(`Setting preference ${key} to ${JSON.stringify(value)}`);
82 | } else {
83 | logger.debug(`Setting preference ${key} to ${value}`);
84 | }
85 | return this.config.set(key, value);
86 | }
87 |
88 | /**
89 | Set the value specified if the key doesn't exist or the current value is invalid
90 | */
91 | async setDefaultPreferences(preferences: PreferenceWithValidator) {
92 | logger.debug("Setting default user preferences");
93 | logger.debug(`Current preferences`);
94 | logger.debug(this.getPreferences().store);
95 | for (const [key, { value, validate }] of Object.entries(preferences)) {
96 | const valid = validate ? await validate() : true;
97 | if (!valid) {
98 | logger.warn(
99 | `Current ${key} preference is invalid. Setting to default: ${value}`
100 | );
101 | }
102 | if ((!this.config.has(key) || !valid) && value) {
103 | this.setPreference(key, value);
104 | }
105 | }
106 | logger.debug("New preferences");
107 | logger.debug(this.getPreferences().store);
108 | }
109 |
110 | getPreferences() {
111 | return this.config;
112 | }
113 |
114 | editPreferences() {
115 | return userPreferences.openInEditor();
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/services/error.service.ts:
--------------------------------------------------------------------------------
1 | import { dialog } from "electron";
2 | import { logger } from "@/main/logger";
3 | import { BindingScope, injectable } from "@loopback/context";
4 |
5 | @injectable({
6 | scope: BindingScope.SINGLETON,
7 | })
8 | export class ErrorService {
9 | async handleError(title: string, message: string) {
10 | logger.error(`${title}: ${message}`);
11 | logger.error(new Error().stack);
12 | await dialog.showErrorBox(title, message);
13 | }
14 |
15 | async handleUnknownError(error?: unknown) {
16 | logger.error(
17 | `Unknown error. The stack trace might hold more details. ${error}`
18 | );
19 | logger.error(new Error().stack);
20 | await dialog.showErrorBox(
21 | "An unknown error has occurred",
22 | "If you require more support, please post a message in the official modpack Discord and send your launcher log files."
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/services/game.service.ts:
--------------------------------------------------------------------------------
1 | import { BindingScope, injectable } from "@loopback/context";
2 | import { ConfigService } from "@/main/services/config.service";
3 | import { service } from "@loopback/core";
4 | import fs from "fs";
5 | import { logger } from "@/main/logger";
6 |
7 | @injectable({
8 | scope: BindingScope.SINGLETON,
9 | })
10 | export class GameService {
11 | constructor(@service(ConfigService) private configService: ConfigService) {}
12 |
13 | async copySkyrimLaunchLogs() {
14 | logger.info("Copying Skyrim launch logs");
15 | const launchLogPath = `${this.configService.skyrimDirectory()}/d3dx9_42.log`;
16 | const logPath = this.configService.getLogDirectory();
17 | if (fs.existsSync(launchLogPath)) {
18 | return fs.promises.copyFile(
19 | launchLogPath,
20 | `${logPath}/skyrim-launch-logs.log`
21 | );
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/services/launcher.service.ts:
--------------------------------------------------------------------------------
1 | import { USER_PREFERENCE_KEYS } from "@/shared/enums/userPreferenceKeys";
2 | import { service } from "@loopback/core";
3 | import { ModOrganizerService } from "@/main/services/modOrganizer.service";
4 | import { ProfileService } from "@/main/services/profile.service";
5 | import { EnbService } from "@/main/services/enb.service";
6 | import { ConfigService } from "@/main/services/config.service";
7 | import { ResolutionService } from "@/main/services/resolution.service";
8 | import { ModpackService } from "@/main/services/modpack.service";
9 | import { BindingScope, injectable } from "@loopback/context";
10 | import { app } from "electron";
11 | import { logger } from "@/main/logger";
12 | import { ErrorService } from "@/main/services/error.service";
13 | import { WindowService } from "@/main/services/window.service";
14 | import { GraphicsService } from "@/main/services/graphics.service";
15 | import { MigrationService } from "@/main/services/migration.service";
16 |
17 | @injectable({
18 | scope: BindingScope.SINGLETON,
19 | })
20 | export class LauncherService {
21 | constructor(
22 | @service(EnbService) private enbService: EnbService,
23 | @service(ConfigService) private configService: ConfigService,
24 | @service(ResolutionService) private resolutionService: ResolutionService,
25 | @service(ModpackService) private modpackService: ModpackService,
26 | @service(ProfileService) private profileService: ProfileService,
27 | @service(ModOrganizerService)
28 | private modOrganizerService: ModOrganizerService,
29 | @service(ErrorService) private errorService: ErrorService,
30 | @service(WindowService) private windowService: WindowService,
31 | @service(GraphicsService) private graphicsService: GraphicsService,
32 | @service(MigrationService) private migrationService: MigrationService
33 | ) {}
34 |
35 | async refreshModpack() {
36 | logger.debug("Refreshing modpack");
37 | return this.setModpack(this.modpackService.getModpackDirectory());
38 | }
39 |
40 | async setModpack(filepath: string) {
41 | try {
42 | await this.configService.setPreference(
43 | USER_PREFERENCE_KEYS.MOD_DIRECTORY,
44 | filepath
45 | );
46 | await this.migrationService.separateProfileFromGraphics();
47 | await this.validateConfig();
48 | await this.backupAssets();
49 | await this.enbService.resetCurrentEnb(false);
50 | await this.resolutionService.setResolution(
51 | this.resolutionService.getResolutionPreference()
52 | );
53 | await this.resolutionService.setShouldDisableUltraWidescreen();
54 | await this.graphicsService.setGraphics(
55 | this.graphicsService.getGraphicsPreference()
56 | );
57 | } catch (error) {
58 | if (error instanceof Error && error.message.includes("EPERM")) {
59 | await this.errorService.handleError(
60 | "Permission error",
61 | `
62 | The launcher has been unable to create/modify some files due to a permissions error.
63 | It is strongly recommended you restart the application as an administrator.
64 | If this does not work, you will need to change the permissions of the install directory.`
65 | );
66 | this.windowService.quit();
67 | } else {
68 | await this.errorService.handleUnknownError(error);
69 | }
70 | }
71 | }
72 |
73 | async validateConfig() {
74 | logger.debug("Validating config...");
75 | await this.configService.setDefaultPreferences({
76 | [USER_PREFERENCE_KEYS.ENB_PROFILE]: {
77 | value: await this.enbService.getDefaultPreference(),
78 | validate: async () =>
79 | this.enbService.isValid(await this.enbService.getEnbPreference()),
80 | },
81 | [USER_PREFERENCE_KEYS.PRESET]: {
82 | value: await this.profileService.getDefaultPreference(),
83 | validate: async () =>
84 | this.profileService.isValid(
85 | await this.profileService.getProfilePreference()
86 | ),
87 | },
88 | [USER_PREFERENCE_KEYS.RESOLUTION]: {
89 | value: this.resolutionService.getCurrentResolution(),
90 | },
91 | [USER_PREFERENCE_KEYS.GRAPHICS]: {
92 | value: await this.graphicsService.getDefaultPreference(),
93 | validate: async () =>
94 | this.graphicsService.isValid(
95 | this.graphicsService.getGraphicsPreference()
96 | ),
97 | },
98 | });
99 | logger.debug("Config validated");
100 | }
101 |
102 | async backupAssets() {
103 | await this.enbService.backupOriginalENBs();
104 | await this.profileService.backupOriginalProfiles();
105 | await this.graphicsService.backupOriginalGraphics();
106 | }
107 |
108 | getVersion() {
109 | return app.getVersion();
110 | }
111 |
112 | setCheckPrerequisites(value: boolean) {
113 | return this.configService.setPreference(
114 | USER_PREFERENCE_KEYS.CHECK_PREREQUISITES,
115 | value
116 | );
117 | }
118 |
119 | getCheckPrerequisites() {
120 | return this.configService.getPreference(
121 | USER_PREFERENCE_KEYS.CHECK_PREREQUISITES
122 | );
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/services/modpack.service.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { logger } from "@/main/logger";
3 | import { MO2Names } from "@/main/services/modOrganizer.service";
4 | import { BindingScope, injectable } from "@loopback/context";
5 | import modpack from "@/modpack.json";
6 | import { IsModpackValidResponse } from "@/main/controllers/modpack/mopack.events";
7 | import { Modpack } from "@/modpack-metadata";
8 | import { service } from "@loopback/core";
9 | import { ConfigService } from "@/main/services/config.service";
10 | import { USER_PREFERENCE_KEYS } from "@/shared/enums/userPreferenceKeys";
11 |
12 | @injectable({
13 | scope: BindingScope.SINGLETON,
14 | })
15 | export class ModpackService {
16 | constructor(@service(ConfigService) private configService: ConfigService) {}
17 |
18 | checkModpackPathIsValid(modpackPath: string): IsModpackValidResponse {
19 | const missingPaths = [MO2Names.MO2EXE, "profiles", "launcher"]
20 | .filter((path) => !fs.existsSync(`${modpackPath}/${path}`))
21 | .map((path) => {
22 | logger.warn(
23 | `Selected mod directory "${modpackPath}" doesn't contain a "${path}" directory/file`
24 | );
25 | return path;
26 | });
27 |
28 | return { ok: missingPaths.length === 0, missingPaths };
29 | }
30 |
31 | checkCurrentModpackPathIsValid() {
32 | return (
33 | this.isModpackSet() &&
34 | this.checkModpackPathIsValid(this.getModpackDirectory()).ok
35 | );
36 | }
37 |
38 | getModpackDirectory() {
39 | return this.configService.getPreference(
40 | USER_PREFERENCE_KEYS.MOD_DIRECTORY
41 | );
42 | }
43 |
44 | getModpackMetadata(): Modpack {
45 | return modpack;
46 | }
47 |
48 | deleteModpackDirectory() {
49 | return this.configService.deletePreference(
50 | USER_PREFERENCE_KEYS.MOD_DIRECTORY
51 | );
52 | }
53 |
54 | isModpackSet(): boolean {
55 | return this.configService
56 | .getPreferences()
57 | .has(USER_PREFERENCE_KEYS.MOD_DIRECTORY);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/services/profile.service.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from "@/main/services/config.service";
2 | import { USER_PREFERENCE_KEYS } from "@/shared/enums/userPreferenceKeys";
3 | import { FriendlyDirectoryMap } from "@/modpack-metadata";
4 | import fs from "fs";
5 | import { not as isNotJunk } from "junk";
6 | import { logger } from "@/main/logger";
7 | import { copy, existsSync } from "fs-extra";
8 | import { service } from "@loopback/core";
9 |
10 | export class ProfileService {
11 | constructor(@service(ConfigService) private configService: ConfigService) {}
12 |
13 | profileDirectory() {
14 | return `${this.configService.modDirectory()}/profiles`;
15 | }
16 |
17 | profileBackupDirectory() {
18 | return `${this.configService.backupDirectory()}/profiles`;
19 | }
20 |
21 | profileMappingFile() {
22 | return `${this.configService.launcherDirectory()}/namesMO2.json`;
23 | }
24 |
25 | getShowHiddenProfiles() {
26 | return (
27 | this.configService.getPreference(
28 | USER_PREFERENCE_KEYS.SHOW_HIDDEN_PROFILE
29 | ) ?? false
30 | );
31 | }
32 |
33 | setShowHiddenProfiles(show: boolean) {
34 | return this.configService.setPreference(
35 | USER_PREFERENCE_KEYS.SHOW_HIDDEN_PROFILE,
36 | show
37 | );
38 | }
39 |
40 | async getProfiles(): Promise {
41 | // Get mapped profile names that have a mapping
42 | const mappedProfiles = await this.getMappedProfiles();
43 |
44 | // Get any profiles that don't have a mapping
45 | const unmappedProfiles = await this.getUnmappedProfiles(mappedProfiles);
46 |
47 | return [...mappedProfiles, ...unmappedProfiles];
48 | }
49 |
50 | getPhysicalProfiles() {
51 | return fs.promises.readdir(this.profileDirectory(), {
52 | withFileTypes: true,
53 | });
54 | }
55 |
56 | async getMappedProfiles() {
57 | return JSON.parse(
58 | await fs.promises.readFile(this.profileMappingFile(), "utf-8")
59 | ) as FriendlyDirectoryMap[];
60 | }
61 |
62 | async getUnmappedProfiles(mappedProfiles: FriendlyDirectoryMap[]) {
63 | return (
64 | (await this.getPhysicalProfiles())
65 | .filter((dirent) => dirent.isDirectory())
66 | .map((dirent) => dirent.name)
67 | .filter(isNotJunk)
68 | .map(
69 | (preset): FriendlyDirectoryMap => ({ real: preset, friendly: preset })
70 | )
71 | // Remove any profiles that have a mapping
72 | .filter(
73 | (unmappedPreset) =>
74 | !mappedProfiles.find(
75 | (mappedPreset: FriendlyDirectoryMap) =>
76 | mappedPreset.real === unmappedPreset.real
77 | )
78 | )
79 | );
80 | }
81 |
82 | async getProfileDirectories() {
83 | return (await this.getPhysicalProfiles()).map(
84 | ({ name }) => `${this.profileDirectory()}/${name}`
85 | );
86 | }
87 |
88 | async getBackedUpProfiles() {
89 | return fs.promises.readdir(this.profileBackupDirectory());
90 | }
91 |
92 | async getBackedUpProfileDirectories() {
93 | return (await this.getBackedUpProfiles()).map(
94 | (profile) => `${this.profileBackupDirectory()}/${profile}`
95 | );
96 | }
97 |
98 | prependProfileDirectory(profile: string) {
99 | return `${this.profileDirectory()}/${profile}`;
100 | }
101 |
102 | /**
103 | * Return the current profile preference or the first if it is invalid
104 | */
105 | async getProfilePreference() {
106 | return this.configService.getPreference(
107 | USER_PREFERENCE_KEYS.PRESET
108 | );
109 | }
110 |
111 | async getDefaultPreference() {
112 | return (await this.getPhysicalProfiles())[0].name;
113 | }
114 |
115 | setProfilePreference(profile: string) {
116 | this.configService.setPreference(USER_PREFERENCE_KEYS.PRESET, profile);
117 | }
118 |
119 | async isInProfileList(profile: string) {
120 | return (
121 | (await this.getPhysicalProfiles()).filter(({ name }) => name === profile)
122 | .length > 0
123 | );
124 | }
125 |
126 | async isValid(profile: string) {
127 | return this.isInProfileList(profile);
128 | }
129 |
130 | async backupOriginalProfiles() {
131 | const backupExists = existsSync(this.profileBackupDirectory());
132 | logger.debug(`Backup for profiles exists: ${backupExists}`);
133 |
134 | if (!backupExists) {
135 | logger.info("No profiles backup exists. Backing up...");
136 | await fs.promises.mkdir(this.configService.backupDirectory(), {
137 | recursive: true,
138 | });
139 |
140 | await copy(this.profileDirectory(), this.profileBackupDirectory());
141 | }
142 | }
143 |
144 | async restoreProfiles() {
145 | logger.info("Restoring MO2 profiles");
146 | await copy(this.profileBackupDirectory(), this.profileDirectory(), {
147 | overwrite: true,
148 | });
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/main/services/update.service.ts:
--------------------------------------------------------------------------------
1 | import { autoUpdater } from "electron-updater";
2 | import { app } from "electron";
3 | import path from "path";
4 | import { isDevelopment } from "@/main/services/config.service";
5 | import fs from "fs";
6 | import { service } from "@loopback/core";
7 | import { WindowService } from "@/main/services/window.service";
8 | import { logger } from "@/main/logger";
9 | import { BindingScope, injectable } from "@loopback/context";
10 | import { UPDATE_EVENTS } from "@/main/controllers/update/update.events";
11 | import { ErrorService } from "@/main/services/error.service";
12 |
13 | @injectable({
14 | scope: BindingScope.SINGLETON,
15 | })
16 | export class UpdateService {
17 | private devAppUpdatePath = path.join(
18 | __dirname,
19 | "../../../dev-app-update.yml"
20 | );
21 |
22 | constructor(
23 | @service(WindowService) private renderService: WindowService,
24 | @service(ErrorService) private errorService: ErrorService,
25 | @service(WindowService) private windowService: WindowService
26 | ) {}
27 |
28 | async update() {
29 | await this.windowService.createBrowserWindow();
30 | await this.windowService.load("/auto-update");
31 |
32 | return new Promise((resolve) => {
33 | // Only register if there is no update available. If there is an update, the window will close itself anyway.
34 | autoUpdater.on(UPDATE_EVENTS.UPDATE_NOT_AVAILABLE, () => {
35 | logger.debug("No update available");
36 | resolve();
37 | });
38 |
39 | if (this.shouldUpdate()) {
40 | this.checkForUpdate().catch((error) => {
41 | logger.debug(`Update failed with error ${error}. Continuing anyway.`);
42 | resolve();
43 | });
44 | } else {
45 | resolve();
46 | }
47 | });
48 | }
49 |
50 | shouldUpdate() {
51 | let shouldUpdate;
52 |
53 | if (isDevelopment && fs.existsSync(this.devAppUpdatePath)) {
54 | logger.debug(`Setting auto update path to ${this.devAppUpdatePath}`);
55 | autoUpdater.updateConfigPath = this.devAppUpdatePath;
56 | shouldUpdate = true;
57 | } else if (isDevelopment) {
58 | logger.debug(
59 | "Skipping app update check because we're in development mode"
60 | );
61 | shouldUpdate = false;
62 | } else if (app.getVersion().includes("-")) {
63 | logger.debug(
64 | "Skipping app update check because this is a pre-release version"
65 | );
66 | shouldUpdate = false;
67 | } else {
68 | shouldUpdate = true;
69 | }
70 |
71 | return shouldUpdate;
72 | }
73 |
74 | registerEvents() {
75 | autoUpdater.on(UPDATE_EVENTS.UPDATE_AVAILABLE, () => {
76 | logger.info(`Update available`);
77 | this.renderService.getWebContents().send(UPDATE_EVENTS.UPDATE_AVAILABLE);
78 | });
79 |
80 | autoUpdater.on(UPDATE_EVENTS.DOWNLOAD_PROGRESS, ({ percent }) => {
81 | this.renderService
82 | .getWebContents()
83 | .send(UPDATE_EVENTS.DOWNLOAD_PROGRESS, Math.floor(percent));
84 | });
85 |
86 | autoUpdater.on(UPDATE_EVENTS.UPDATE_DOWNLOADED, () => {
87 | logger.debug("Update downloaded");
88 | autoUpdater.quitAndInstall();
89 | });
90 |
91 | autoUpdater.on(UPDATE_EVENTS.ERROR, async (error: Error) => {
92 | let message;
93 | if (error.message.includes("net::ERR_NAME_NOT_RESOLVED")) {
94 | message = `This likely means you are not connected to the internet. It is recommended you use the latest launcher version as it might contain bug fixes for the modpack itself.`;
95 | } else {
96 | message = `An unknown error has occurred. Please try relaunching the launcher.`;
97 | }
98 |
99 | await this.errorService.handleError(
100 | "Error checking for update",
101 | `Cannot check for update. ${message}`
102 | );
103 | });
104 | }
105 |
106 | async checkForUpdate() {
107 | this.registerEvents();
108 | const updateCheckResult = await autoUpdater.checkForUpdates();
109 | logger.debug("Auto update check result");
110 | logger.debug(updateCheckResult);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/modpack.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Wildlander",
3 | "logo": "/images/logos/wildlander-full-light.svg",
4 | "backgroundImage": "/images/default-background.png",
5 | "website": "https://www.wildlandermod.com",
6 | "wiki": "https://wiki.wildlandermod.com/",
7 | "patreon": "https://www.patreon.com/dylanbperry",
8 | "roadmap": "https://airtable.com/shrvAxHcCeCqKfnGe"
9 | }
10 |
--------------------------------------------------------------------------------
/src/renderer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
49 |
50 |
110 |
--------------------------------------------------------------------------------
/src/renderer/components/AppDropdownFileSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/renderer/components/AppModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
21 | Close
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
74 |
75 |
109 |
--------------------------------------------------------------------------------
/src/renderer/components/AppPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
19 |
20 |
21 |
22 |
23 |
24 |
91 |
92 |
114 |
--------------------------------------------------------------------------------
/src/renderer/components/AppPageContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
26 |
27 |
55 |
--------------------------------------------------------------------------------
/src/renderer/components/BaseButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
76 |
--------------------------------------------------------------------------------
/src/renderer/components/BaseImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
--------------------------------------------------------------------------------
/src/renderer/components/BaseInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
41 |
42 |
58 |
--------------------------------------------------------------------------------
/src/renderer/components/BaseLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ label }}
4 |
5 |
6 |
7 |
17 |
18 |
34 |
--------------------------------------------------------------------------------
/src/renderer/components/BaseLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
39 |
40 |
53 |
--------------------------------------------------------------------------------
/src/renderer/components/BaseList.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
21 |
22 |
54 |
--------------------------------------------------------------------------------
/src/renderer/components/Community.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
12 |
13 |
17 |
22 |
23 |
24 |
29 |
30 |
31 |
36 |
37 |
38 |
39 |
40 |
65 |
66 |
97 |
--------------------------------------------------------------------------------
/src/renderer/components/ENB.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 | Uses GPU. Determines the quality of post-processing effects: ambient
11 | occlusion, sun rays, advanced lighting, and more.
12 |
13 |
14 |
15 |
85 |
--------------------------------------------------------------------------------
/src/renderer/components/GraphicsSelection.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 | Uses CPU and GPU. Determines the draw distance and quality of objects,
11 | lighting, shadows, and grass.
12 |
13 |
14 |
15 |
71 |
--------------------------------------------------------------------------------
/src/renderer/components/ImageWithText.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
27 |
28 |
35 |
--------------------------------------------------------------------------------
/src/renderer/components/LauncherVersion.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Warning: you are using a beta version of the application. This will not
6 | automatically update. Click to be taken to the download page for the
7 | latest version.
8 |
9 |
12 |
13 | Launcher Version: {{ version }}
14 |
15 | warning
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Launcher Version: {{ version }}
24 |
25 |
26 |
27 |
43 |
44 |
49 |
--------------------------------------------------------------------------------
/src/renderer/components/MO2RunningModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Mod Organizer 2 is currently running.
6 |
7 | To prevent conflicts, the launcher has been locked until Mod Organizer
8 | is closed.
9 |
10 |
11 |
Kill all MO2 Processes
13 |
14 |
15 |
16 |
17 |
18 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/renderer/components/ModDirectory.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
110 |
--------------------------------------------------------------------------------
/src/renderer/components/NavigationItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
33 |
--------------------------------------------------------------------------------
/src/renderer/components/News.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | {{ new Date(newsItem.published).toLocaleDateString() }}
11 |
12 |
13 |
14 | {{ newsItem.title }}
15 |
16 |
17 |
18 |
19 |
20 |
21 | Unable to load latest news.
22 |
23 |
24 |
25 |
60 |
61 |
104 |
--------------------------------------------------------------------------------
/src/renderer/components/Patrons.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 | star_outline
12 |
13 |
14 | star_outline
15 | star_outline
18 |
19 |
20 |
Super Patrons
21 |
22 |
23 |
27 |
28 |
29 | star_outline
30 |
31 |
Patrons
32 |
33 |
34 |
Unable to retrieve Patron list.
35 |
36 |
37 |
38 |
78 |
79 |
133 |
--------------------------------------------------------------------------------
/src/renderer/components/ProfileSelection.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 | Uses CPU and GPU. Determines the quality of visual mods, like textures and
12 | models. Gameplay is not affected.
13 |
14 |
15 |
16 |
96 |
--------------------------------------------------------------------------------
/src/renderer/components/Resolution.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 | info
14 |
15 |
16 | Ultra-widescreen resolutions are not supported in this modpack.
17 |
21 | More info.
22 |
23 |
24 |
25 |
26 | If your desired resolution has not been detected, go to the advanced tab
27 | and edit the launcher config.
28 |
29 |
30 |
31 |
32 |
33 |
119 |
120 |
127 |
--------------------------------------------------------------------------------
/src/renderer/components/TheHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
48 |
49 |
75 |
--------------------------------------------------------------------------------
/src/renderer/components/TheTitleBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | remove
6 |
7 |
8 | close
9 |
10 |
11 |
12 |
13 |
36 |
37 |
73 |
--------------------------------------------------------------------------------
/src/renderer/index.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { getRouter } from "./router";
3 | import VueFinalModal from "vue-final-modal";
4 | import VueClickAway from "vue3-click-away";
5 | import "reflect-metadata";
6 | import App from "@/renderer/App.vue";
7 | import { registerServices } from "@/renderer/services/service-container";
8 |
9 | const app = createApp(App);
10 |
11 | const { modpackService } = registerServices(app);
12 |
13 | app
14 | .use(getRouter(modpackService))
15 | .use(VueFinalModal())
16 | .use(VueClickAway)
17 | .mount("#app");
18 |
19 | // Prevent Mouse 4 and Mouse 5 from navigating the app
20 | // Ref: https://stackoverflow.com/a/66318490 GitHub Issue 654
21 | window.addEventListener("mouseup", (e) => {
22 | if (e.button === 3 || e.button === 4) e.preventDefault();
23 | });
24 |
--------------------------------------------------------------------------------
/src/renderer/router/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createRouter,
3 | createWebHashHistory,
4 | NavigationGuardWithThis,
5 | RouteLocationNormalized,
6 | RouteRecordRaw,
7 | } from "vue-router";
8 |
9 | import ViewHome from "@/renderer/views/ViewHome.vue";
10 | import ViewCommunity from "@/renderer/views/ViewCommunity.vue";
11 | import ViewAdvanced from "@/renderer/views/ViewAdvanced.vue";
12 | import AutoUpdate from "@/renderer/views/AutoUpdate.vue";
13 | import ModDirectoryView from "@/renderer/views/ModDirectory.vue";
14 | import { ModpackService } from "@/renderer/services/modpack.service";
15 |
16 | const HomeRouteName = "Home";
17 | const AutoUpdateRouteName = "AutoUpdate";
18 | const ModDirectoryRouteName = "ModDirectory";
19 |
20 | const checkModDirectory =
21 | (modpackService: ModpackService): NavigationGuardWithThis =>
22 | async () => {
23 | if (
24 | !(await modpackService.isModDirectorySet()) ||
25 | !(await modpackService.isCurrentModpackValid())
26 | ) {
27 | await modpackService.deleteModpackDirectory();
28 | return {
29 | name: ModDirectoryRouteName,
30 | };
31 | }
32 | };
33 |
34 | const getRoutes = (modpackService: ModpackService): RouteRecordRaw[] =>
35 | [
36 | {
37 | path: "/",
38 | name: HomeRouteName,
39 | component: ViewHome,
40 | beforeEnter: [checkModDirectory(modpackService)],
41 | },
42 | {
43 | path: "/community",
44 | name: "Community",
45 | component: ViewCommunity,
46 | beforeEnter: [checkModDirectory(modpackService)],
47 | },
48 | {
49 | path: "/advanced",
50 | name: "Advanced",
51 | component: ViewAdvanced,
52 | beforeEnter: [checkModDirectory(modpackService)],
53 | },
54 | {
55 | path: "/auto-update",
56 | name: AutoUpdateRouteName,
57 | component: AutoUpdate,
58 | meta: {
59 | preload: true,
60 | },
61 | },
62 | {
63 | path: "/mod-directory",
64 | name: ModDirectoryRouteName,
65 | component: ModDirectoryView,
66 | props: true,
67 | meta: {
68 | preload: true,
69 | },
70 | beforeEnter: async (to: RouteLocationNormalized) => {
71 | if (
72 | (await modpackService.isModDirectorySet()) &&
73 | to.name === ModDirectoryRouteName
74 | ) {
75 | // If the page is refreshed on the mod directory selection, just redirect to the home page
76 | return { name: HomeRouteName };
77 | }
78 | },
79 | },
80 | ]
81 | // If preload metadata is not set, set it to false
82 | .map((route) => {
83 | return {
84 | ...route,
85 | meta: { ...route.meta, preload: route.meta?.preload ?? false },
86 | };
87 | });
88 |
89 | export const getRouter = (modpackService: ModpackService) =>
90 | createRouter({
91 | history: createWebHashHistory(),
92 | routes: getRoutes(modpackService),
93 | });
94 |
--------------------------------------------------------------------------------
/src/renderer/services/cache.service.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@/main/logger";
2 |
3 | /**
4 | * The CacheService class is a simple wrapper around window.localStorage
5 | * localStorage can only store strings so this wrapper JSON.stringifies all data
6 | */
7 | export class CacheService {
8 | /**
9 | * Returns data from the localStorage if it exists and is newer than maxAge
10 | */
11 | public get(
12 | key: string,
13 | maxAge?: number
14 | ): { age: number; content: T } | undefined {
15 | const now = new Date();
16 | logger.debug(
17 | `Cache: query "${key}" with maxAge: ${maxAge || "unlimited"} seconds.`
18 | );
19 | const rawData = window.localStorage.getItem(key);
20 | if (rawData !== null) {
21 | const data: { age: number; content: T } = JSON.parse(rawData);
22 | // Convert maxAge to ms for comparison
23 | if (!maxAge || now.getTime() - data.age < maxAge * 1000) {
24 | logger.debug(`Cache: returning data for "${key}"`);
25 | return data;
26 | }
27 | }
28 | logger.debug(`Cache: could not find "${key}" in cache or too old`);
29 | return undefined;
30 | }
31 |
32 | /**
33 | * Sets data in the localStorage, overwrites regardless of previous data presence
34 | */
35 | public set(key: string, data: unknown) {
36 | logger.debug(`Cache: setting data for "${key}".`);
37 | window.localStorage.setItem(
38 | key,
39 | JSON.stringify({
40 | age: new Date().getTime(),
41 | content: data,
42 | })
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/renderer/services/event.service.ts:
--------------------------------------------------------------------------------
1 | import mitt from "mitt";
2 |
3 | export const ENABLE_LOADING_EVENT = "ENABLE_LOADING_EVENT";
4 | export const DISABLE_LOADING_EVENT = "DISABLE_LOADING_EVENT";
5 |
6 | export const EventService = mitt();
7 |
--------------------------------------------------------------------------------
/src/renderer/services/ipc.service.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from "electron";
2 |
3 | export class IpcService {
4 | public invoke(channel: string, ...args: unknown[]): Promise {
5 | return ipcRenderer.invoke(channel, ...args);
6 | }
7 |
8 | public on(channel: string, listener: (...args: unknown[]) => void) {
9 | return ipcRenderer.on(channel, listener);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/renderer/services/message.service.ts:
--------------------------------------------------------------------------------
1 | import { DIALOG_EVENTS } from "@/main/controllers/dialog/dialog.events";
2 | import { logger } from "@/main/logger";
3 | import { IpcService } from "@/renderer/services/ipc.service";
4 | import { MessageBoxReturnValue } from "electron";
5 |
6 | export class MessageService {
7 | constructor(private ipcService: IpcService) {}
8 |
9 | async error({ title, error }: { title: string; error: string }) {
10 | logger.error(`${title}: ${error}`);
11 | await this.ipcService.invoke(DIALOG_EVENTS.ERROR, { title, error });
12 | }
13 |
14 | async confirmation(message: string, buttons: string[]) {
15 | return this.ipcService.invoke(
16 | DIALOG_EVENTS.CONFIRMATION,
17 | {
18 | message,
19 | buttons,
20 | }
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/renderer/services/modal.service.ts:
--------------------------------------------------------------------------------
1 | import { EventService } from "@/renderer/services/service-container";
2 | import { VueFinalModalProperty } from "vue-final-modal";
3 |
4 | export const modalOpenedEvent = "modalOpened";
5 |
6 | export class ModalService {
7 | // maintain a list of modals that have tried to open to prevent more than one being open at once
8 | private modalQueue: string[] = [];
9 |
10 | constructor(private eventService: EventService) {}
11 |
12 | public openModal(name: string, vfm: VueFinalModalProperty) {
13 | if (this.modalQueue.length === 0) {
14 | // Currently no other modals waiting to be opened so just open the modal
15 | vfm.show(name);
16 | // Add to the modal queue until the modal is closed
17 | this.modalQueue.push(name);
18 | this.eventService.emit(modalOpenedEvent, true);
19 | } else {
20 | // There are currently modals open so just add to the queue
21 | this.modalQueue.push(name);
22 | }
23 | }
24 |
25 | public closeModal(name: string, vfm: VueFinalModalProperty) {
26 | vfm.hide(name);
27 | this.modalQueue = this.modalQueue.filter((modal) => modal !== name);
28 | this.eventService.emit(modalOpenedEvent, false);
29 | if (this.modalQueue.length > 0) {
30 | // There are still modals waiting to be opened, open the next one
31 | vfm.show(this.modalQueue[0]);
32 | this.eventService.emit(modalOpenedEvent, true);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/renderer/services/modpack.service.ts:
--------------------------------------------------------------------------------
1 | import { IpcService } from "@/renderer/services/ipc.service";
2 | import {
3 | IsModpackValidResponse,
4 | MODPACK_EVENTS,
5 | } from "@/main/controllers/modpack/mopack.events";
6 |
7 | export class ModpackService {
8 | constructor(private ipcService: IpcService) {}
9 |
10 | public isModDirectorySet() {
11 | return this.ipcService.invoke(MODPACK_EVENTS.IS_MODPACK_SET);
12 | }
13 |
14 | public getModpackDirectory() {
15 | return this.ipcService.invoke(MODPACK_EVENTS.GET_MODPACK);
16 | }
17 |
18 | public async isModDirectoryValid(modDirectory: string) {
19 | return this.ipcService.invoke(
20 | MODPACK_EVENTS.IS_MODPACK_DIRECTORY_VALID,
21 | modDirectory
22 | );
23 | }
24 |
25 | public async isCurrentModpackValid() {
26 | return this.isModDirectoryValid(await this.getModpackDirectory());
27 | }
28 |
29 | public async deleteModpackDirectory() {
30 | await this.ipcService.invoke(MODPACK_EVENTS.DELETE_MODPACK_DIRECTORY);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/renderer/services/patreon.service.ts:
--------------------------------------------------------------------------------
1 | import { CacheService } from "@/renderer/services/cache.service";
2 |
3 | export class PatreonService {
4 | private patrons: Patron[] = [];
5 | private readonly cacheKey = "patreon.patrons";
6 | private readonly cacheService: CacheService;
7 |
8 | constructor(cacheService: CacheService) {
9 | this.cacheService = cacheService;
10 | }
11 |
12 | /**
13 | * Taken from https://stackoverflow.com/a/12646864/3379536
14 | */
15 | private static shuffleArray(array: Patron[]) {
16 | for (let i = array.length - 1; i > 0; i--) {
17 | const j = Math.floor(Math.random() * (i + 1));
18 | [array[i], array[j]] = [array[j], array[i]];
19 | }
20 | return array;
21 | }
22 |
23 | public async getPatrons(shuffle = true): Promise {
24 | if (this.patrons.length === 0) {
25 | const cache = this.cacheService.get(
26 | this.cacheKey,
27 | 60 * 60 * 24
28 | );
29 | if (cache?.content) {
30 | this.patrons = cache.content;
31 | }
32 | }
33 |
34 | try {
35 | if (this.patrons.length > 0) {
36 | return this.patrons;
37 | }
38 |
39 | const response = await fetch("https://ultsky.phinocio.com/api/patreon");
40 | const data = (await response.json()) as { patrons: Patron[] };
41 | // Shuffle the array to show different Patrons each time
42 | this.patrons = shuffle
43 | ? PatreonService.shuffleArray(data.patrons)
44 | : data.patrons;
45 | this.cacheService.set(this.cacheKey, this.patrons);
46 | return this.patrons;
47 | } catch (error) {
48 | throw new Error(`Failed to get Patrons: ${error}`);
49 | }
50 | }
51 | }
52 |
53 | export interface Patron {
54 | name: string;
55 | tier: "Patron" | "Super Patron";
56 | }
57 |
--------------------------------------------------------------------------------
/src/renderer/services/posts.service.ts:
--------------------------------------------------------------------------------
1 | import { CacheService } from "@/renderer/services/cache.service";
2 | import { logger } from "@/main/logger";
3 |
4 | export class PostsService {
5 | private posts: Post[] | undefined;
6 | private readonly cacheKey = "patreon.posts";
7 | private readonly cacheService: CacheService;
8 | private readonly remoteEndpoint = "https://ultsky.phinocio.com/api";
9 |
10 | constructor(cacheService: CacheService) {
11 | this.cacheService = cacheService;
12 | }
13 |
14 | /**
15 | *
16 | * @param afterFetch - optional method to be called after fetching data from the API. Only called if the cache is used.
17 | */
18 | public async getPosts(afterFetch?: (posts: Post[]) => void) {
19 | logger.info("Getting posts");
20 | // If posts are already available, just return them.
21 | if (this.posts) {
22 | logger.debug("Posts already in memory, returning");
23 | return this.posts;
24 | }
25 |
26 | const cache = this.cacheService.get(this.cacheKey);
27 | if (cache?.content) {
28 | logger.debug(`Posts in cache with age: ${cache.age}`);
29 | this.posts = cache.content;
30 |
31 | // If the cache is outdated, fetch the most recent data but still return the cache
32 | if (cache.age && (await this.checkIfCacheOutdated(cache.age / 1000))) {
33 | logger.debug(
34 | "Cache outdated after checking remote. Fetching up to date posts."
35 | );
36 | new Promise(() => {
37 | this.fetchPosts().then((posts) => {
38 | if (afterFetch) {
39 | logger.debug("Calling optional method after fetching posts");
40 | afterFetch(posts);
41 | logger.debug("Finished calling optional method");
42 | }
43 | });
44 | });
45 | }
46 |
47 | return this.posts;
48 | }
49 |
50 | try {
51 | this.posts = await this.fetchPosts();
52 | return this.posts;
53 | } catch (error) {
54 | throw new Error(`Failed to get News: ${error}`);
55 | }
56 | }
57 |
58 | async fetchPosts() {
59 | const response = await fetch(`${this.remoteEndpoint}/patreon`);
60 | const posts = (await response.json()).posts as Post[];
61 | this.cacheService.set(this.cacheKey, posts);
62 | return posts;
63 | }
64 |
65 | async checkIfCacheOutdated(comparisonTime: number): Promise {
66 | const response = await fetch(`${this.remoteEndpoint}/last-updated`);
67 | const { last_updated: lastUpdated } =
68 | (await response.json()) as LastUpdatedResponse;
69 | return comparisonTime < lastUpdated;
70 | }
71 | }
72 |
73 | export interface Post {
74 | title: string;
75 | content: string;
76 | published: string;
77 | url: string;
78 | tags: string[];
79 | }
80 |
81 | interface LastUpdatedResponse {
82 | last_updated: number;
83 | }
84 |
--------------------------------------------------------------------------------
/src/renderer/services/service-container.ts:
--------------------------------------------------------------------------------
1 | import { App, inject, InjectionKey } from "vue";
2 | import { PatreonService } from "./patreon.service";
3 | import { PostsService } from "@/renderer/services/posts.service";
4 | import { Emitter, EventType } from "mitt";
5 | import { ModalService } from "@/renderer/services/modal.service";
6 | import { MessageService } from "@/renderer/services/message.service";
7 | import { EventService } from "@/renderer/services/event.service";
8 | import { IpcService } from "@/renderer/services/ipc.service";
9 | import { ModpackService } from "@/renderer/services/modpack.service";
10 | import { CacheService } from "@/renderer/services/cache.service";
11 |
12 | export type EventService = Emitter>;
13 |
14 | /*
15 | * IoC container to handle injecting services into Vue components
16 | */
17 |
18 | /**
19 | * Create a new typed binding key
20 | */
21 | export function createBinding(key: string): InjectionKey {
22 | return Symbol(key);
23 | }
24 |
25 | /**
26 | * Available keys to access services from the IoC container
27 | */
28 | export const SERVICE_BINDINGS = {
29 | PATRON_SERVICE: createBinding("keys.services.patron"),
30 | NEWS_SERVICE: createBinding("keys.services.news"),
31 | EVENT_SERVICE: createBinding("keys.services.event"),
32 | MODAL_SERVICE: createBinding("keys.services.modal"),
33 | MESSAGE_SERVICE: createBinding("keys.services.message"),
34 | IPC_SERVICE: createBinding("keys.services.ipc"),
35 | MODPACK_SERVICE: createBinding("keys.services.modpack"),
36 | };
37 |
38 | /**
39 | * Register all services
40 | */
41 | export function registerServices(app: App) {
42 | const ipcService = new IpcService();
43 | const modpackService = new ModpackService(ipcService);
44 | const cacheService = new CacheService();
45 | app.provide(SERVICE_BINDINGS.IPC_SERVICE, ipcService);
46 | app.provide(
47 | SERVICE_BINDINGS.PATRON_SERVICE,
48 | new PatreonService(cacheService)
49 | );
50 | app.provide(SERVICE_BINDINGS.NEWS_SERVICE, new PostsService(cacheService));
51 | app.provide(SERVICE_BINDINGS.MESSAGE_SERVICE, new MessageService(ipcService));
52 | app.provide(SERVICE_BINDINGS.EVENT_SERVICE, EventService);
53 | app.provide(SERVICE_BINDINGS.MODAL_SERVICE, new ModalService(EventService));
54 | app.provide(SERVICE_BINDINGS.MODPACK_SERVICE, modpackService);
55 |
56 | return { ipcService, modpackService };
57 | }
58 |
59 | /**
60 | * Used when it makes sense to error if the injection doesn't exist
61 | * Useful for services as the application likely won't work without these
62 | */
63 | export function injectStrict(key: InjectionKey, fallback?: T) {
64 | const resolved = inject(key, fallback);
65 | if (!resolved) {
66 | throw new Error(`Could not resolve ${key.description}`);
67 | }
68 |
69 | return resolved;
70 | }
71 |
--------------------------------------------------------------------------------
/src/renderer/views/AutoUpdate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Checking for update...
6 |
7 |
8 | There is a new version of the launcher available.
9 |
10 | The application will download an update and restart automatically
11 | before continuing.
12 |
13 |
14 | Download progress {{ downloadProgress }}%
15 |
16 |
17 |
18 |
19 |
20 |
56 |
57 |
64 |
--------------------------------------------------------------------------------
/src/renderer/views/ModDirectory.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 | Note: you should not install modpacks to any of
13 |
17 | these directories .
19 |
20 |
21 |
22 |
23 |
51 |
52 |
65 |
--------------------------------------------------------------------------------
/src/renderer/views/ViewCommunity.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/src/renderer/views/ViewHome.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/shared/enums/userPreferenceKeys.ts:
--------------------------------------------------------------------------------
1 | export enum USER_PREFERENCE_KEYS {
2 | MOD_DIRECTORY = "MOD_DIRECTORY",
3 | PRESET = "PRESET",
4 | GRAPHICS = "GRAPHICS",
5 | ENB_PROFILE = "ENB_PROFILE",
6 | PREVIOUS_ENB_PROFILE = "PREVIOUS_ENB_PROFILE",
7 | RESOLUTION = "RESOLUTION",
8 | SHOW_HIDDEN_PROFILE = "SHOW_HIDDEN_PROFILE",
9 |
10 | CHECK_PREREQUISITES = "CHECK_PREREQUISITES",
11 | }
12 |
--------------------------------------------------------------------------------
/src/shared/util/asyncFilter.ts:
--------------------------------------------------------------------------------
1 | // An async version of array filter
2 | // Taken from: https://stackoverflow.com/a/46842181/3379536
3 | export async function asyncFilter(
4 | arr: T[],
5 | callback: (item: T) => Promise
6 | ) {
7 | const fail = Symbol();
8 | return (
9 | await Promise.all(
10 | arr.map(async (item) => ((await callback(item)) ? item : fail))
11 | )
12 | ).filter((i) => i !== fail);
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/ModOrganizer.ini.d.ts:
--------------------------------------------------------------------------------
1 | import { IIniObject } from "js-ini/lib/interfaces/ini-object";
2 |
3 | export interface ModOrganizerIni extends IIniObject {
4 | General: object;
5 | customExecutables: {
6 | size: number;
7 | [key: string]: unknown;
8 | };
9 | Settings: object;
10 | }
11 |
--------------------------------------------------------------------------------
/src/types/Resolution.d.ts:
--------------------------------------------------------------------------------
1 | export interface Resolution {
2 | height: number;
3 | width: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/types/additional-instructions.d.ts:
--------------------------------------------------------------------------------
1 | interface AdditionalInstructionBase {
2 | // The version the instruction applies to
3 | version?: string;
4 | }
5 |
6 | export type Target = string | string[];
7 |
8 | export interface PluginOrModInstruction extends AdditionalInstructionBase {
9 | type: "enb" | "resolution-ratio";
10 | target: Target;
11 | }
12 |
13 | // Instruction will be run when enb is changed
14 | // The `target` is the enb that the change will be applied on
15 | interface PluginInstruction extends PluginOrModInstruction {
16 | action: "disable-plugin" | "enable-plugin";
17 | plugin: string;
18 | }
19 |
20 | // Instruction will be run when enb is changed
21 | // The `target` is the enb that the change will be applied on
22 | interface ModInstruction extends PluginOrModInstruction {
23 | action: "disable-mod" | "enable-mod";
24 | mod: string;
25 | }
26 |
27 | // A single entry of `disable-ultra-widescreen` is enough for the instruction to return true
28 | interface DisableUltraWidescreenInstruction extends AdditionalInstructionBase {
29 | type?: string;
30 | action: "disable-ultra-widescreen";
31 | }
32 |
33 | export type AdditionalInstruction =
34 | | PluginInstruction
35 | | ModInstruction
36 | | DisableUltraWidescreenInstruction;
37 |
38 | export type AdditionalInstructions = AdditionalInstruction[];
39 |
--------------------------------------------------------------------------------
/src/types/fetch-installed-software.d.ts:
--------------------------------------------------------------------------------
1 | declare module "fetch-installed-software" {
2 | interface Response {
3 | RegistryDirName: string;
4 | DisplayIcon?: string;
5 | DisplayName?: string;
6 | HelpLink?: string;
7 | InstallLocation?: string;
8 | Publisher?: string;
9 | UninstallString?: string;
10 | URLInfoAbout?: string;
11 | NoRepair?: string;
12 | NoModify?: string;
13 | }
14 |
15 | export const getAllInstalledSoftware: () => Promise;
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/modpack-metadata.d.ts:
--------------------------------------------------------------------------------
1 | export interface Modpack {
2 | name: string;
3 | logo: string;
4 | backgroundImage?: string;
5 | website: string;
6 | wiki: string;
7 | patreon: string;
8 | roadmap: string;
9 | }
10 |
11 | export interface FriendlyDirectoryMap {
12 | real: string;
13 | friendly: string;
14 | hidden?: boolean;
15 | }
16 |
--------------------------------------------------------------------------------
/src/types/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module "*.vue" {
3 | import type { DefineComponent } from "vue";
4 | const component: DefineComponent<{}, {}, any>;
5 | export default component;
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/vu3-popper.d.ts:
--------------------------------------------------------------------------------
1 | declare module "vue3-popper" {
2 | import { Component } from "vue";
3 | const file: Component;
4 | export default file;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/wabbajack.d.ts:
--------------------------------------------------------------------------------
1 | export interface WabbajackModpackMetadata {
2 | [key: string]: {
3 | title?: string;
4 | version?: string;
5 | installPath: string;
6 | lastUpdated?: Date;
7 | };
8 | }
9 |
10 | export interface WabbajackV2SettingsFile {
11 | $type: string;
12 |
13 | [key: string]: {
14 | $type: string;
15 | Metadata: unknown;
16 | ModList: {
17 | $type: string;
18 | Archives: Array;
19 | Author: string;
20 | Description: string;
21 | Directives: Array;
22 | GameType: string;
23 | Image: string;
24 | ModManager: 0;
25 | Name: string;
26 | Readme: string;
27 | WabbajackVersion: string;
28 | Website: string;
29 | Version: string;
30 | IsNSFW: boolean;
31 | };
32 | InstallationPath: string;
33 | DownloadPath: string;
34 | WabbajackPath: string;
35 | InstalledAt: string;
36 | };
37 | }
38 |
39 | export interface WabbajackInstallSettings {
40 | ModListLocation: string;
41 | InstallLocation: string;
42 | DownloadLoadction: string;
43 | Metadata?: {
44 | title: string;
45 | description: string;
46 | author: string;
47 | maintainers: string[];
48 | game: string;
49 | official: boolean;
50 | tags: string[];
51 | nsfw: boolean;
52 | utility_list: boolean;
53 | image_contains_title: boolean;
54 | force_down: boolean;
55 | links: {
56 | image: string;
57 | readme: string;
58 | download: string;
59 | machineURL: string;
60 | discordURL: string;
61 | };
62 | download_metadata: {
63 | Hash: string;
64 | Size: number;
65 | NumberOfArchives: number;
66 | SizeOfArchives: number;
67 | NumberOfInstalledFiles: number;
68 | SizeOfInstalledFiles: number;
69 | };
70 | version: string;
71 | dateCreated: string;
72 | dateUpdated: string;
73 | repositoryName: string;
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "module": "CommonJS",
5 | "outDir": "dist",
6 | "strict": true,
7 | "jsx": "preserve",
8 | "importHelpers": true,
9 | "noImplicitAny": true,
10 | "noImplicitReturns": false,
11 | "noImplicitThis": true,
12 | "moduleResolution": "node",
13 | "experimentalDecorators": true,
14 | "emitDecoratorMetadata": true,
15 | "skipLibCheck": true,
16 | "esModuleInterop": true,
17 | "allowSyntheticDefaultImports": true,
18 | "sourceMap": true,
19 | "resolveJsonModule": true,
20 | "baseUrl": ".",
21 | "types": [
22 | "webpack-env",
23 | "mocha"
24 | ],
25 | "paths": {
26 | "@/*": [
27 | "src/*",
28 | "src/types/*"
29 | ]
30 | },
31 | "lib": [
32 | "esnext",
33 | "dom",
34 | "dom.iterable",
35 | "scripthost"
36 | ]
37 | },
38 | "include": [
39 | "src/**/*.ts",
40 | "src/**/*.tsx",
41 | "src/**/*.vue"
42 | ],
43 | "exclude": [
44 | "node_modules"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const dependencies = require("./package.json").dependencies;
2 |
3 | module.exports = {
4 | configureWebpack: {
5 | externals: {
6 | electron: "window",
7 | "@/main/logger": "window",
8 | },
9 | },
10 | pluginOptions: {
11 | electronBuilder: {
12 | nodeIntegration: false,
13 | externals: Object.keys(dependencies),
14 | bundleMainProcess: false,
15 | mainProcessFile: "dist/main.js",
16 | rendererProcessFile: "src/renderer/index.ts",
17 | outputDir: "dist",
18 | builderOptions: {
19 | productName: "Wildlander Launcher",
20 | icon: "public/images/logos/wildlander-icon-light.png",
21 | publish: {
22 | provider: "github",
23 | releaseType: "release",
24 | },
25 | extraMetadata: {
26 | name: "Wildlander Launcher",
27 | },
28 | files: [
29 | "**",
30 | {
31 | from: "../main",
32 | to: "./main",
33 | filter: ["!*.map*"],
34 | },
35 | {
36 | from: "../shared",
37 | to: "./shared",
38 | filter: ["!*.map*"],
39 | },
40 | {
41 | from: "../",
42 | to: ".",
43 | filter: [
44 | "preload.*",
45 | "modpack.json",
46 | "additional-instructions.json",
47 | ],
48 | },
49 | ],
50 | extraResources: [
51 | {
52 | from: "./src/assets/tools",
53 | to: "tools",
54 | filter: ["**/*"],
55 | },
56 | ],
57 | nsis: {
58 | deleteAppDataOnUninstall: true,
59 | },
60 | },
61 | },
62 | },
63 | };
64 |
--------------------------------------------------------------------------------
/wallaby.js:
--------------------------------------------------------------------------------
1 | module.exports = function (w) {
2 | return {
3 | files: [
4 | "src/**/*.ts",
5 | "src/**/*.js",
6 | "src/**/*.json",
7 | "!src/__tests__/unit/**/*.ts",
8 | "tsconfig.json",
9 | ],
10 | tests: ["src/__tests__/unit/**/*.ts"],
11 |
12 | testFramework: "mocha",
13 |
14 | compilers: {
15 | "**/*.ts": w.compilers.typeScript({
16 | noResolve: false,
17 | }),
18 | },
19 |
20 | env: {
21 | type: "node",
22 | },
23 |
24 | setup: function () {
25 | // Enable TypeScript aliases
26 | if (global._tsconfigPathsRegistered) return;
27 | // eslint-disable-next-line @typescript-eslint/no-var-requires
28 | const tsConfigPaths = require("tsconfig-paths");
29 | // eslint-disable-next-line @typescript-eslint/no-var-requires
30 | const tsconfig = require("./tsconfig.json");
31 | tsConfigPaths.register({
32 | baseUrl: tsconfig.compilerOptions.baseUrl,
33 | paths: tsconfig.compilerOptions.paths,
34 | });
35 | global._tsconfigPathsRegistered = true;
36 |
37 | // Ensure MockFs has fully reset before starting
38 | require("mock-fs").restore();
39 | },
40 | };
41 | };
42 |
--------------------------------------------------------------------------------