html terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
use super::{Context, Result};
use crate::bot::get_nextblock;
use futures_util::StreamExt;
use poise::serenity_prelude::*;
use tokio::sync::Mutex;
use tokio::sync::{broadcast, OnceCell};

macro_rules! val {
    ($($k:ident($v:ty)),+) => {
        #[derive(Clone, Debug)]
        pub enum Val {
            $($k($v)),+
        }

        $(
            impl From<$v> for Val {
                fn from(value: $v) -> Self {
                    Self::$k(value)
                }
            }
        )+

        impl std::fmt::Display for Val {
            fn fmt(&self, f:&mut  std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    $(Self::$k(x) => write!(f, "{x}"),)+
                }
            }
        }
    }
}

val! {
    Bool(bool),
    Float(f32),
    Int(i32),
    Str(String)
}

macro_rules! rules {
    { of $s: ident :
        $($k:literal: $ty:ty [$doc:literal]),+ $(,)?
    } => { paste::paste! {
        impl $s {
            pub fn reduce(&self) -> impl Iterator<Item = (&'static str, Val)> {
                [$(self. [ < $k : snake > ].clone().map(Val::from).map(|x| ($k, x))),+].into_iter().filter_map(|x| x.clone())
            }

            pub fn name() -> &'static [&'static str] {
                &[$(stringify!([<$k:snake>])),+]
            }

            pub fn set(&mut self, x: &str,to: &str) -> Result<()> {
                match x {
                    $(stringify!([< $k : snake> ]) => {
                        self.[ < $k : snake >] = Some(to.parse()?);
                    }),+
                    _ => anyhow::bail!("no such field"),
                }
                Ok(())
            }

            /// returns [`None`] if field not found,
            /// returns [`Some`](if the field was not none)
            pub fn delete(&mut self, x: &str) -> Option<bool> {
                match x {
                    $(stringify!([< $k : snake> ]) => {
                        match &mut self.[ < $k : snake >] {
                            y @ Some(_) => {
                                *y = None;
                                Some(true)
                            },
                            _ => {
                                Some(false)
                            }
                        }
                    }),+
                    _ => None,
                }
            }
        }


        #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
        pub struct $s {
            $(
                #[serde(default)]
                #[serde(rename = $k)]
                #[serde(skip_serializing_if = "Option::is_none")]
                pub [< $k : snake>]: Option<$ty>,
            )+
        }
    } }
}

rules!(
    of Rules:
    "infiniteResources": bool ["Sandbox mode: Enables infinite resources, build range and build speed."],
    // mindustry does a wackky thing here, but
    // "teams": HashMap<u8, TeamRule> ["Team-specific rules."],
    "coreCapture": bool ["Whether cores change teams when they are destroyed."],
    "reactorExplosions": bool ["Whether reactors can explode and damage other blocks."],
    "possessionAllowed": bool ["Whether to allow manual unit control."],
    "schematicsAllowed": bool ["Whether schematics are allowed."],
    "damageExplosions": bool ["Whether friendly explosions can occur and set fire/damage other blocks."],
    "fire": bool ["Whether fire (and neoplasm spread) is enabled."],
    "unitAmmo": bool ["Whether units use and require ammo."],
    "unitPayloadUpdate": bool ["EXPERIMENTAL! If true, blocks will update in units and share power."],
    "unitCapVariable": bool ["Whether cores add to unit limit"],
    "showSpawns": bool ["If true, unit spawn points are shown."],
    "solarMultiplier": f32 ["Multiplies power output of solar panels."],
    "unitBuildSpeedMultiplier": f32 ["How fast unit factories build units."],
    "unitCostMultiplier": f32 ["Multiplier of resources that units take to build."],
    "unitDamageMultiplier": f32 ["How much damage units deal."],
    "unitHealthMultiplier": f32 ["How much health units start with."],
    "unitCrashDamageMultiplier": f32 ["How much damage unit crash damage deals. (Compounds with unitDamageMultiplier)"],
    "ghostBlocks": bool ["If true, ghost blocks will appear upon destruction, letting builder blocks/units rebuild them."],
    "logicUnitBuild": bool ["Whether to allow units to build with logic."],
    "disableWorldProcessors": bool ["If true, world processors no longer update. Used for testing."],
    "blockHealthMultiplier": f32 ["How much health blocks start with."],
    "blockDamageMultiplier": f32 ["How much damage blocks (turrets) deal."],
    "buildCostMultiplier": f32 ["Multiplier for buildings resource cost."],
    "buildSpeedMultiplier": f32 ["Multiplier for building speed."],
    "deconstructRefundMultiplier": f32 ["Multiplier for percentage of materials refunded when deconstructing."],
    "enemyCoreBuildRadius": f32 ["No-build zone around enemy core radius."],
    "polygonCoreProtection": bool ["If true, no-build zones are calculated based on the closest core."],
    "placeRangeCheck": bool ["If true, blocks cannot be placed near blocks that are near the enemy team."],
    "cleanupDeadTeams": bool ["If true, dead teams in PvP automatically have their blocks & units converted to derelict upon death."],
    "onlyDepositCore": bool ["If true, items can only be deposited in the core."],
    "coreDestroyClear": bool ["If true, every enemy block in the radius of the (enemy) core is destroyed upon death. Used for campaign maps."],
    "hideBannedBlocks": bool ["If true, banned blocks are hidden from the build menu."],
    "blockWhitelist": bool ["If true, bannedBlocks becomes a whitelist."],
    "unitWhitelist": bool ["If true, bannedUnits becomes a whitelist."],
    "unitCap": i32 ["Base unit cap. Can still be increased by blocks."],
    "dragMultiplier": f32 ["Environment drag multiplier."],
    "env": i32["Environmental flags that dictate visuals & how blocks function."],
    // TODO
    // "weather": Vec<Weather> ["Weather events that occur here."],
    // "bannedBlocks": Vec<Block> ["Blocks that cannot be placed."],
    // "bannedUnits": Vec<Unit> ["Units that cannot be built."],
    // "revealedBlocks": Vec<Block> ["Reveals blocks normally hidden by build visibility."],
    // "hiddenBuildItems": Vec<Item> ["Block containing these items as requirements are hidden."],
    // "objectives": MapObjectives ["In-map objective executor."],
    // "objectiveFlags": ObjectSet<String> ["Flags set by objectives. Used in world processors."],
    // "planet": Planet ["Rules from this planet are applied. If it's "sun", mixed tech is enabled."],
    "fog": bool ["If true, fog of war is enabled. Enemy units and buildings are hidden unless in radar view."],
    "staticFog": bool ["If fog = true, this is whether static (black) fog is enabled."],
    "staticColor": String ["Color for static, undiscovered fog of war areas."],
    "dynamicColor": String ["Color for discovered but un-monitored fog of war areas."],
    "lighting": bool ["Whether ambient lighting is enabled."],
    "ambientLight": String ["Ambient light color, used when lighting is enabled."],
    "modeName": String ["name of the custom mode that this ruleset describes, or null."],
    "mission": String ["Mission string displayed instead of wave/core counter. Null to disable."],
    "coreIncinerates": bool ["Whether cores incinerate items when full, just like in the campaign."],
    "borderDarkness": bool ["If false, borders fade out into darkness. Only use with custom backgrounds!"],
    "backgroundTexture": String ["path to background texture with extension (e.g. \"sprites/space.png\")"],
    "backgroundSpeed": f32 ["background texture move speed scaling - bigger numbers mean slower movement. 0 to disable."],
    "backgroundScl": f32 ["background texture scaling factor"],
    "backgroundOffsetX": f32 ["background UV offsets"],

);

// rules!(
//     of TeamRule:
//     "aiCoreSpawn": bool ["Whether, when AI is enabled, ships should be spawned from the core."],
//     "cheat": bool ["If true, blocks don't require power or resources."],
//     "infiniteResources": bool ["If true, resources are not consumed when building."],
//     "infiniteAmmo": bool ["If true, this team has infinite unit ammo."],
//     "buildAi": bool ["AI that builds random schematics."],
//     "buildAiTier": f32 ["Tier of builder AI. [0, 1]"],
//     "rtsAi": bool ["Enables \"RTS\" unit AI."],
//     "rtsMinSquad": i32["Minimum size of attack squads."],
//     "rtsMaxSquad": i32["Maximum size of attack squads."],
//     "rtsMinWeight": f32 ["Minimum \"advantage\" needed for a squad to attack. Higher -> more cautious."],
//     "unitBuildSpeedMultiplier": f32 ["How fast unit factories build units."],
//     "unitDamageMultiplier": f32 ["How much damage units deal."],
//     "unitCrashDamageMultiplier": f32 ["How much damage unit crash damage deals. (Compounds with unitDamageMultiplier)"],
//     "unitCostMultiplier": f32 ["Multiplier of resources that units take to build."],
//     "unitHealthMultiplier": f32 ["How much health units start with."],
//     "blockHealthMultiplier": f32 ["How much health blocks start with."],
//     "blockDamageMultiplier": f32 ["How much damage blocks (turrets) deal."],
//     "buildSpeedMultiplier": f32 ["Multiplier for building speed."],
// );

pub async fn commit(stdin: &broadcast::Sender<String>) {
    crate::send!(
        stdin,
        "rules {}",
        serde_json::to_string(&*rules(stdin).await).unwrap()
    )
    .unwrap();
}

pub async fn rules(stdin: &broadcast::Sender<String>) -> tokio::sync::MutexGuard<Rules> {
    static RULES: OnceCell<Mutex<Rules>> = OnceCell::const_new();
    RULES
        .get_or_init(|| async move {
            crate::send!(stdin, "rules").unwrap();
            let res = get_nextblock().await;
            Mutex::new(deser_hjson::from_str(&res).unwrap())
        })
        .await
        .lock()
        .await
}

#[poise::command(slash_command, category = "Configuration", rename = "list_rules")]
/// check them rules
pub async fn list(ctx: Context<'_>) -> Result<()> {
    poise::send_reply(
        ctx,
        poise::CreateReply::default().embed(
            CreateEmbed::new()
                .title("rules")
                .fields(
                    rules(&ctx.data().stdin)
                        .await
                        .reduce()
                        .map(|(a, b)| (a.to_string(), b.to_string(), true)),
                )
                .color(super::SUCCESS),
        ),
    )
    .await?;
    Ok(())
}

pub async fn autocomplete<'a>(
    _: Context<'a>,
    partial: &'a str,
) -> impl futures::Stream<Item = &'a str> + 'a {
    futures::stream::iter(Rules::name())
        .filter(move |name| futures::future::ready(name.starts_with(partial)))
        .map(|&x| x)
}

#[poise::command(
    slash_command,
    category = "Configuration",
    rename = "set_rule",
    default_member_permissions = "ADMINISTRATOR",
    required_permissions = "ADMINISTRATOR"
)]
/// set a rule
pub async fn set(
    ctx: Context<'_>,
    #[description = "rule"]
    #[autocomplete = "autocomplete"]
    rule: String,
    #[description = "lol"] value: String,
) -> Result<()> {
    rules(&ctx.data().stdin).await.set(&rule, &value)?;
    commit(&ctx.data().stdin).await;
    poise::say_reply(ctx, "<:ok:1182120559916625971>").await?;
    Ok(())
}

#[poise::command(
    slash_command,
    category = "Configuration",
    rename = "delete_rule",
    default_member_permissions = "ADMINISTRATOR",
    required_permissions = "ADMINISTRATOR"
)]
/// delete a rule
pub async fn del(
    ctx: Context<'_>,
    #[description = "rule"]
    #[autocomplete = "autocomplete"]
    rule: String,
) -> Result<()> {
    match rules(&ctx.data().stdin).await.delete(&rule) {
        Some(true) => poise::say_reply(ctx, "<:ok:1182120559916625971>"),
        Some(false) => poise::say_reply(
            ctx,
            "<:warning:1182119952048726066> rule existed, but already none",
        ),

        None => poise::say_reply(ctx, "<:cancel:1182128899166064720> invalid rule!"),
    }
    .await?;
    commit(&ctx.data().stdin).await;

    Ok(())
}