ShotgunNinja / Kerbalism

Hundreds of Kerbals were killed in the making of this mod.
The Unlicense
43 stars 19 forks source link

Architecture changes #118

Open ShotgunNinja opened 7 years ago

ShotgunNinja commented 7 years ago

The general idea is:

Update 2017-05-22: Reduced the scope of changes after some more thinking. Discarded concepts are striked like this Dubious concepts are represented [like this]

ShotgunNinja commented 7 years ago

Proposed by @gotmachine:

ShotgunNinja commented 7 years ago

Motivating example

Rule
{
  name = biology
  title = Simulate the physical health of Kerbals
  desc = Kerbals need to breath <b>Oxygen</b>, eat <b>Food</b> and drink <b>Water</b> to survive

  Process
  {
    name = breathing
    modifier = crew.count * env.not_breathable
    input = Oxygen@...
    output = WasteAtmosphere@...    
    dump = WasteAtmosphere
  }

  Process
  {
    name = eating
    modifier = crew.count
    input = Food@...
    output = Waste@...
    dump = Waste
  }

  Process
  {
    name = drinking
    modifier = crew.count
    input = Water@...
    output = WasteWater@...
    dump = WasteWater
  }

  Process
  {
    name = no_oxygen
    modifier = res.Oxygen.amount < 0.01
    degen = health@...                      // lower 'health' property of crew
  }

  Process
  {
    name = no_food
    modifier = res.Food.amount < 0.01
    degen = health@...
  }

  Process
  {
    name = no_water
    modifier = res.Water.amount < 0.01
    degen = health@...
  }

  Process
  {
    name = recovery                     // this will show on the info about health
    // you can split modifier on multiple lines, multiplication between them is implied
    modifier = res.Oxygen.amount > 0.01 // if oxygen is present
    modifier = res.Food.amount > 0.01   // and food is present
    modifier = res.Water.amount > 0.01  // and water is present
    modifier = health<1.0               // and health is not full (this is only used to avoid 'recovery' ui label always present)
    regen = health@...                  // increase 'health' property of crew
  }

  Alert
  {
    name = asphyxia_imminent    
    condition = res.Oxygen.amount < 0.01
    icon = problem-asphyxia             // add an arbitrary problem icon
    log = true                          // add entry to vessel log
    message = true                      // show a warning message
    stopwarp = true                     // stop timewarp
  }

  Alert
  {
    name = oxygen_low
    modifier = res.Oxygen.level<0.33
    icon = // none
    log = true
    message = true
    stopwarp = false // don't stop warp
  }  
}

Rule
{
  name = environment_control
  title = Simulate the internal atmosphere of vessels
  desc = <b>CarbonDioxide</b> need to be scrubbed, pressure need to be maintained using <b>Nitrogen</b>
  require = biology     // this rule can only be enabled if biology is also enabled

  Process
  {
    name = co2_poisoning
    modifier = res.WasteAtmo.level>0.008
    degen = health@...
  }

  Process
  {
    name = atmosphere_leaking
    modifier = hab.surface * env.not_breathable
    input = Atmosphere@0.000001
  }

  Alert
  {
    name = co2poisoning    
    modifier = res.WasteAtmo.level>0.004  // a bit lower than actual effect on health start
    icon = problem-poisoning
    log = true
    message = true
    stopwarp = true
  }
}

Rule
{
  name = radiation
  title = Simulate radiation and shielding
  desc = A complex radiation environment, and the means to deal with it.

  Process
  {
    name = uv_irradiation
    modifier = hab.uv                 // EM flux in a particular bandwidth reaching the internal habitat
    degen = health@...
  }

  Process
  {
    name = xray_irradiation
    modifier = hab.xray
    degen = health@...
  }

  Process
  {
    name = gamma_irradiation
    modifier = hab.gamma
    degen = health@...
  }

  Process
  {
    name = plasma_irradiation
    modifier = hab.plasma
    degen = health@...
  }

  Process
  {
    modifier = res.ShieldHighZ.amount / hab.surface     // amount of HighZ shielding per-m²
    absorb = xray@...
    absorb = gamma@...
  }

  Process
  {
    modifier = res.ShieldHighZ.amount / hab.surface     // amount of HighZ shielding per-m²
    absorb = plasma@...
    emit = gamma@...
  }

  Process
  {
    modifier = res.ShieldLowZ.amount / hab.surface      // amount of LowZ shielding per m²
    absorb = plasma@...
    absorb = xray@...
  }

  Process
  {
    modifier = res.ShieldLowZ.amount / hab.surface      // amount of LowZ shielding per m²
    absorb = gamma@...
    emit = xray@...
  }

  Process
  {
    modifier = res.ShieldGradedZ.amount / hab.surface   // amount of GradedZ shielding per m²
    absorb = plasma@...
    absorb = xray@...
    absorb = gamma@...
  }

  // bonus: you can use this to implement implicit shielding from amount of water or other resources
}

Component
{
  name = none
  title = Nothing selected
  desc = If you can't or wan't to take anything else.
  // does nothing
}

Component
{
  // scaling convention: enough to scrub 1 kerbal, with something to spare
  name = scrubber
  title = Sequester co2 bla bla
  desc = Arbitrary long description.
  require = atmosphere_control
  tag = eclss
  toggle = true                     // if the resource flow state and process can be toggled
  running = false                   // the resource flow starting state and the process starting state
  scalable = true                   // if the scale unit can be tweaked by the user in VAB (arbitrary module values are not scaled)
  hardware = 5                      // how many Hardware units are required to install or repair this component
  skill = engineer@2                // trait and experience required to install this component
  mtbf = ...                        // how often does the component fail
  mass = ...                        // how much extra mass this component add when installed
  cost = ...                        // extra cost in space-bucks when component is installed
  tech = ...                        // technology required to unlock

  Process
  {
    modifier = env.not_breathable 
    input = WasteAtmo@...
    output = CarbonDioxide@...
    dump = true
  }
}

Component
{
  // scale convention: volume in m²
  name = oxygen_supply
  title = Oxygen supply
  desc = ...
  require = biology             // only enable if biology is enabled
  tag = supplies
  toggle = false
  running = true
  scalable = false
  hardware = 0.05               // i'll elaborate on this another time
  skill = engineer@1
  mtbf = ...                    // leaks?
  mass = 0                      // no extra mass/cost here
  cost = 0

  resource = Oxygen@120
}

// scale convention: habitat volume in m³
Component
{
  name = habitat_interior
  require = atmosphere_control
  resource = Atmosphere@1
  resource = WasteAtmosphere@0/1 // can specify amount and capacity separately
  toggle = true
  running = true
}

// scale convention: habitat surface in m²
Component
{
  name = habitat_exterior
  require = radiation
  resource = ShieldingLowZ@0/1
  resource = ShieldingHighZ@0/1
  resource = ShieldingGradedZ@0/1
}

// now we add a component to a part
@PART[mk1pod]
{
  MODULE
  {
    name = Controller
    allow = none, eclss@crew, pod_experiments   // the parameter after '@' is the scale: you can use a number or one of:
                                                // - 'crew' to scale using crew capacity of part
                                                // - 'volume' to scale using part volume
                                                // - 'surface' to scale using part surface
  }

  MODULE
  {
    name = Controller
    allow = supplies@0.05                       // here we are allowing anything tagged as 'supplies', passing 0.05m³ volume
                                                // (these components here are assumed to use container volume as scaling convention)
  }

  MODULE
  {
    name = Controller
    allow = habitat_interior@volume                    
  }

  MODULE
  {
    name = Controller
    allow = habitat_exterior@surface
  }
}
ShotgunNinja commented 7 years ago

Major advantages

A level of indirection between enabled rule-set and what's added to parts, thanks to Controller ability to take a list of names and tags of components. So it make sense to add a Controller with allow=eclss to all manned pods, irregardless of the rule-set the user will choose. Then if no enabled component is tagged as eclss in a particular savegame, no extra functionality is added to the part. But if one or more components match 'eclss', then functionality is added to the part.

Removal of profiles and focus on rules allow composition between mechanics provided by different authors. Distribute a set of rules, and the user may enable/disable those rules when starting a new game. Without these rules being mutually exclusive to others distributed by somebody else.

Enabled rule-set specified per-savegame thanks to the lack of reliance on MM for most things. The user will choose what rules/components to enable when starting a new game, using a configuration window. The rule-set is then serialized into the savegame.

Module enable/disable technique use is generalized to implement: installation/dismantling of components, reliability (fixing a failed component mean re-installing it) and disabling stock/third-party modules without relying on MM (so we can do it per-savegame).

Much less verbose and hackish modifiers when implementing relatively complex mechanics, thanks to modifier-expressions.

Third-party modules emulation is explicit, defined per-component, so ideally other mod authors can specify when and where one of their modules is emulated.

Arbitrary telemetry and alerts will be a powerful way to enrich your own rules, and allow to remove most (if not all) of the hard-coded concepts in the UI.

Can use MM patches against Rules and Components, with all the flexibility that gives.

gotmachine commented 7 years ago

This is awesome. One thing that seems absent (or than I didn't understand) is how the telemetry info is defined and linked to all this (and specifically to alerts). Could you add an example ?

A few remarks :

As for my proposals :

GUIpanel
{
   title = comfort         // the name/gui title of the panel
   index = 3               // add the panel to the third "panel space" in the planner
}
GUIline
{
  panel = comfort                            // planner panel the line is added to, added to telemetry panel is unspecified
  index = 1                                  // order of the line in the telemetry UI / planner panel
  title = breakdown                          // the line left text
  // example note : "comfort" is a degen value like "health" in your example
  value = comfort.capacity / [rate_formula]  // the line right text, modifier-expression or use "" to evaluate as a string
  value_format = duration                    // returned value is Lib.HumanReadableDuration(value)
  visible = info.comfort < 0.2               // modifier-expression that determine visibility
}
GUIline
{
  panel = comfort              // planner panel the line is added to, added to telemetry panel is unspecified
  index = 2                    // order of the line in the telemetry UI / planner panel
  panel = comfort              // planner panel the line is added to, added to telemetry panel is unspecified
  title = comfort              // the line left text
  value = "poor"               // the line right text, modifier-expression or use "" to evaluate as a string
  color = #ff0000              // color of the value
  visible = info.comfort < 0.2 // modifier-expression that determine visibility
}
GUIline
{
  panel = comfort      
  index = 2 
  title = comfort
  value = "modest"
  color = #ff8300
  visible = (info.comfort >= 0.2) && (info.comfort < 0.4)
}
GUItooltip
{
  line_title = comfort      // show tooltip on GUIline whose title = comfort
  title = firm ground       // the line left text
  value = info.firm_ground  // the line right text, modifier-expression or use "" to evaluate as a string
  value_format = boolean    // returned value is Lib.HumanReadableBoolean(bool value) {return value ? "true" : "false";}
  color = #00ff00           // color of the value
}
ShotgunNinja commented 7 years ago

@gotmachine Each vessel has a set of Process, Telemetry and Alerts compiled from the active rule-set (that apply to all vessels) and from the components actually installed in parts of the vessel.

An habitat part may provide telemetry about pressure. Or that may be provided by a pressure control ECLSS component instead. Or even by the atmosphere_control rule. Telemetry entries with the same name 'collapse' into a single one (with average value, and individual readings in its tooltip). The collapsed set of telemetries is then shown in the omonimous panel in Monitor.

For Alerts, it is pretty much the same as above. You can imagine a pressure control ECLSS component that include an alert about pressure dropping. The alerts in a vessel will be shown in a new panel in Monitor, where they can be toggled individually per-vessel. Each enabled alert in essence check if a modifier-expression condition evaluate to true (more than zero). If that's the case, it show a custom problem-icon (that is just a png image, there are only width/height constrains), a warning and/or stop timewarp.

Component
{
  name = pressure_control
  ...
  Telemetry
  {
    name = habitat_pressure                
    title = pressure                                
    value = Atmosphere.level * 101    
    format = F2                                    
    unit = kPA                                      
  }
  ...
  Alert
  {
    name = pressure_dropping
    title = Pressure is dropping
    condition = Atmosphere.rate < 0.0
    problem-icon = my_path/my_custom_png_icon_without_extension
    message = Atmospheric pressure in the internal habitat is dropping
    stopwarp = false
  }
  ...
}

For killing/breaking kerbals, I was thinking of simplifing the system so that there are only two 'hard-coded properties': health and sanity. Both start from a value of 1.0. When health reach zero, there is death. When sanity reach zero, there is mental breakdown. Each rule/component can increment/decrement these two hard-coded properties using 'degen/regen' fields in Process. Both properties will also get the special vitals 'icons' in the Telemetry panel of Monitor.

The 'motivating example' was written before your suggestion about custom per-kerbal properties, that make sense. But there is some elegance in having a unique mapping between 'property' and 'consequence'. Anyway, if kerbal properties end up being not hard-coded, their definition may look something like this:

Property
{
  name = health
  consequence = death
  warning-threshold = 0.5
  danger-threshold = 0.2
  nominal-icon = Kerbalism/icons/heart-gray
  warning-icon = Kerbalism/icons/heart-yellow
  danger-icon = Kerbalism/icons/heart-red
}

with implicit 'capacity' of 1, and 'fatal-threshold' of 0.

About 'full recovery on kerbin': if properties are not reset once a kerbal is back at KSC, we'll need a dedicated UI to show the user their values. So that's more work. I can also imagine players firing/hiring kerbals to get around it.

Module{ allow = } "@" parameter could also have a "mass" option in addition to crew, volume & surface

Agreed. I think these 4 scaling types should cover pretty much every possible use case.

For reference, here is the proposed "GUIpanel/GUIline/GUItooltip" nodes system to define telemetry/planner UI [...]

Thanks for the efforts on that. I anticipate it will be not easy keep the current planner structure intact. An alternative may be to essentially re-use Telemetry entries for both monitor and planner. That would be a fundamentally different planner, so I'm not sure.

PiezPiedPy commented 7 years ago

If I may throw in, I would go for having the Kerbal properties not hard coded, I like the idea of the ability to make kerbal's little delicate snowflakes or hard ass gangsters, with values chosen at random or specific kerbal's getting their own personal set. i.e Jeb could have a higher danger-threshold and lower warning-threshold. Can I assume even if the monitor and planner are obviously going to change that they will still be using the window class to be displayed, as I am currently working on a way to stop the annoying click through effect.

ShotgunNinja commented 7 years ago

@PiezPiedPy There is a 'per-kerbal variance' that can be specified in degeneration, and that will probably be preserved in the new rule system. But that is only meant to avoid 'all crew suffocating or going crazy at once' scenarios. There will be no way to specify that a particular kerbal is different than the others.

Can I assume even if the monitor and planner are obviously going to change that they will still be using the window class to be displayed, as I am currently working on a way to stop the annoying click through effect

I don't think that the overall architecture of the UI library will require any changes at all. The changes will be related mostly to content. Monitor & Planner are not using Window, but any changes you make in Window should be trivial to back-port to them afterward.

gotmachine commented 7 years ago

Isn't having a single health variable going to be problematic ? Various rules regen/degen will interfere : eating will regen health lost by radiation, for example.

The telemetry concept feel great to represent a rate or amount of something. And the planner or monitor could make use of a structured hierarchical condition-visibility based string-able line/tooltip system.

ShotgunNinja commented 7 years ago

@gotmachine The idea was to have health/sanity like an 'health bar' in other videogames, with 'idle recovery'. But you are right, and on further analysis having common properties for food and radiation is not going to work. I think this aspect need more thinking.

ShotgunNinja commented 7 years ago

The issue has been updated, and the scope of changes reduced sensibly. Further reduction is likely before implementation start.