Try   HackMD

Surgery Code Refactor

From op_stage to surgeryHolder

Foreword

This is a difficult problem, and the currently implemented solution most importanly works very consistently; it's already worked through most potential corner-cases and potential issues. I am coming to this code with a different objective and perspective, many years on from the original constraints that this design fulfilled.

Thank you for your consideration.

Goals & Non-Goals

Goals

  1. Make it easier to figure out current/next surgery states
  2. Reduce duplication within surgery code
  3. Provide greater flexibility for creating/modifying surgery steps

Non-goals

  1. Disrupt the current player actions/order for surgery

Current Implementation

Surgery status is handled via an integer variable stored on an object, usually the organ, and which "stage" the surgery is in is represented via integers. These integers live on different objects:

  • Every /obj/item/organ (i.e. head, chest, internal organs)
    • op_stage - keeps track of the current OPeration STAGE
    • organ_holder_required_op_stage - value to check against op_stage when attaching
  • Every /obj/item/parts (i.e. limbs) has the remove_stage variable
  • Every /obj/item/organ/head has the scalp_op_stage variable
  • Every /mob/living has the butt_op_stage variable

These integers are then gently nudged by large procs custom built for each surgery type. Individual items must specifically call the related surgery proc in order to attempt surgery.

Impact of current design on code

The more surgery steps done with the tool, the greater the complexity of individual procs:

Surgery Proc Name Lines in File Number of ifs
scalpel_surgery 690 81
saw_surgery 361 46
suture_surgery 165 29
cautery_surgery 150 19
spoon_surgery 105 16
snip_surgery 271 32
crowbar_surgery 54 8
wrench_surgery 34 8

In addition, this complexity spills out into other code; get_desc.dm can be impenetrable without knowing op_stages. Within surgery_procs itself, there are duplicated sections of code that resist condensing into helper procs because they are, in essence, data about the surgery step. This style of design makes the data encoded within difficult to use in other contexts without writing bespoke interpretations for the various organs and op_stage values.

To quote from the code at writing:

Understanding Op Stages
Step 1 - dont
Step 2 - uhg, fine

The Underlying Issue

This is a difficult problem.

Surgery itself has state, and is also the interface between a surgeon and several other state systems at once:

  • the /datum/human_limbs system for limbs
  • the /datum/organHolder system for internal organs
  • changes to the system are called from various /obj/item/ attacks

It also interacts more directly with some other play systems:

  • Trait checking (clumsy, training_medical)
  • bioHolder.mutantrace (tails, skeletons) on some surgery types

Proposed Implementation

Represent the state of surgery as a set of node graphs, with a state machine-like traversal proc.

This works alongside the existing limb and organHolder systems; it is not a replacement.

Diagram

This shows all node graphs, joined together for ease of viewing; not designed to work with the Simulate option.
https://stately.ai/registry/editor/3668c891-176d-4275-9b7c-adca95c720fc?machineId=a9531a72-9fd9-4a9f-a011-c4468208e88b

Component Parts

  • A node in the graph, node
    • The transitions map, whose keys are other node datums and values are a map:
      • conditions - a list of condition
      • effects - a list of effect
    • Static data about the surgery state (e.g. descriptions for examine)
    • These are not mutated. They can be reused across all mobs.
  • A condition that must be met, condition
    • Calls a check proc that returns bool TRUE/FALSE
    • Can contain variable data
    • Can be any check on the surgeon, patient, or tool; commonly:
      • /datum/surgeryHolder/condition/has/* has a part in place?
      • /datum/surgeryHolder/condition/tool/* surgeon's tool?
      • /datum/surgeryHolder/condition/intent/* surgeon's intent?
      • /datum/surgeryHolder/condition/hand/* surgeon using specific hand?
  • A side-effect of changing nodes, effect
    • Calls a do_effect proc
    • Can contain variable data e.g. min-max for damage rolls
    • Can do anything to the surgeon, patient, or tool; commonly:
      • /datum/surgeryHolder/effect/damage/* surgery damage
      • /datum/surgeryHolder/effect/bleed Bleed damage/animation
      • /datum/surgeryHolder/condition/attach/* Part (de-)attachment
  • A proc for which surgery to try, get_surgery_transition
    • Runs the check for every condition
    • If all checks run, return the next node & effects to take on success
  • The traversal proc, attempt_surgery
    • Rolls the actual surgery success attempt
    • On success
      • Applies side effects
      • Changes relevant surgeryHolder state

Supplemental Parts

  • Surgeon zone_sel switch to determine which graph(s) to check
    • Reduces complexity of node graphs considerably
  • Re-use/adapt some existing surgery procs
    • surgeryCheck - adaptable
    • calc_screw_up_prob - as-is

State Storage Example

From some lightweight testing, I think this is the minimum collection of graphs to keep track of.

This is an example of what we'd commonly see on a new /mob/living/carbon/human, having all their parts together.

  • /obj/item/organ/head
    • var/list/surgeryHolders
      • "neck" = /datum/surgeryHolder/node/neck/attached
      • "l_eye" = /datum/surgeryHolder/node/l_eye/attached
      • "r_eye" = /datum/surgeryHolder/node/r_eye/attached
      • "brain" = /datum/surgeryHolder/node/brain/attached
  • /obj/item/organ/chest
    • var/surgeryHolder
      • /datum/surgeryHolder/node/chest/neutral
  • /obj/item/limb
    • var/surgeryHolder
      • /datum/surgeryHolder/node/limb/attached

Other Additions

  • New item flag: surgery_flags & helper issurgerytool defines
    • assigning items as surgery tools is super easy
    • minimize special handling for tools with multiple potential uses (welder, omnitool)

Benefits

  • Much smaller size per-function
    • Easy to add individual condtions and effects to surgery
  • Data is structured so we can programatically find next steps
    • User interfaces can rely on game data for surgery steps
  • Possible to include secret surgeries
  • Potential for validating the node graph works correctly in test

Challenges

  • Debugging requires understanding node traversal
  • Replacing all op_stage checks is a lot of work
  • Unknown size of impact on secret content