1use crate::{Channel, Message, Platform};
7
8struct Rng(u64);
9impl Rng {
10 fn next(&mut self) -> u64 {
11 let mut x = self.0;
12 x ^= x << 13;
13 x ^= x >> 7;
14 x ^= x << 17;
15 self.0 = x;
16 x
17 }
18 fn below(&mut self, n: u64) -> u64 {
19 self.next() % n.max(1)
20 }
21 fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
22 &xs[self.below(xs.len() as u64) as usize]
23 }
24 fn unit(&mut self) -> f64 {
25 (self.next() >> 11) as f64 / (1u64 << 53) as f64
26 }
27}
28
29const USERS: &[&str] = &[
30 "forsen",
31 "nymn",
32 "pajlada",
33 "supibot",
34 "mel",
35 "kkona",
36 "wide",
37 "peeposit",
38 "gigachad",
39 "monkas",
40 "widepeepo",
41 "hasanabi",
42 "vedal",
43 "neuro",
44 "anon",
45];
46
47const WORDS: &[&str] = &[
48 "GAMBA",
49 "OMEGALUL",
50 "Sadge",
51 "PepeLaugh",
52 "gigachad moment",
53 "actual clip",
54 "no way",
55 "chat is this real",
56 "COPIUM",
57 "based",
58 "ratio",
59 "Nerd",
60 "KEKW",
61 "this is peak",
62 "mods asleep",
63 "PogU",
64 "sptvJAM",
65 "Aware",
66 "Kappa RainTime w!",
68 "KEKW ffzW ffzRainbow",
69 "w!h!ffzX KEKW",
70 "GAMBA notL ffzSpin",
71 "c!#0f0 PogU ffzBounce",
72];
73
74struct Seed {
76 name: &'static str,
77 platform: Platform,
78 activity: f64,
79}
80
81const SEEDS: &[Seed] = &[
82 Seed {
83 name: "xqc",
84 platform: Platform::Twitch,
85 activity: 7.0,
86 },
87 Seed {
88 name: "forsen",
89 platform: Platform::Twitch,
90 activity: 2.5,
91 },
92 Seed {
93 name: "hasanabi",
94 platform: Platform::Twitch,
95 activity: 1.0,
96 },
97 Seed {
98 name: "trainwreckstv",
99 platform: Platform::Kick,
100 activity: 11.0,
101 },
102 Seed {
103 name: "adin",
104 platform: Platform::Kick,
105 activity: 0.4,
106 },
107];
108
109pub fn channels() -> Vec<Channel> {
111 SEEDS
112 .iter()
113 .map(|s| Channel::new(s.name, s.platform, 200))
114 .collect()
115}
116
117pub struct Driver {
119 rng: Rng,
120 clock_ms: u64,
121 activity: Vec<f64>,
122}
123
124impl Default for Driver {
125 fn default() -> Driver {
126 Driver::new()
127 }
128}
129
130impl Driver {
131 pub fn new() -> Driver {
132 Driver {
133 rng: Rng(0x9E3779B97F4A7C15),
134 clock_ms: 1,
135 activity: SEEDS.iter().map(|s| s.activity).collect(),
136 }
137 }
138
139 pub fn tick(&mut self, channels: &mut [Channel]) {
142 self.clock_ms += 350;
143 let now = self.clock_ms;
144 for (i, ch) in channels.iter_mut().enumerate() {
145 ch.cool(now);
146 let spike = self.rng.unit() < 0.03;
147 let mult = if spike { 6.0 } else { 1.0 };
148 let mut budget = self.activity.get(i).copied().unwrap_or(1.0) * mult;
149 if self.rng.unit() < 0.008 {
151 let user = self.rng.pick(USERS).to_string();
152 let (kind, what, text) = match (self.rng.unit() * 8.0) as u32 {
153 0 => (
154 crate::NoteKind::Sub,
155 "subscribed at Tier 1. They've subscribed for 14 months!",
156 "still here Pog",
157 ),
158 1 => (crate::NoteKind::Gift, "is gifting 5 Tier 1 Subs!", ""),
159 2 => (crate::NoteKind::Raid, "12 raiders have joined!", ""),
160 3 => (crate::NoteKind::Redeem, "redeemed Hydrate (500)", ""),
161 4 => (crate::NoteKind::Live, "went live — Just Chatting", ""),
162 5 => (crate::NoteKind::Category, "switched to Elden Ring", ""),
163 6 => (crate::NoteKind::Offline, "went offline", ""),
164 _ => (crate::NoteKind::Cheer, "cheered 100 bits", "gg"),
165 };
166 ch.record(
167 Message {
168 platform: ch.platform,
169 user: match kind {
170 crate::NoteKind::Raid => String::new(),
171 crate::NoteKind::Live
173 | crate::NoteKind::Category
174 | crate::NoteKind::Offline => ch.name.to_lowercase(),
175 _ => user,
176 },
177 text: text.to_string(),
178 color: None,
179 badges: Vec::new(),
180 id: None,
181 gone: None,
182 reply_to: None,
183 note: Some(crate::Note {
184 kind,
185 what: what.to_string(),
186 }),
187 extra: Default::default(),
188 emotes: None,
189 tags: Vec::new(),
190 heat: 0.0,
191 uid: None,
192 },
193 now,
194 );
195 if matches!(
196 kind,
197 crate::NoteKind::Live | crate::NoteKind::Category | crate::NoteKind::Offline
198 ) {
199 ch.flag_event();
200 }
201 }
202 while budget > 0.0 {
203 if self.rng.unit() < budget.min(1.0) {
204 let user = self.rng.pick(USERS).to_string();
205 let text = self.rng.pick(WORDS).to_string();
206 let badges = match self.rng.unit() {
209 r if r < 0.03 => vec![crate::Badge::new(crate::BadgeKind::Moderator)],
210 r if r < 0.05 => vec![crate::Badge::new(crate::BadgeKind::Vip)],
211 r if r < 0.12 => vec![crate::Badge::new(crate::BadgeKind::Subscriber)],
212 _ => Vec::new(),
213 };
214 let roll = self.rng.unit();
217 let reply_to = (roll < 0.04).then(|| self.rng.pick(USERS).to_string());
218 let extra = crate::Extra {
219 reply_text: reply_to
220 .as_ref()
221 .map(|_| "wait is that actually true".to_string())
222 .unwrap_or_default(),
223 action: (0.04..0.06).contains(&roll),
224 first: (0.06..0.07).contains(&roll),
225 ..Default::default()
226 };
227 ch.record(
228 Message {
229 platform: ch.platform,
230 user,
231 text,
232 color: None,
233 badges,
234 id: None,
235 gone: None,
236 reply_to,
237 note: None,
238 extra,
239 emotes: None,
240 tags: Vec::new(),
241 heat: 0.0,
242 uid: None,
243 },
244 now,
245 );
246 }
247 budget -= 1.0;
248 }
249 }
250 }
251}