Skip to content

Commit

Permalink
Merge pull request #311 from GiovanH/mspfa
Browse files Browse the repository at this point in the history
MSPFA Feature Support working draft
  • Loading branch information
GiovanH authored Mar 21, 2024
2 parents ee64d69 + 152e864 commit 2f160a1
Show file tree
Hide file tree
Showing 17 changed files with 1,918 additions and 19 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ pnpm-debug.log*
install

# generated file
src/imods.tar
src/imods.tar

/tools/mspfa/*/
/tools/mspfa/*.log
39 changes: 37 additions & 2 deletions MODDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ Some changes don't require any sort of reload at all. Some require a soft reload

Basically, anything that requires the main process to reload requires an application restart. This is usually if you change an actual file in the mods directory. Anything that modifies vue or adds CSS requires a soft reload, and stuff that just modifies the archive or adds footnotes can reload within vue.

### MSPFA

The archive has first-class support for MSPFA-styled fan adventures.

Make mods that are ready for zipping distribution using the tools/mspfa.py script, provided.

There is currently no way to make a "live-updating" MSPFA; fan adventures are snapshotted, so this is best used for complete fan adventures.

## API specification

As per [Installing mods](#installing-mods) above, there are two forms of mods: single-file scripts and mod folders.
Expand Down Expand Up @@ -343,6 +351,26 @@ Aside: Internally, there is no such thing as a `FootnoteScope`. Instead the pars

### `computed`

<aside>
**New with 2.4**: You can also define an async `asyncComputed` which has access to the more efficient readFileAsync variant functions.

For example, as seen in mods:
```js
async asyncComputed(api) {
const story = await api.readYamlAsync("./story.yaml")
return {
styles: [
{body: await api.readFileAsync("./adventure.scss")}
],
edit(archive){
archive.mspfa[story_id] = story
}
}
}
```

</aside>

There are some resources your mod might want to request from TUHC at runtime, like a namespaced logger object or access to a settings store. For this, use the `computed` function.

(There is no relation between the `module.exports.computed` field and the vue conception of computed values, except for the general idea of computation.)
Expand All @@ -353,8 +381,15 @@ The `computed` function is passed the `api` object as an argument, which current

```js
api = {
store,
logger,
store
readFile(asset_path),
readJson(asset_path),
readYaml(asset_path),
async readFileAsync(asset_path, callback),
async readJsonAsync(asset_path, callback),
async readYamlAsync(asset_path, callback),
Resources
}
```

Expand All @@ -366,6 +401,7 @@ The `store` object is a special namespaced store you can use for reading setting
- `get(k, default_)`: Get the value of key `k`, or `default_` if `k` is not yet set.
- `has(k)`
- `delete(k)`
- `onDidChange(key, callback)`
- `clear()`

The store provided is namespaced. This means it is safe to use commonly used keys in your mod without any risk of conflicting with the main program or other mods.
Expand Down Expand Up @@ -668,4 +704,3 @@ and Mod B can check
`archive.flags['MOD_A_LOADED']`
Note that there is no special namespacing done on these flags; any mod can theoretically read and write to any flag at any time. Also note that in the above example, Mod A must be loaded before Mod B in order to recognize its presence. This is intentional behavior.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"engines": {
"node": ">= 14.18"
},
"version": "2.4.4",
"version": "2.5.0",
"license": "GPL-3.0",
"repository": "github:Bambosh/unofficial-homestuck-collection",
"private": true,
Expand Down
6 changes: 2 additions & 4 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-assembler'
import fs from 'fs'

import yaml from 'js-yaml'

import Resources from "./resources.js"
import Mods from "./mods.js"

const { nativeImage } = require('electron');
const APP_VERSION = app.getVersion()
Expand Down Expand Up @@ -231,7 +228,8 @@ async function loadArchiveData(){
extras: JSON.parse(fs.readFileSync(path.join(assetDir, 'archive/data/extras.json'), 'utf8')),
tweaks: JSON.parse(fs.readFileSync(path.join(assetDir, 'archive/data/tweaks.json'), 'utf8')),
audioData: {},
flags: {}
flags: {},
mspfa: {}
}
} catch (e) {
// Error loading json. Probably a bad asset pack installation.
Expand Down
80 changes: 80 additions & 0 deletions src/components/CustomContent/MSPFADisambig.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script>
import MSPFAPage from '@/components/CustomContent/MSPFAPage.vue'
import MSPFALog from '@/components/CustomContent/MSPFALog.vue'
import MSPFAIndex from '@/components/CustomContent/MSPFAIndex.vue'
function resolveStory(ctx, input) {
const archive = (ctx?.$archive || ctx.parent.$archive)
if (input in archive.mspfa) {
return input
} else {
// Resolve numerical ID
const query = Object.entries(archive.mspfa).filter(t => t[1].i == input)
if (query.length > 0) {
// If you have the same adventure installed twice, we're picking one. Sorry!
return query[0][0]
} else {
throw Error(`MSPFA with id ${input} not loaded!`)
}
}
}
export default {
name: 'MSPFADisambig',
props: [
'tab', 'routeParams'
],
components: {
MSPFALog, MSPFAPage, MSPFAIndex
},
theme: () => "mspfa",
title: function(ctx) {
if (!ctx.routeParams.story)
return 'MSPFA'
else {
const story_id = resolveStory(ctx, ctx.routeParams.story)
const comic = ctx.$archive.mspfa[story_id].n
if (ctx.routeParams.p == 'log') {
return `Adventure Log - ${comic}`
} else {
const command = ctx.$archive.mspfa[story_id].p[Number(ctx.routeParams.p - 1)].c
return command ? `${command} - ${comic}` : comic
}
}
},
functional: true,
render: function (h, ctx) {
const options = ctx // pass through everything
// some special changes here
options['class'] = ctx.data.class
options['ref'] = ctx.data.ref
// compute props globally, yolo
if (ctx.props.routeParams.story) {
options.props['storyId'] = resolveStory(ctx, ctx.props.routeParams.story)
options.props['pageNum'] = Number(ctx.props.routeParams.p)
}
// these will be invalid values sometimes but only on pages that don't use them
// {
// props: {
// tab: ctx.props.tab
// },
// data: {
// ref: ctx.data.ref,
// },
// class: ctx.data.class,
// ref: ctx.data.ref
// }
if (!ctx.props.routeParams.story)
return h(MSPFAIndex, options)
else if (ctx.props.routeParams.p == 'log') {
return h(MSPFALog, options)
} else if (ctx.props.routeParams.p && ctx.props.routeParams.story) {
return h(MSPFAPage, options)
} else return h("pre", {
text: ctx.props.routeParams
})
}
}
</script>
112 changes: 112 additions & 0 deletions src/components/CustomContent/MSPFAIndex.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<template>
<GenericPage>
<h2 class="pageTitle">MS Paint Fan Adventures</h2>
<div class="adventureLinks">
<div class="adventure" v-for="advlink in mspfaLinks" :key="advlink.href">
<a :href="advlink.href" class="icon">
<MediaEmbed :url="advlink.img" />
<span v-text="advlink.label" />
<span class="date">{{ datestr(advlink) }}</span>
<span class="date">{{ advlink.pageCount }} pages</span>
</a>
</div>
</div>
</GenericPage>
</template>

<script>
import MediaEmbed from '@/components/UIElements/MediaEmbed.vue'
import GenericPage from '@/components/UIElements/GenericPage.vue'
const DateTime = require('luxon').DateTime
export default {
name: 'MSPFALog',
props: [
],
components: {
GenericPage, MediaEmbed
},
data: function() {
return { }
},
computed: {
mspfaLinks(){
return (Object.keys(this.$archive.mspfa) || []).map(k => {
const story = this.$archive.mspfa[k]
return {
href: `/mspfa/${k}/`,
img: story.o || "assets://images/mspalogo_mspa.png",
label: story.n,
startDate: DateTime.fromMillis(story.d).setZone("America/New_York"),
updatedDate: DateTime.fromMillis(story.u).setZone("America/New_York"),
status: ['Inactive', 'Ongoing', 'Complete'][story.h-1],
pageCount: story.p.length
}
})
}
},
methods: {
datestr(advlink){
const start_str = advlink.startDate.toFormat('LLL yyyy')
const updated_str = (advlink.status == 'Ongoing') ? '' : advlink.updatedDate.toFormat('LLL yyyy')
if (start_str == updated_str) {
return start_str
} else {
return `${start_str} - ${updated_str}`
}
}
}
}
</script>

<style scoped lang="scss">
a.icon {
text-align: center;
text-decoration: none;
> span {
display: block;
text-decoration: underline;
}
.date {
padding-top: 5px;
font-family: Verdana, Arial, Helvetica, sans-serif;
font-weight: normal;
color: var(--page-nav-meta);
font-size: 12px;
text-decoration: none;
}
}
h2.pageTitle {
max-width: 590px;
text-align: center;
line-height: 1.1;
font-size: 32px;
padding: 15px 0;
margin: 0 auto;
}
.adventureLinks {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
margin: 0 auto;
width: 600px;
.adventure {
margin-bottom: 20px;
text-align: center;
line-height: 1.1;
font-size: 18px;
width: 200px;
img {
object-fit: contain;
width: 164px;
height: 164px;
display: inline-block;
}
}
}
</style>

Loading

0 comments on commit 2f160a1

Please sign in to comment.