html terminal
Diffstat (limited to 'src/bot/rules.rs')
-rw-r--r--src/bot/rules.rs285
1 files changed, 285 insertions, 0 deletions
diff --git a/src/bot/rules.rs b/src/bot/rules.rs
new file mode 100644
index 0000000..ba9874c
--- /dev/null
+++ b/src/bot/rules.rs
@@ -0,0 +1,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(())
+}