Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/0dot5'
Browse files Browse the repository at this point in the history
  • Loading branch information
LordTocs committed Jun 24, 2024
2 parents c6ba7c1 + 736966b commit 7f44911
Show file tree
Hide file tree
Showing 27 changed files with 258 additions and 130 deletions.
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
user/
user_old/
user_test/
user_premigrate/
user*/
/dist
/release
dist-electron/
Expand Down
24 changes: 7 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
# CastMate

CastMate is an all-in-one Broadcaster Automation Suite for Twitch. It allows you to build compelling interactive stream setups using without writing any code. It features simple click and drag automations and overlays.
CastMate is an all-in-one Broadcaster Production Suite for Twitch. It lets a streamer create viewer interactions, automate common tasks, display overlays, and plan streams.

## Triggers

In CastMate you can setup triggers to respond to twitch events like channel point redemptions, bits being cheered, gaining a follower, receiving a sub, chat messages, being raided, hype trains, and more. In response to a trigger you can setup any number of actions using the click and drag UI.
In CastMate you can setup triggers to respond to twitch events like channel point redemptions, bits being cheered, gaining a follower, receiving a sub, chat messages, being raided, hype trains, and more.

![CastMate UI Automations](docs/images/automation.png?raw=true)
You can use actions to automatically respond to triggers. Using CastMate's click and drag timeline automation system you can easily setup sounds to play, lights to change, chat to twitch, message to discord, etc. Using triggers lets you easily create an interactive stream for your audience.

![CastMate UI Triggers](docs/images/trigger.png?raw=true)

## Profiles

A profile is meant to organize triggers together based on context. For example, a Minecraft profile might contain a set of commands which are only available to viewers when the streamer is playing Minecraft, or a "Stream Starting" profile might only be active when a specific scene is active in OBS. Multiple profiles can be active at once.
A Profile is an organizational tool to group triggers together. They also serve as a way to change what triggers are active. Profiles can be manually turned on and off, or they can be set to automatically turn on and off based custom conditions you choose.

![CastMate UI Profiles](docs/images/profile.png?raw=true)

Profiles can be activated automatically based on conditions such as the number of viewers, the currently active OBS scene, the value of a custom variable, various states within CastMate itself, and much more. Multiple conditions can be combined together using logical expressions like AND, OR, EQUAL, GREATER THAN, etc. If a channel point reward isn't used by any active profile they are automatically disabled and dissappear from view.
When a profile is off it will automatically disable the channel point rewards no longer needed, any chat commands in it will become unavailable, and

![CastMate UI Profiles](docs/images/ChannelPointRewards.gif?raw=true)

The possibilities are endless, so here are some ideas to get you started:

- Create alternate versions of a channel point redemption that costs more to redeem when the number of viewers is higher, to encourage engagement during slow streams and to reduce spam during active streams.
- Allow text-to-speech with channel points, but only if the stream has less than some number of viewers.
- A "Stream Ending" profile which automatically starts sending social media links to chat while your "End Stream" scene is active in OBS.
- Automatically run ads whenever your "Be Right Back" scene is active.
- Have a jump scare sound effect that automatically costs triple during specific stream segments.
- Let viewers troll you with a command that plays different sounds based on the game, like enemy footsteps during a shooter or creepers during minecraft.
- Make a song request channel point redemption that's only available during a Music stream segment.
- Set up a "First" channel point redemption item that can only be claimed once per stream.
- Set up a channel point redemption that is only visible when there are exactly 69 viewers. Nice.

## Overlays

CastMate has a WYSIWYG interface for creating overlays. Create custom alerts entirely though the UI. Sync labels with CastMate's internal state automatically. Easily edit your overlays right in the UI and have their results appear immediately once saved in OBS.
Expand Down
Binary file modified docs/images/SpellCast.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/WYSIWYG.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/Wheel.gif
Binary file not shown.
Binary file removed docs/images/activation.png
Binary file not shown.
Binary file removed docs/images/automation.png
Binary file not shown.
Binary file modified docs/images/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/trigger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 34 additions & 11 deletions libs/castmate-core/src/pubsub/pubsub-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { EventList } from "../util/events"
import { onLoad, onUnload } from "../plugins/plugin"
import { initingPlugin } from "../plugins/plugin-init"
import { ReactiveEffect, autoRerun } from "../reactivity/reactivity"
import { coreAxios } from "../util/request-utils"

const logger = usePluginLogger("pubsub")

Expand All @@ -27,6 +26,7 @@ export const PubSubManager = Service(
private onMessage = new EventList<(plugin: string, event: string, context: object) => any>()

private onConnect = new EventList()
private onBeforeDisconnect = new EventList()

constructor() {}

Expand Down Expand Up @@ -54,11 +54,16 @@ export const PubSubManager = Service(
this.disconnect()
}

private disconnect() {
private async disconnect() {
if (this.azSocket) {
try {
await this.onBeforeDisconnect.run()
} catch {}

const socket = this.azSocket
logger.log("Disconnecting from Cloud PubSub")
this.azSocket?.stop()
this.azSocket = undefined
socket?.stop()
}

this.connected = false
Expand Down Expand Up @@ -88,7 +93,7 @@ export const PubSubManager = Service(
logger.log("Starting Cloud PubSub Connection")
this.connecting = true

const negotiationResp = await coreAxios.get("/pubsub/negotiate", {
const negotiationResp = await axios.get("/pubsub/negotiate", {
baseURL,
headers: {
Authorization: `Bearer ${this.token}`,
Expand Down Expand Up @@ -121,11 +126,11 @@ export const PubSubManager = Service(

const messageData = ev.message.data as {
plugin: string
message: string
event: string
context: object
}

this.onMessage.run(messageData.plugin, messageData.message, messageData.context)
this.onMessage.run(messageData.plugin, messageData.event, messageData.context)
})

this.azSocket.on("connected", async (ev) => {
Expand All @@ -135,12 +140,9 @@ export const PubSubManager = Service(
})

this.azSocket.on("disconnected", (ev) => {
logger.error("Lost Connection to CastMate Pubsub", ev.message)

//ON DISCONNECT

this.connected = false
this.connecting = false
logger.error("Lost Connection to CastMate Pubsub", ev.message)
})

await this.azSocket.start()
Expand All @@ -160,6 +162,7 @@ export const PubSubManager = Service(
}

try {
//logger.log("Sending", eventName, this.connected, this.connecting)
await this.azSocket.sendEvent(eventName, data, "json")
} catch (err) {
logger.error("Cloud PubSub Error", err)
Expand Down Expand Up @@ -193,6 +196,14 @@ export const PubSubManager = Service(
unregisterOnConnect(func: () => any) {
this.onConnect.unregister(func)
}

registerOnBeforeDisconnect(func: () => any) {
this.onBeforeDisconnect.register(func)
}

unregisterOnBeforeDisconnect(func: () => any) {
this.onBeforeDisconnect.unregister(func)
}
}
)

Expand Down Expand Up @@ -228,11 +239,13 @@ export function onCloudPubSubMessage<T extends object>(
if (activeFunc()) {
if (!registered) {
registered = true
//logger.log("Registering", pluginId, eventName)
PubSubManager.getInstance().registerOnMessage(handler)
}
} else {
if (registered) {
registered = false
//logger.log("Unregistering", pluginId, eventName)
PubSubManager.getInstance().unregisterOnMessage(handler)
}
}
Expand All @@ -258,9 +271,19 @@ export function onCloudPubSubConnect(func: () => any) {
})
}

export function onCloudPubSubBeforeDisconnect(func: () => any) {
onLoad(() => {
PubSubManager.getInstance().registerOnBeforeDisconnect(func)
})

onUnload(() => {
PubSubManager.getInstance().unregisterOnBeforeDisconnect(func)
})
}

export function useSendCloudPubSubMessage<T extends object>(eventName: string) {
if (!initingPlugin) throw new Error()
const pluginId = initingPlugin.name
const pluginId = initingPlugin.id

return async (data: T) => {
return await PubSubManager.getInstance().send(pluginId, eventName, data)
Expand Down
1 change: 0 additions & 1 deletion libs/castmate-core/src/reactivity/reactivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export namespace DependencyStorage {
if (key == "constructor") return undefined
const alias = aliasMap.get(target)
if (!alias) return undefined
logger.log("Alias Obj", alias)
return alias[key]
}

Expand Down
8 changes: 7 additions & 1 deletion libs/castmate-core/src/util/events.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { globalLogger } from "../logging/logging"

export class EventList<TFunc extends (...args: any[]) => any = () => any> {
private list: TFunc[] = []

Expand All @@ -23,7 +25,11 @@ export class EventList<TFunc extends (...args: any[]) => any = () => any> {
//const promises = this.list.map((f) => f(...args))
//await Promise.all(promises)
for (const f of this.list) {
await f(...args)
try {
await f(...args)
} catch (err) {
globalLogger.error("ERROR w EVENT LIST:", err)
}
}
}
}
3 changes: 2 additions & 1 deletion packages/castmate/src/main/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ function quit() {
}

function createMainWindow() {
const win = createWindow("index.html", 1600, 900)
const portable = process.env.PORTABLE_EXECUTABLE_FILE != null || process.argv.includes("--portable")
const win = createWindow("index.html", 1600, 900, { portable: String(portable) })

win.on("close", () => {
//Workaround for electron bug.
Expand Down
8 changes: 6 additions & 2 deletions packages/castmate/src/main/electron/electron-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ export function createWindow(
if (process.env.VITE_DEV_SERVER_URL) {
//get it from vite
const url = path.posix.join(process.env.VITE_DEV_SERVER_URL, "html", htmlFile)
win.loadURL(url) //TODO: FORMAT URL WITH QUERY
const params = new URLSearchParams(urlQuery)
win.loadURL(`${url}?${params}`)
} else {
win.loadFile(path.join(__dirname, `../../dist/html/${htmlFile}`))
const url = path.join(__dirname, `../../dist/html/${htmlFile}`)
win.loadFile(url, {
query: urlQuery,
})
}

win.addListener("maximize", () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/castmate/src/main/migration/old-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,10 @@ async function migrateOldAutomation(oldAutomation: OldAutomation): Promise<Seque
actions: [],
}

if (!oldAutomation) {
return result
}

const seqStack: SequenceStackItem[] = [{ sequence: result, offset: 0 }]

function canFit(pushSeq: SequenceStackItem) {
Expand Down Expand Up @@ -2158,8 +2162,8 @@ async function migrateOldProfile(name: string, oldProfile: OldProfile): Promise<

newTrigger.sequence = await migrateInlineOldAutomation(oldTrigger.automation)

if (typeof oldTrigger.automation == "object") {
if (oldTrigger.automation.sync) {
if (typeof oldTrigger?.automation == "object") {
if (oldTrigger?.automation?.sync) {
newTrigger.queue = mainQueueId
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/castmate/src/renderer/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ const isFirstTimeStartup = useIpcCaller<() => boolean>("info", "isFirstTimeStart
const hasUpdate = useIpcCaller<() => boolean>("info", "hasUpdate")
onMounted(async () => {
const queryString = window.location.search
const urlParams = new URLSearchParams(queryString)
const isPortable = urlParams.get("portable")
if (isPortable == "true") {
document.title = "CastMate - Portable"
}
let migrated = false
if (await needsMigrate()) {
migrated = true
Expand Down
8 changes: 4 additions & 4 deletions plugins/spellcast/main/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function apiGet<T = any>(url: string) {

const token = TwitchAccount.channel.secrets.accessToken

return coreAxios.get<T>(url, {
return axios.get<T>(url, {
baseURL,
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -23,7 +23,7 @@ function apiPost<T = any>(url: string, data: any) {

const token = TwitchAccount.channel.secrets.accessToken

return coreAxios.post<T>(url, data, {
return axios.post<T>(url, data, {
baseURL,
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -36,7 +36,7 @@ function apiPut<T = any>(url: string, data: any) {

const token = TwitchAccount.channel.secrets.accessToken

return coreAxios.put<T>(url, data, {
return axios.put<T>(url, data, {
baseURL,
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -49,7 +49,7 @@ function apiDel<T = any>(url: string) {

const token = TwitchAccount.channel.secrets.accessToken

return coreAxios.delete<T>(url, {
return axios.delete<T>(url, {
baseURL,
headers: {
Authorization: `Bearer ${token}`,
Expand Down
Loading

0 comments on commit 7f44911

Please sign in to comment.