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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes will land here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow
[SemVer](https://semver.org/spec/v2.0.0.html).

## [0.1.16] - Delays up to 1 hour, Twitch events and alerts, smart lights, and two InstantClones side by side
## [0.1.16] - Delays up to 1 hour, Twitch events and alerts, smart lights, chat games and points, and two InstantClones side by side

### Delays up to 1 hour

Expand Down Expand Up @@ -291,6 +291,74 @@ dock's destination list says "Can't connect", with the reason on hover.
seconds, and Shelly and WLED are only ever asked directly: an answer
pointing somewhere else counts as a no.

### Chat games and points

What a chat economy needs, as steps you can build anything else with too.

- **Viewer value** keeps a number per viewer under a name you pick (coins,
points), between streams. It gives, takes, sets, or **spends**: taken only
when the viewer has enough, with `{paid}` saying yes or no. It can also pay
**everyone who chatted lately**, or a viewer the step names. Read it
anywhere as `{viewer.coins}` or `{target.coins}`, add `.rank` for their
place, and `{top.coins}` lists the five who hold the most. The step's
editor shows who holds the most and lets you fix a viewer by hand.
- **Maths** adds, subtracts, multiplies, divides, takes a percent, the
smaller or bigger of two numbers, or a random number between them.
- **A "% chance" check** in If: `25` passes one time in four, `0.1` one in
a thousand.
- **Wait for chat** pauses a run until someone types something (anyone, the
viewer, or who they named), up to 5 minutes, and hands on who answered.
Duels, "first to type !grab" and turn-based games need nothing else.
- **Blocks**: a new kind of trigger that never starts on its own. A **Run a
block** step runs its steps by name with the run's values, so a check
written once serves every command. A block can run itself (a turn that
repeats), 8 deep at most.
- **Remember a value** can keep its text between runs and streams, read
back as `{saved.name}`.
- **A chat command can answer a viewer who is too early**, with
`{cooldown_left}`, once per wait. Cooldowns of 10 minutes or more now
survive a restart, so a daily bonus stays daily.
- **Stop** can leave its run out of the history, for a timer that rolls for
a rare drop every minute.
- **Your values** (a button on the Integrations tab): things you set once
and any message or step reads as `{my.name}`, like what a point is
called, how often a game is won or a list to pick from. Change one there
and everything that uses it follows. Each value shows what it is for,
where it is used, and one field made for it: a slider for a chance, a
box with one entry per line for a list. Its name and label sit behind
the pencil. A second tab shows what your viewers hold, where you can set
one viewer's amount or start everyone over. In a counting word, write a
value with a `$`: `{viewer.coins#$my.coin#$my.coins}` says "1 gem" or
"5 gems" in your words. A recipe brings the values it uses; one you
already set stays as you set it, and the import preview says so.
- **Channel points rewards, made and managed here.** A "Someone redeems
channel points" trigger now has a list to tick which rewards start it
(or any reward), and under it your rewards: a switch for each, edit
(name, cost, description, whether the viewer types a message) and
delete, plus **New reward**, all live on your channel without opening
Twitch. Renaming a reward there updates the integration you are editing.
A **Channel points reward** step switches one on or off during a run, so
a reward is only there when it makes sense. Twitch only lets an app
change rewards it made itself, and channel points need an Affiliate or
Partner channel. If you logged in to Twitch before this build, log out
and in again once under Connections to allow it; hearing redemptions
keeps working either way.
- **A chat message trigger can match whole words.** "Has the word" with
`nt` fires on "nt!" but not on "want" or "don't". New triggers start with
it; ones you already have keep "Contains these letters". Under the
choice, the editor says what it means and marks a few messages made from
your own text as firing or not, and **Try a message** tells you for any
message you type.
- **A test run starts from the values you give it**: `counter.wins`,
`viewer.coins` and `target.coins` set what the test reads, and what it
counts or pays stays in the test. Nothing saved changes.
- A counter is now read as it is at that moment, not as an earlier step of
the same run left it, so a run that waited sees what changed meanwhile.
The Counter step can also save its value under a fixed name.
- Twitch events (follow, sub, cheer, raid, channel points) bring
`{user_login}`, and chat commands `{target_login}`, so viewer values work
from them.

### Integrations

- **Why a run failed:** the editor's "Last ran · failed" and every Recent
Expand Down
42 changes: 42 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ pub struct Settings {
/// Integrations (alerts, chat commands, automations). Each is saved as
/// one `integration.<id>=<json>` line. See `crate::integrations`.
pub integrations: Vec<Integration>,
/// Values the streamer sets once for every integration (`{my.name}`).
pub user_values: Vec<crate::integrations::model::UserValue>,
/// Discord webhooks the integrations can post to.
pub discord_channels: Vec<DiscordChannel>,
/// Phone pushes (ntfy).
Expand Down Expand Up @@ -801,6 +803,18 @@ impl Settings {
let look = self.alerts_look.to_json().to_json();
writeln!(f, "alerts.look={}", one_line(&look))?;
}
for v in &self.user_values {
let kept = json::obj([
("label", json::str(&v.label)),
("value", json::str(&v.value)),
]);
writeln!(
f,
"integration_value.{}={}",
one_line(&v.name),
one_line(&kept.to_json())
)?;
}
for i in &self.integrations {
writeln!(
f,
Expand All @@ -815,6 +829,25 @@ impl Settings {
/// Load one integration line. A line that no longer parses (a hand edit,
/// a newer version's step type) is skipped rather than failing the
/// whole config: the other integrations and settings still load.
/// One `integration_value.<name>=<json>` line. A broken or repeated
/// one is skipped, like a broken integration.
fn load_user_value(&mut self, name: &str, value: &str) {
use crate::integrations::model::{UserValue, MAX_USER_VALUES};
let Ok(mut parsed) = json::parse(value) else {
return;
};
if let json::Value::Obj(fields) = &mut parsed {
fields.push(("name".to_string(), json::str(name)));
}
let Some(kept) = UserValue::from_json(&parsed) else {
return;
};
let known = self.user_values.iter().any(|v| v.name == kept.name);
if !known && self.user_values.len() < MAX_USER_VALUES {
self.user_values.push(kept);
}
}

fn load_integration(&mut self, value: &str) {
if self.integrations.len() >= MAX_INTEGRATIONS {
return;
Expand Down Expand Up @@ -963,6 +996,7 @@ impl Settings {
midi_device: String::new(),
crash_protection: CrashProtection::default(),
integrations: Vec::new(),
user_values: Vec::new(),
discord_channels: Vec::new(),
phone: PhoneConnection::default(),
alerts_look: Default::default(),
Expand Down Expand Up @@ -1475,6 +1509,9 @@ impl Settings {
}
"integrations_migrated" => self.integrations_migrated = value == "true",
k if k.starts_with("integration.") => self.load_integration(value),
k if k.starts_with("integration_value.") => {
self.load_user_value(&k["integration_value.".len()..], value)
}
k if k.starts_with("connection.discord.") => {
self.load_discord_channel(&k["connection.discord.".len()..], value)
}
Expand Down Expand Up @@ -3978,6 +4015,11 @@ Name@x"
.unwrap(),
crate::integrations::presets::build("mod_controls", "mods2".into(), "").unwrap(),
],
user_values: vec![crate::integrations::model::UserValue {
name: "coins".into(),
label: "What coins = called".into(),
value: "gems | \"shiny\"".into(),
}],
discord_channels: vec![DiscordChannel {
id: "mods".into(),
name: "Mods = team".into(),
Expand Down
Loading
Loading