FolkeLogo

Hammerting

Genre: Fantasy,  Strategy, Lore-Rich


Developer: Warpzone Studios


Publisher: Team17 Digital


Platform & Control: Keyboard and Mouse


Engine: Warpcore ( C/C++ )


Source Control: Git + Git-Fork


Available: Steam (Early Access)


Describtion of the game:

Manage a Dwarven mining colony in the unexplored mountains of Mara. As a war rages in the Overworld, you will need to craft, explore and fight as you provide your allies with the finest weapons and gear to assist in the war against evil.

Job Broker

Job broker is a way to distribute jobs. It is shown in game with a UI window.

Jump to Job Broker


Keybinds

I implemented first iteration of remapping of key-binding. Eventually created a system to use key modifiers such as Shift/Ctrl + key.

Jump to Keybinds


Name Component

Added a name component that the player can use to set custom name. 

Jump to Name Component


Set Job Priority

Due to some jobs being more important than others, a system to priority certain jobs is already in place. I made it possible for the player to set the prioty by themself.

Jump to Set Job Priority


Fluid Source

The game added fluids during my time at the company that spawned through various fluid sources.  

Jump to Fluid Source

Job Broker

Job Broker


When playing the game, the player must macromanage the workers with various jobs throughout the game. You want to build something? That's a job. You want to craft something? That's a job.. etc. Job broker is a UI window in-game that shows the current existing jobs in various categories. 


When I first joined the development the Job Broker was barely in use. The task was to list the various jobs and access the data so that the player can tweak the value accordingly.  

I started with creating a struct that can hold information necessary for the job list.


Job Instance struct: 

  • job_type - A struct containing various tags such as a string saying what type the job is.
  • job_id - Every job has different id to easy access its data.
  • job_priority - An enum between "low - normal - high" priority. High priority will be focused, then normal, then low.
  • pause - The player can pause the jobs if they want the workers to focus something else.
  • is_possible - A bool that keeps track if the job can be executed or not.
  • impossible_reason - If the bool "is_possible" is false, a string will say what reason being. 
  • workplace - The craft and building jobs have a "workplace", a location that spawns the various jobs.


The struct variables set right after a job is being added. 

          		
struct job_broker_ui_job_instance {
  const job_type*                   _type;
  job_id                            _job_id;
  job_priority                      _job_prio;
  bool                              _is_paused;
  bool                              _is_possible;
  wc_tag                            _impossible_reason;
  job_broker_ui_workplace_instance* _workplace;
};

        
	
          		
for ( const job_instance& job : _job_instances ) {
    job_broker_ui_job_instance job_broker_instance;
    job_broker_instance._job_prio          = job._priority;
    job_broker_instance._job_id            = job._id;
    job_broker_instance._is_paused         = job._is_paused;
    job_broker_instance._is_possible       = job._is_possible;
    job_broker_instance._impossible_reason = job._impossible_reason;
    job_broker_instance._type              = job._type;
    for ( u64 i = 0; i < _ui_data._workplace_instances.size(); i++ ) {
      if ( _ui_data._workplace_instances[i]._id == job._workplace ) {
        job_broker_instance._workplace = &_ui_data._workplace_instances[i];
        break;
      }
    }
    _ui_data._job_instances.push( job_broker_instance );
    _ui_data._ui_friendly_job_instances.push( &_ui_data._job_instances.back() );
        
	

During my time I was tasked to improve the list and sort the jobs into various categories. I made a callback function that keep the list updated. In the callback function, I sort the jobs into different lists. 

The list was made with structs of various variables, with an additional list, so that entries in the list can act as a sub-category.


Job Category struct: 

  • header - Name of the category / sub-category.
  • header_icon - Localize a string containing an icon accordingly to each job.
  • job_instance - A pointer to the job instance data.
  • entry_workplace - String and a hash for the workplace.
  • workplace_entity - The entity id for accessing the workplace.
  • job_type - Same as earlier, a struct containing various tags such as a string saying what type the job is.
  • job_category_entry - list of all the jobs for reach category.
  • job_category_entry_ui_friendly - same, but const for UI related logic.


          		
struct job_list_category_entry_ui {
  const char*                                        _header;
  const char*                                        _header_icon;
  const job_broker_ui_job_instance*                  _job_instance;
  wc_tag                                             _entry_workplace;
  entity                                             _workplace_entity;
  wc_tag                                             _job_type;
  warpcore::array<const job_list_category_entry_ui*> _ui_friendly_entries;
  warpcore::array<job_list_category_entry_ui>        _entries;
};

struct job_list_category_ui {
  warpcore::array<const job_list_category_entry_ui*> _ui_friendly_entries;
  warpcore::array<job_list_category_entry_ui>        _entries;
  wc_tag                                             _job_type;
  const char*                                        _category_name;
};

struct job_list_ui {
  warpcore::array<job_list_category_ui>        _categories;
  warpcore::array<const job_list_category_ui*> _ui_friendly_categories;
};
        
	

By no prior sorting, the callback function is used to sort the jobs into the various categories. There are currently 5 different categories for the jobs:


  • Mining - Shows all the excavation jobs.
  • Crafting - Shows buildings and infrastructures being built.
  • Building Craft Jobs - Shows the various crafting jobs.
  • Storage Haul - Shows hauling for any storage.
  • Extra - Any job that doesn't fall into previous category get sorted here.


For each category the list gets an increase in size by 1 and since we don't know how many jobs under each category there are, we need to reserve memory of same size as all the jobs for each category. "Reserve" is not allocating memory, rather it is as the name suggest, it reserves it.



          		
gs_job_list_ui_callback( job_list_ui* job_list, void* user_data ) {
  gamestate*                         gs                = static_cast<gamestate*>( user_data );
  entity_component_system*           job_broker_system = &gs->_mainstate->_job_broker_system;
  const job_broker_ui_compiled_data* ptr               = nullptr;
  job_broker_system_get_ui_data( job_broker_system, &ptr );
  array<const job_broker_ui_job_instance*> job_inc;
  job_inc.reserve( ptr->_job_instances.size() );
  array<const job_broker_ui_job_instance*> job_to_remove;
  job_to_remove.reserve( job_inc.size() );
  array<u64> profession_craft_jobs;
  profession_craft_jobs.reserve( 32 );
  const profession_record* arcanist =
    database_get_by_name<profession_record>( &gs->_database, PROF_ARCANIST );
  profession_craft_jobs.push( arcanist->_craft_job_name._hash );
  const profession_record* blacksmith =
    database_get_by_name<profession_record>( &gs->_database, PROF_BLACKSMITH );
  profession_craft_jobs.push( blacksmith->_craft_job_name._hash );
  const profession_record* cook =
    database_get_by_name<profession_record>( &gs->_database, PROF_COOK );
  profession_craft_jobs.push( cook->_craft_job_name._hash );
  const profession_record* farmer =
    database_get_by_name<profession_record>( &gs->_database, PROF_FARMER );
  profession_craft_jobs.push( farmer->_craft_job_name._hash );
  const profession_record* metallurgist =
    database_get_by_name<profession_record>( &gs->_database, PROF_METALLURGIST );
  profession_craft_jobs.push( metallurgist->_craft_job_name._hash );
  const profession_record* stonemason =
    database_get_by_name<profession_record>( &gs->_database, PROF_STONEMASON );
  profession_craft_jobs.push( stonemason->_craft_job_name._hash );
  const profession_record* tailor =
    database_get_by_name<profession_record>( &gs->_database, PROF_TAILOR );
  profession_craft_jobs.push( tailor->_craft_job_name._hash );
  const profession_record* whitesmith =
    database_get_by_name<profession_record>( &gs->_database, PROF_WHITESMITH );
  profession_craft_jobs.push( whitesmith->_craft_job_name._hash );

  for ( const job_broker_ui_job_instance& job : ptr->_job_instances ) {
    job_inc.push( &job );
  }
  job_list->_categories.clear();
  job_list->_ui_friendly_categories.clear();
  job_list->_categories.reserve( 32 );
  job_list->_ui_friendly_categories.reserve( 32 );
        
	
          		
for ( const job_broker_ui_job_instance* job : job_inc ) {
    if ( job->_type->_tag != "JOB_DIG_MINE" ) {
      continue;
    }
    add_category_entry( category_dig, *job );
    job_to_remove.push( job );
  }
  for ( sys_u64 i = 0; i < job_to_remove.size(); i++ ) { // clear the array for futher use
    job_inc.remove_unordered( job_to_remove[i] );
  }
  job_to_remove.clear();
  job_list->_ui_friendly_categories.push( &job_list->_categories.back() );
        
	
          		
  job_list->_categories.increase_size( 1 );
  job_list_category_ui& category_craft = job_list->_categories.back();
  add_category( category_craft, "JOB_UI_LIST_CRAFT_BUILDING", ptr );

  for ( const job_broker_ui_job_instance* job : job_inc ) {
    if ( job->_type->_tag == "JOB_CRAFT_PROF_BUILDER" ) {
      add_sub_category_entry( *job, category_craft );
    }
  }
  for ( sys_u64 i = 0; i < category_craft._entries.size(); i++ ) {
    job_list_category_entry_ui& cat_ent = category_craft._entries[i];
    for ( const job_broker_ui_job_instance* job : job_inc ) {
      if ( job->_workplace->_entity == cat_ent._workplace_entity ) {
        job_list_category_entry_ui category_entry;
        category_entry._header       = job->_type->_tag._string;
        category_entry._header_icon  = job->_type->_icon._string;
        category_entry._job_instance = job;
        category_entry._job_type     = job->_type->_tag;
        job_to_remove.push( job );
        cat_ent._entries.push( category_entry );
        cat_ent._ui_friendly_entries.push( &cat_ent._entries.back() );
      }
    }
  }
  for ( sys_u64 i = 0; i < job_to_remove.size(); i++ ) { // clear the array for futher use
    job_inc.remove_unordered( job_to_remove[i] );
  }
  job_to_remove.clear();
  job_list->_ui_friendly_categories.push( &job_list->_categories.back() );
        
	
          		
  job_list->_categories.increase_size( 1 );
  job_list_category_ui& category_building = job_list->_categories.back();
  array<entity>         craft_prof_job;
  craft_prof_job.reserve( job_inc.size() );
  add_category( category_building, "JOB_UI_LIST_BUILDING_CRAFT_JOBS", ptr );
  // array<entity> build_craft_job;

  for ( const job_broker_ui_job_instance* job : job_inc ) {

    if ( job->_type->_tag == "JOB_REFILL_STATUSBAR" ||
         profession_craft_jobs.contains( job->_type->_profession_record->_craft_job_name._hash ) ) {
      sys_bool has_added_job = false;
      for ( sys_u64 i = 0; i < craft_prof_job.size(); i++ ) {
        if ( job->_workplace->_entity == craft_prof_job[i] ) {
          has_added_job = true;
          break;
        }
      }
      if ( !has_added_job ) {
        craft_prof_job.push( job->_workplace->_entity );
        add_sub_category_entry( *job, category_building );
      }
    }
  }
  for ( sys_u64 i = 0; i < category_building._entries.size(); i++ ) {
    job_list_category_entry_ui& cat_ent       = category_building._entries[i];
    sys_bool                    has_added_job = false;
    for ( const job_broker_ui_job_instance* job : job_inc ) {
      if ( job->_workplace->_entity == cat_ent._workplace_entity ) {
        has_added_job = false;
        for ( sys_u64 y = 0; y < cat_ent._entries.size(); y++ ) {
          job_list_category_entry_ui& temp_cat_ent = cat_ent._entries[y];
          if ( temp_cat_ent._job_type == job->_type->_tag ) {
            job_to_remove.push( job );
            has_added_job = true;
            break;
          }
        }
        if ( !has_added_job ) {
          job_list_category_entry_ui category_entry;
          category_entry._header       = job->_type->_tag._string;
          category_entry._header_icon  = job->_type->_icon._string;
          category_entry._job_instance = job;
          category_entry._job_type     = job->_type->_tag;
          job_to_remove.push( job );
          cat_ent._entries.push( category_entry );
          cat_ent._ui_friendly_entries.push( &cat_ent._entries.back() );
        }
      }
    }
    for ( sys_u64 y = 0; y < job_to_remove.size(); y++ ) { // clear the array for futher use
      job_inc.remove_unordered( job_to_remove[y] );
    }
  }
  for ( sys_u64 i = 0; i < job_to_remove.size(); i++ ) { // clear the array for futher use
    job_inc.remove_unordered( job_to_remove[i] );
  }
  job_to_remove.clear();
  job_list->_ui_friendly_categories.push( &job_list->_categories.back() );
        
	
          		
  job_list->_categories.increase_size( 1 );
  job_list_category_ui& inventory_haul = job_list->_categories.back();
  add_category( inventory_haul, "JOB_UI_LIST_STORAGE_HAULING", ptr );

  for ( const job_broker_ui_job_instance* job : job_inc ) {
    if ( job->_type->_tag == "JOB_HAUL" ) {
      add_category_entry( inventory_haul, *job );
      job_to_remove.push( job );
    }
  }
  for ( sys_u64 y = 0; y < job_to_remove.size(); y++ ) { // clear the array for futher use
    job_inc.remove_unordered( job_to_remove[y] );
  }
  job_list->_ui_friendly_categories.push( &job_list->_categories.back() );
        
	
          		
  job_list->_categories.increase_size( 1 );
  job_list_category_ui& extra_jobs = job_list->_categories.back();
  add_category( extra_jobs, "JOB_UI_LIST_EXTRA", ptr );
  for ( const job_broker_ui_job_instance* job : job_inc ) {
    add_category_entry( extra_jobs, *job );
    job_to_remove.push( job );
  }
  for ( sys_u64 y = 0; y < job_to_remove.size(); y++ ) { // clear the array for futher use
    job_inc.remove_unordered( job_to_remove[y] );
  }
  ASSERT( job_inc.size() == 0 );
  job_list->_ui_friendly_categories.push( &job_list->_categories.back() );
        
	

Keybinds

I helped adding support to use modifiers for key bindings, such as Shift or Ctrl. 

By having a list with all virtual keys possible (individual logic for various languages since not all keyboards is mapped the same), I created support a 2D-array. Each row in the 2D array is for either "No modifier" "Shift modifier" "Ctrl modifier" or "Shift | Ctrl modifier".


On start-up, the initializer for key mapper checks a text document for the mapping. If the document has been modified, the code will re-apply the mapping.


I created a struct to reach various data from the keys that can be modified. The struct contains:

  • localization_tag - A string that contain what the key is used for.
  • event_tag - A tag with string + hash for the key.
  • command_option_name - The name of key.
          		
  enum e_remap_keys{
  REMAP_KEYS_NONE       = 0,
  REMAP_KEYS_SHIFT      = 0x1,
  REMAP_KEYS_CTRL       = 0x2,
  REMAP_KEYS_CTRL_SHIFT = REMAP_KEYS_SHIFT | REMAP_KEYS_CTRL,

  REMAP_KEYS_COUNT = 4,
};

struct remapped {
  const sys_u64*                         _events;
  sys_u64                                _event_count;
  const struct msg_WINDOWMSG_KB_VIRTUAL* _msg;
};

struct key_remapper {
  WC_INLINE void init() {
    for ( sys_u8 i = 0; i < REMAP_KEYS_COUNT; i++ ) {
      _virtual_key_remap[i].resize( VIRTUAL_N_KEYS );
    }
  }

  WC_INLINE remapped virtual_key_remap( const struct msg_WINDOWMSG_KB_VIRTUAL* msg ) {
    sys_u64  modifier_keys = REMAP_KEYS_NONE;
    remapped ret           = {};
    if ( window_is_virtual_key_down( VIRTUAL_LSHIFT ) ||
         window_is_virtual_key_down( VIRTUAL_RSHIFT ) ) {
      modifier_keys |= REMAP_KEYS_SHIFT;
    }
    if ( window_is_virtual_key_down( VIRTUAL_LCONTROL ) ||
         window_is_virtual_key_down( VIRTUAL_RCONTROL ) ) {
      modifier_keys |= REMAP_KEYS_CTRL;
    }
    ret._event_count += _virtual_key_remap[modifier_keys][msg->_key].size();
    ret._events = _virtual_key_remap[modifier_keys][msg->_key].begin();
    ret._msg    = msg;
    return ret;
  }
        
	
          		
static enum e_remap_keys calc_modifier_keys( const char*& in ) {
    sys_u64 ret = 0;
    if ( sys_strbegin( in, "CTRL_" ) ) {
      ret |= REMAP_KEYS_CTRL;
      in += sys_strlen( "CTRL_" );
    }
    if ( sys_strbegin( in, "SHIFT_" ) ) {
      ret |= REMAP_KEYS_SHIFT;
      in += sys_strlen( "SHIFT_" );
    }

    return static_cast<e_remap_keys>( ret );
  }

  static sys_u64 write_string( const sys_u64 compound, char* buf, sys_u64 buf_size ) {
    return write_string( static_cast<window_virtual_key>( static_cast<sys_u32>( compound ) ),
                         static_cast<e_remap_keys>( compound >> 32 ),
                         buf,
                         buf_size );
  }

  static sys_u64 write_string( const window_virtual_key key, const e_remap_keys mod, char* buf, sys_u64 buf_size ) {
    sys_u64 written = 0;
    if ( mod & REMAP_KEYS_CTRL ) {
      written += sys_snprintf( buf + written, buf_size - written, "CTRL_" );
    }
    if ( mod & REMAP_KEYS_SHIFT ) {
      written += sys_snprintf( buf + written, buf_size - written, "SHIFT_" );
    }
    written += sys_snprintf( buf + written, buf_size - written, window_virtual_key_strings[key] );
    return written;
  }

  static sys_u64 to_compound( const window_virtual_key key, const e_remap_keys mod ) {
    return sys_u64( key ) | ( sys_u64( mod ) << 32 );
  }
array<warpcore::hybrid_array<sys_u64, 4>> _virtual_key_remap[REMAP_KEYS_COUNT];
        
	
          		
struct remappable_key_info {
  const char* _localization_str;
  wc_tag      _event_tag;
  const char* _command_option_name;
};

static const remappable_key_info remapped_keys[] = {
  { "OPTION_KEY_PAUSE",                    GAME_INPUT_SPEED_PAUSE,                  "gamemedia-key-pause",                 },
  { "OPTION_KEY_SPEED1",                   GAME_INPUT_SPEED_PLAY,                   "gamemedia-key-speed1",                },
  { "OPTION_KEY_SPEED2",                   GAME_INPUT_SPEED_FAST,                   "gamemedia-key-speed2",                },
  { "OPTION_KEY_SPEED3",                   GAME_INPUT_SPEED_FASTER,                 "gamemedia-key-speed3",                },
  { "OPTION_COMMAND_CANCEL",               GAME_INPUT_COMMAND_CANCEL,               "gamemedia-key-command-cancel",        },
  { "OPTION_COMMAND_MINE",                 GAME_INPUT_COMMAND_MINE,                 "gamemedia-key-command-mine",          },
  { "OPTION_COMMAND_PLACE_TILE",           GAME_INPUT_COMMAND_PLACE_TILE,           "gamemedia-key-command-place-tile",    },
  { "OPTION_COMMAND_BUILD_INFRASTRUCTURE", GAME_INPUT_COMMAND_BUILD_INFRASTRUCTURE, "gamemedia-key-command-infrastructure",},
  { "OPTION_COMMAND_BUILD_ROOM",           GAME_INPUT_COMMAND_BUILD_ROOM,           "gamemedia-key-command-room",          },
  { "OPTION_COMMAND_DESTROY",              GAME_INPUT_COMMAND_DESTROY,              "gamemedia-key-command-destroy",       },
  { "OPTION_COMMAND_MOVE_TO",              GAME_INPUT_COMMAND_MOVE_TO,              "gamemedia-key-command-move-to",       },
  { "OPTION_COMMAND_ATTACK",               GAME_INPUT_COMMAND_ATTACK,               "gamemedia-key-command-attack",        },
  { "OPTION_COMMAND_SET_PRIO",             GAME_INPUT_COMMAND_SET_PRIO,             "gamemedia-key-command-set-prio",      },
  { "OPTION_WINDOW_DWARFELOPEDIA",         GAME_INPUT_WINDOW_DWARFELOPEDIA,         "gamemedia-key-dwarfelopedia",         },
  { "OPTION_WINDOW_OVERWORLD",             GAME_INPUT_WINDOW_OVERWORLD,             "gamemedia-key-overworld",             },
  { "OPTION_WINDOW_RESEARCH",              GAME_INPUT_WINDOW_RESEARCH,              "gamemedia-key-research",              },
  { "OPTION_WINDOW_LOGBOOK",               GAME_INPUT_WINDOW_LOGBOOK,               "gamemedia-key-logbook",               },
  { "OPTION_WINDOW_LEDGER",                GAME_INPUT_WINDOW_LEDGER,                "gamemedia-key-ledger",                },
  { "OPTION_WINDOW_JOBBROKER",             GAME_INPUT_WINDOW_JOB_BROKER,            "gamemedia-key-jobbroker",             },
  { "OPTION_WINDOW_RECRUITMENT",           GAME_INPUT_WINDOW_RECRUITMENT,           "gamemedia-key-recruitment",           },
  { "OPTION_CAMERA_UP_0",                  GAME_INPUT_CAMERA_UP_0,                  "gamemedia-key-camera-up-0",           },
  { "OPTION_CAMERA_UP_1",                  GAME_INPUT_CAMERA_UP_1,                  "gamemedia-key-camera-up-1",           },
  { "OPTION_CAMERA_DOWN_0",                GAME_INPUT_CAMERA_DOWN_0,                "gamemedia-key-camera-down-0",         },
  { "OPTION_CAMERA_DOWN_1",                GAME_INPUT_CAMERA_DOWN_1,                "gamemedia-key-camera-down-1",         },
  { "OPTION_CAMERA_LEFT_0",                GAME_INPUT_CAMERA_LEFT_0,                "gamemedia-key-camera-left-0",         },
  { "OPTION_CAMERA_LEFT_1",                GAME_INPUT_CAMERA_LEFT_1,                "gamemedia-key-camera-left-1",         },
  { "OPTION_CAMERA_RIGHT_0",               GAME_INPUT_CAMERA_RIGHT_0,               "gamemedia-key-camera-right-0",        },
  { "OPTION_CAMERA_RIGHT_1",               GAME_INPUT_CAMERA_RIGHT_1,               "gamemedia-key-camera-right-1",        },
  { "OPTION_SCREENSHOT",                   GAME_INPUT_SCREENSHOT,                   "gamemedia-key-screenshot",            },
  { "OPTION_SCREENSHOT_WORLD",             GAME_INPUT_SCREENSHOT_WORLD,             "gamemedia-key-screenshot-world",      },
  { "OPTION_SCREENSHOT_UI",                GAME_INPUT_SCREENSHOT_UI,                "gamemedia-key-screenshot-ui",         },
};
        
	
          		
static void
init_key_remapper( key_remapper& kr, const localization_manager& lm ) {
  kr.init();
  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_ESCAPE].push( GAME_INPUT_CANCEL_HASH );
  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_ESCAPE].push( GAME_INPUT_SPAWN_PAUSE_MENU_HASH );

  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_TAB].push( GAME_INPUT_SELECT_NEXT_HASH );
  kr._virtual_key_remap[REMAP_KEYS_SHIFT][VIRTUAL_TAB].push( GAME_INPUT_SELECT_PREV_HASH );
  kr._virtual_key_remap[REMAP_KEYS_CTRL][VIRTUAL_TAB].push( GAME_INPUT_SELECT_NEXT_IMPORTANT_HASH );
  kr._virtual_key_remap[REMAP_KEYS_CTRL_SHIFT][VIRTUAL_TAB].push(
    GAME_INPUT_SELECT_PREV_IMPORTANT_HASH );

  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_ENTER].push( GAME_INPUT_OK_HASH );
  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_PAD_ENTER].push( GAME_INPUT_OK_HASH );

  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_SPACEBAR].push( GAME_INPUT_SPEED_PAUSE_HASH );
  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_1].push( GAME_INPUT_SPEED_PLAY_HASH );
  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_2].push( GAME_INPUT_SPEED_FAST_HASH );
  kr._virtual_key_remap[REMAP_KEYS_NONE][VIRTUAL_3].push( GAME_INPUT_SPEED_FASTER_HASH );
  //...
        
	

Later on, I added a button to reset any key binding that's been made. 

It loops through the keys that could have been mapped differently and reset them, using a freshly initialized keymap and use the initial values for each hash. 

          		
static void
reset_keybinds( default_ui_system& ui, key_remapper& kr, const localization_manager& loc ) {
  for ( const remappable_key_info& rki : remapped_keys ) {
    command_option             co     = command_option_lookup( rki._command_option_name );
    const remappable_key_info* df_rki = &rki;
    co.clear();
    u64 b = update_key_bind( kr, loc, df_rki );
    ASSERT( b != 0 );
    property_comp pc = {};
    pc.set_value( b );
    key_value_component_system_set_value(
      &ui._key_value_system, ui._master_ui_entity, wc_hash_string( rki._localization_str ), pc );
  }
}
        
	
          
void
gamegfx_base::reset_all_keybinds() {
  reset_keybinds( _ui, _key_remapper, _gfx->get_gamecore()->get_localization() );
  register_settings( true );
}

//UI
{ PCT_UNKNOWN,
  0,
  "RESET_KEYBINDS",
  PCT_UNKNOWN,
  0,
  ggfx_base,
  []( const property_chain_entry_data* data ) -> sys_u64 {
    PROPERTY_CHAIN_UNPACK( data )
    PROPERTY_CHAIN_UNPACK_USERDATA( gamegfx_base, base );
    base.reset_all_keybinds();
    out->set_void();
    return 0;
  } },
        
	
          		
NEW_ENTITY_WITH_PARENT( HEADER, nullptr, BASE )
ADD_UI_BOUNDS_POSITION_ANCHOR( 0, -10, UI_BOUNDS_CENTER_TOP )
ADD_UI_FONT_LOCALIZE_TEXT( "MISSION_ABANDON_WARNING", "standard_header_small", FF_ALIGN_CENTER )

NEW_ENTITY_WITH_PARENT( DESC, nullptr, BASE )
ADD_UI_BOUNDS_POSITION_ANCHOR( 0, 10, UI_BOUNDS_CENTER )
ADD_UI_FONT_LOCALIZE_TEXT( "OPTION_RESET_KEYBINDINGS_WARNING", "standard_small", FF_ALIGN_CENTER )

NEW_ENTITY_MAKE_UI_BUTTON( YESBUTTON, "ui/button/button_decline", BASE )
ADD_UI_BOUNDS_POSITION_ANCHOR( 14, 10, UI_BOUNDS_LEFT_BOTTOM )
ON_CLICK_SEND_EVENT( TDTD_EVENT_DEPOPUP )
ON_CLICK_SET_PROPERTY( "RESET_KEYBINDS" )

NEW_UNNAMED_ENTITY_WITH_PARENT( nullptr, YESBUTTON )
ADD_UI_BOUNDS_POSITION_ANCHOR( 0, 0, UI_BOUNDS_CENTER )
ADD_UI_FONT_LOCALIZE_TEXT( "WORD_YES", "title_small", FF_ALIGN_CENTER )
ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
        
	

Name Component

Since the game is about control and managing, it seemed only logical for the player to have the option to rename its buildings and workers accordingly for easier supervision.


This was commonly used by the player to easily organize their colony.


The workers have a "name component", but the buildings and chests did not at first. So, I made it possible to check the components to see if there is a name somewhere that can be changed. And if it doesn't have one, a name component will be created.  


I also had to connect the new name into the UI list. A lot of the work I did was connected somehow to the UI.

          		
    const room_record* room = room_system_get_room_record( &ms->_room_system, arg->_entity );
    if ( room ) {
      list_of_tags[num_tags_to_test] = &room->_tag;
      num_tags_to_test++;
    }

    const production_queue_entry* entry =
      production_system_get_queue( &ms->_production_system, arg->_entity, 0 );
    if ( entry ) {
      list_of_tags[num_tags_to_test] = &entry->_recipe->_producer_classification[0];
      num_tags_to_test++;
    }
  }

  ASSERT( num_tags_to_test <= WC_ARR_LEN( list_of_tags ) );

  // First try entity lookup for 'gie', since it's faster
  if ( arg->_entity ) {
    gie = global_inventory_system_get_entity_entry( &ms->_global_inventory_system, arg->_entity );
  }

  if ( !gie && arg->_total_modifier_hash ) {
    gie =
      global_inventory_system_get_entry( &ms->_global_inventory_system, arg->_total_modifier_hash );
  }
        
	
          		
if ( arg->_entity ) {
    if ( !arg->_ignore_explicit_name ) {
      const char* name_maybe = name_system_get_full_name( &ms->_name_system, arg->_entity );
      if ( name_maybe ) {
        written += sys_snprintf( arg->_buf + written, arg->_buf_size - written, "%s", name_maybe );
        return written;
      }
    }
  }

  if ( gie ) {
    if ( arg->_entity && gie->_dynamic_name_hash ) {
      property_comp in = {};
      in.set_entity( arg->_entity, PC_ENTITY_TAG_GAME );

      const property_chain_header* pch       = nullptr;
      const char*                  loc_exist = localization_localization_manager_localize_hash(
        ms->_gamestate->_localization, gie->_dynamic_name_hash, &pch );

      ASSERT( loc_exist );
      written += property_print( arg->_buf + written,
                                 arg->_buf_size - written,
                                 in,
                                 &ms->_property_system,
                                 loc_exist,
                                 pch,
                                 nullptr,
                                 0 );
      return written;
    }

    written += sys_snprintf( arg->_buf + written, arg->_buf_size - written, "%s", gie->_name );
    return written;
  }

  for ( sys_u64 i = 0; i < num_tags_to_test; i++ ) {
    const char* loc_exist = localization_localization_manager_localize_safe_no_params(
      ms->_gamestate->_localization, list_of_tags[i]->_string );
    if ( loc_exist ) {
      written += sys_snprintf( arg->_buf + written, arg->_buf_size - written, "%s", loc_exist );
      return written;
    }
  }

  const char* final_name = "<unknown>";
  if ( arg->_entity == 0 ) {
    final_name = "";
  }

  written += sys_snprintf( arg->_buf + written, arg->_buf_size - written, "%s", final_name );
  return written;
        
	

Set Job Priority

Job prio


There are jobs that are quite more important than others, such as hauling items for timed missions, or restoring buildings that have taken damage from attacking enemies. 


I made it possible for the player to tweak these values and organize itself what job seems more important. Maybe you really need bandages to heal your workers, but they are all focus on doing something way less important.. One click and you make sure they will priorities what's important!


The player can set priority on either the individual job, or, set priority on the entire workplace so that each job created at that workplace gets the pre-set priority. 



          		
    void
    job_broker_system_set_job_priority( entity_component_system* ecs,
                                      job_id                   job,
                                      enum job_priority        prio ) {
    job_broker_system* system   = cast_to_system( ecs );
    job_instance&      instance = system->access_job( job );
    instance._priority          = prio;
    }
        
	
          		
static void
command_job_prio_execute( const struct player_command_execute_context*     ctx,
                          const struct player_command_data_property_comps* data,
                          const struct player_command*                     cmd ) {

  WC_UNUSED( cmd );
  ASSERT( data->_comp_count == 2 );

  job_id jid                                 = INVALID_JOB_ID;
  jid._internal                              = static_cast<u32>( data->_comps[0].get_u64() );
  job_priority             prio              = job_priority( data->_comps[1].get_u64() );
  entity_component_system* job_broker_system = &ctx->_gs->_mainstate->_job_broker_system;
  workplace_id             wid = job_broker_system_get_job_workplace( job_broker_system, jid );
  entity                   ent = job_broker_system_get_workplace_entity( job_broker_system, wid );
  ASSERT( prio >= 0 );
  ASSERT( prio <= 2 );
  const job_type* jtp = nullptr;
  job_broker_system_get_job_type( job_broker_system, jid, &jtp );

  if ( jtp->_tag == "JOB_HAUL" ) {
    haul_system_set_job_priority( &ctx->_gs->_mainstate->_haul_system, ent, prio );
  }
  else {
    job_broker_system_set_job_priority( job_broker_system, jid, prio );
  }
}
        
	
          		
    { PCT_POINTER,
      PC_PTR_TAG_JOB_LIST_CATEGORY_ENTRIES_UI_DATA,
      "JOB_PRIO",
      PCT_U64,
      0,
      gstate,
      []( const property_chain_entry_data* data ) -> sys_u64 {
        PROPERTY_CHAIN_UNPACK( data );
        const job_list_category_entry_ui* jinc = in.get_pointer<job_list_category_entry_ui>();
        out->set_value( u64( jinc->_job_instance->_job_prio ) );
        return 0;
      } },
      
    NEW_ENTITY_MAKE_UI_BUTTON( LOW_PRIO, "ui/button/button32", CUT )
    ADD_PROPERTY_QUERY_COMP( "ENABLE(GAME_CTX.JOB_PRIO.EQUALS(UINT( '" PRIO_LOW_STR " ')))" )
    ADD_UI_BOUNDS_POSITION_ANCHOR( SPACING * 1, HEIGHT, UI_BOUNDS_LEFT_TOP )
    ADD_STRING_COMP( "tooltip_0", "ORDER_PRIO_LOW" )
    ADD_STRING_COMP( "tooltip_1", "TOOLTIP_SET_PRIO" )
    ON_CLICK_SET_PROPERTY( "EXECUTE_COMMAND( 'change_job_prio', GAME_CTX.JOB_ID,UINT('" PRIO_LOW_STR "'))" )
    NEW_UNNAMED_ENTITY_MAKE_UI_IMAGE_ANCHOR( "ui/icon/32/order_prio_low.ui.img", 0, 0, UI_BOUNDS_LEFT_TOP, LOW_PRIO )
    ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
    
    NEW_ENTITY_MAKE_UI_BUTTON( NORMAL_PRIO, "ui/button/button32", CUT )
    ADD_PROPERTY_QUERY_COMP( "ENABLE(GAME_CTX.JOB_PRIO.EQUALS(UINT('" PRIO_NORMAL_STR "')))" )
    ADD_UI_BOUNDS_POSITION_ANCHOR( SPACING * 2, HEIGHT, UI_BOUNDS_LEFT_TOP )
    ADD_STRING_COMP( "tooltip_0", "ORDER_PRIO_NORMAL" )
    ADD_STRING_COMP( "tooltip_1", "TOOLTIP_SET_PRIO" )
    ON_CLICK_SET_PROPERTY( "EXECUTE_COMMAND( 'change_job_prio', GAME_CTX.JOB_ID,UINT('" PRIO_NORMAL_STR "'))" )
    NEW_UNNAMED_ENTITY_MAKE_UI_IMAGE_ANCHOR( "ui/icon/32/order_prio_normal.ui.img", 0, 0, UI_BOUNDS_LEFT_TOP, NORMAL_PRIO )
    ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
    
    NEW_ENTITY_MAKE_UI_BUTTON( HIGH_PRIO, "ui/button/button32", CUT )
    ADD_PROPERTY_QUERY_COMP( "ENABLE(GAME_CTX.JOB_PRIO.EQUALS(UINT('" PRIO_HIGH_STR "')))" )
    ADD_UI_BOUNDS_POSITION_ANCHOR( SPACING * 3, HEIGHT, UI_BOUNDS_LEFT_TOP )
    ADD_STRING_COMP( "tooltip_0", "ORDER_PRIO_HIGH" )
    ADD_STRING_COMP( "tooltip_1", "TOOLTIP_SET_PRIO" )
    ON_CLICK_SET_PROPERTY( "EXECUTE_COMMAND( 'change_job_prio', GAME_CTX.JOB_ID,UINT('" PRIO_HIGH_STR "') )" )
    NEW_UNNAMED_ENTITY_MAKE_UI_IMAGE_ANCHOR( "ui/icon/32/order_prio_high.ui.img", 0, 0, UI_BOUNDS_LEFT_TOP, HIGH_PRIO )
    ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
        
	

Drag order prio


I made a event based command for the players to do a drag-order on jobs that doesn't have a workplace such as building and mining. 


It was useful for the player when exploring and want to dig long tunnels. Before this command, sometimes the workers had other "more important" jobs to do but was confusing since the player wanted to continue explore. So, by simply increase the priority of the mining/building jobs further away made it possible to continue explore the cave.



          		
static void
command_execute( const struct player_command_execute_context* ctx,
                 const struct player_command_data_two_tiles*  data,
                 const struct player_command*                 cmd,
                 job_priority                                 priority ) {
  WC_UNUSED( cmd, ctx );
  entity_manager& em         = ctx->_gs->get_entity_manager();
  ivec2           start_tile = data->_tiles[0];
  ivec2           end_tile   = data->_tiles[1];
  if ( start_tile._x > end_tile._x ) {
    wc::swap( start_tile._x, end_tile._x );
  }
  if ( start_tile._y > end_tile._y ) {
    wc::swap( start_tile._y, end_tile._y );
  }
  entity_component_system family_system;
  entity_component_system order_system;
  entity_component_system transform_system;
  em.get_system( ORDER_SYSTEM, &order_system );
  em.get_system( FIXED_TRANSFORM_SYSTEM, &transform_system );
  const family_info* infos = family_component_system_get_family_info(
    &ctx->_gs->_mainstate->_family_system, nullptr, nullptr );
  array<entity>             ents;
  array<workplace_id>       wips;
  hybrid_array<order*, 256> orders;
  u64                       num_orders = 256;
  orders.resize( num_orders );
  order_system_get_orders( &order_system, nullptr, nullptr, orders.begin(), &num_orders );
  orders.resize( num_orders );
  if ( num_orders >= 256 ) {
    order_system_get_orders( &order_system, nullptr, nullptr, orders.begin(), &num_orders );
  }
  gamevec3  start_pos( start_tile._x, start_tile._y, 0 );
  gamevec3  end_pos( end_tile._x, end_tile._y, 0 );
  gamevec3* positions = fixed_transform_system_get_world_positions( &transform_system );
  for ( order* ord : orders ) {
    if ( ord->_status == ORDER_IN_CRITICAL_STATE ) {
      continue;
    }
    entity position_ent = INVALID_ENTITY;
    if ( ord->_type->_tag == ORDER_CRAFT ) {
      const order_craft_data_t* order_data =
        reinterpret_cast<const order_craft_data_t*>( ord->_data );
      if ( order_data->_producer_entity != INVALID_ENTITY ) {
        if ( infos[order_data->_producer_entity]._parent != ord->_order_entity ) {
          continue;
        }
        position_ent = order_data->_producer_entity;
      }
    }
    else if ( ord->_type->_tag == ORDER_DIG ) {
      position_ent = ord->_order_entity;
    }

    if ( position_ent ) {
      gamevec3& pos = positions[position_ent];
      if ( start_pos._x <= pos._x && pos._x <= end_pos._x && start_pos._y <= pos._y &&
           pos._y <= end_pos._y ) {
        ents.push( position_ent );
      }
    }
  }

  u64 wip_count = job_broker_system_get_workplaces(
    &ctx->_gs->_mainstate->_job_broker_system, ents.begin(), ents.size(), nullptr, 0 );
  wips.resize( wip_count );
  wip_count = job_broker_system_get_workplaces(
    &ctx->_gs->_mainstate->_job_broker_system, ents.begin(), ents.size(), wips.begin(), wips.size() );
  const job_id* out_jobs = nullptr;
  for ( workplace_id current_work_id : wips ) {
    ASSERT( current_work_id.is_valid() );
    u64 job_count = job_broker_system_get_workplace_jobs(
      &ctx->_gs->_mainstate->_job_broker_system, current_work_id, &out_jobs );
    for ( u64 i_job = 0; i_job < job_count; ++i_job ) {
      job_id job = out_jobs[i_job];
      job_broker_system_set_job_priority(
        &ctx->_gs->_mainstate->_job_broker_system, job, priority );
    }
  }
  for ( entity haul : ents ) {
    haul_system_set_job_priority( &ctx->_gs->_mainstate->_haul_system, haul, priority );
  }
}
        
	
          		
static void
command_execute_low( const struct player_command_execute_context* ctx,
                     const struct player_command_data_two_tiles*  data,
                     const struct player_command*                 cmd ) {
  command_execute( ctx, data, cmd, JOB_PRIO_LOW );
}
static void
command_execute_normal( const struct player_command_execute_context* ctx,
                        const struct player_command_data_two_tiles*  data,
                        const struct player_command*                 cmd ) {
  command_execute( ctx, data, cmd, JOB_PRIO_NORMAL );
}
static void
command_execute_high( const struct player_command_execute_context* ctx,
                      const struct player_command_data_two_tiles*  data,
                      const struct player_command*                 cmd ) {
  command_execute( ctx, data, cmd, JOB_PRIO_HIGH );
}
static player_command defs[] = {
  player_command( "order_prio_low",
                  "MAIN_MENU",
                  "ORDER_PRIO",
                  "ORDER_PRIO_LOW",
                  gamestate_default_player_command_is_active,
                  command_is_valid,
                  command_execute_low,
                  nullptr,
                  vec3( 0, 0, 0 ) ),
  player_command( "order_prio_normal",
                  "MAIN_MENU",
                  "ORDER_PRIO",
                  "ORDER_PRIO_NORMAL",
                  gamestate_default_player_command_is_active,
                  command_is_valid,
                  command_execute_normal,
                  nullptr,
                  vec3( 0, 0, 0 ) ),
  player_command( "order_prio_high",
                  "MAIN_MENU",
                  "ORDER_PRIO",
                  "ORDER_PRIO_HIGH",
                  gamestate_default_player_command_is_active,
                  command_is_valid,
                  command_execute_high,
                  nullptr,
                  vec3( 0, 0, 0 ) ),
};
        
	
          		
    NEW_ENTITY_WITH_PARENT( PRIO_COMMAND, "entity/ui/window/build_menu/command_subcategory_button_generate.entity", BASE )
    ADD_PROPERTY_CONTEXT_STRING_COMP( "ORDER_PRIO" );
        
	
          		
WKZ_COMPILE_ENTITY_PROLOGUE( BASE, nullptr )

// CTX is 'player_command'
MAKE_UI_BUTTON( "ui/button/button72_long" )
ADD_UI_BOUNDS_SIZE( 92, 130 )
ON_CLICK_SET_PROPERTY( "SET_ACTIVE_COMMAND( CTX )" )
ON_CLICK_SEND_EVENT( TDTD_EVENT_COMMAND_DONE )

NEW_ENTITY_MAKE_UI_IMAGE_ANCHOR( SLOT, "ui/shared_component/slot_72_legendary_pressed.ui.img", 0, -5, UI_BOUNDS_CENTER_TOP, BASE )
ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
ADD_COLOR_COMP( 255, 255, 255, 190, 0 )
ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )

WKZ_COMPILE_ENTITY_EPILOGUE
        
	
          		
WKZ_COMPILE_ENTITY_PROLOGUE( BASE, nullptr )
MAKE_UI_BUTTON( "ui/window/build_menu/buildbutton" )
ON_CLICK_SEND_EVENT( TDTD_EVENT_COMMAND_SUBCATEGORY_SELECTED )

NEW_ENTITY_WITH_PARENT( CAT_BUTTON, nullptr, BASE )
ADD_UI_WIDGET_RENDER
ADD_UI_PROPERTY_TEXTURE_ONCE( "CTX.TO_ICON_STRING.HASH", 0 )
ADD_UI_BOUNDS_SIZE_FROM_IMAGE( "ui/icon/build_menu_size_ref.ui.img" )
ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )

NEW_UNNAMED_ENTITY_MAKE_UI_IMAGE_ANIMATED( "ui/window/build_menu/orderborder.ui.img", 0, 0, BASE )
ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
ADD_PROPERTY_QUERY_COMP( "ENABLE(NOT(IS_LAST_CHILD))" )

WKZ_COMPILE_ENTITY_EPILOGUE
        
	
          		
WKZ_COMPILE_ENTITY_PROLOGUE( BASE, "entity/ui/popup/popup_base.entity" )

NEW_ENTITY_WITH_PARENT( CUT, nullptr, BASE )
ADD_UI_BOUNDS_SIZE_PARENT_PERCENTAGE( 1.0f, 1.0f )
ADD_UI_BOUNDS_POSITION( 0, 80 )
ADD_BITFLAG_COMP( UIFLAG_IGNORE_INPUT )
ON_EVENT_SET_PROPERTY( TDTD_EVENT_COMMAND_DONE, "PARENT.DESTROY_SELF" )
ON_EVENT_SET_PROPERTY( TDTD_EVENT_COMMAND_CATEGORY_SELECTED, "PARENT.DESTROY_SELF" )

NEW_UNNAMED_ENTITY_WITH_PARENT( nullptr, CUT )
ADD_UI_GRIDBOX_AUTO( "entity/ui/window/build_menu/command_button_subcategory_generate.entity", "PRIO_GAME.SELECT_COMMAND_SUBCATEGORY(CTX.CTX)", 4, UI_LAYOUT_GRIDBOX_LEFT_TO_RIGHT_BOTTOM_TO_TOP )
ADD_PROPERTY_QUERY_ONCE_COMP( "BOUNDS_SET_POS(CTX.BOUNDS_GLOBAL_X, '-660')" )
ADD_BITFLAG_COMP( UIFLAG_CLIP_TO_PARENT )
ADD_TRANSITION_COMP( TRANSITION_EASE_IN_OUT_QUARTIC, 0.2, "BOUNDS_SET_Y(ARG('0'))", "FLOAT('-660')", "FLOAT('0')" )

NEW_ENTITY_MAKE_UI_IMAGE_ANIMATED( BUTTON, "ui/window/build_menu/orderborder.ui.img", 0, 0, BASE)
ADD_PROPERTY_QUERY_ONCE_COMP( "BOUNDS_SET_POS(CTX.BOUNDS_GLOBAL_X, '7')" )

WKZ_COMPILE_ENTITY_EPILOGUE
        
	

Fluid Source

During my time at the company, fluid was added in form of water and lava, but still support for other fluids also (honey maybe?).


When fluids spawns into the map, they spawn through various "Fluid Sources" that is represented by a hole in the wall. The player can use for example "water" to craft stuff, such as beverages to boost the workers morale.  


The idea was when the player used any fluid to craft, new fluid should through the fluid sources. I fixed so that the fluid should spawn as intended. 

I also added an event on the fluid sources, so that when the source is discovered but the player, it spawns a small amount of fluid. This is so that cave can "feel a bit more alive". 

          		
static void event_callback_exploration_discovery( void*                       user_data,
                                                  sys_u64                     event_hash,
                                                  const struct property_comp* pc,
                                                  sys_u64                     pc_count );
struct fluid_source_system {
  fluid_source_system( const fluid_source_system_config* config ) {
    _entity_manager = config->_entity_manager;
    _gamestate      = config->_gamestate;
    _event_system   = config->_event_system;

    event_component_system_register_listener_safe( &_event_system,
                                                   TDTD_EVENT_EXPLORATION_DISCOVERY,
                                                   this,
                                                   event_callback_exploration_discovery );
  }

  array<entity>                 _entities;
  array<fluid_source_component> _components;
  entity_manager_h              _entity_manager;
  gamestate*                    _gamestate;
  entity_component_system       _event_system;
};   
    
static void
event_callback_exploration_discovery( void*                       user_data,
                                      sys_u64                     event_hash,
                                      const struct property_comp* pc,
                                      sys_u64                     pc_count ) {
  WC_UNUSED( event_hash, pc_count );
  fluid_source_system* system         = reinterpret_cast<fluid_source_system*>( user_data );
  entity               discovered_ent = pc[0].get_entity( PC_ENTITY_TAG_GAME );
  sys_u64              count          = system->_entities.size();
  for ( sys_u64 i_ent = 0; i_ent < count; i_ent++ ) {
    entity ent = system->_entities[i_ent];
    if ( discovered_ent != ent ) {
      continue;
    }
    fluid_source_component& comp   = system->_components[i_ent];
    comp._been_discovered          = true;
    comp._extra_fluid_on_discovery = DISCOVERY_FLUID_AMOUNT;
    break;
  }
}
        
	
          		
static void
fluid_source_system_update( entity_component_system* ecs, f64 dt, game_time_t time_now ) {
  if ( dt == 0 ) {
    return;
  }
  WC_UNUSED( ecs, dt, time_now );
  fluid_source_system* system = reinterpret_cast<fluid_source_system*>( ecs->_system );

  if ( dt == 0 ) {
    return;
  }

  const gamevec3* positions = fixed_transform_system_get_world_positions(
    &system->_gamestate->_mainstate->_transform_system );
  map& m = system->_gamestate->get_map();

  // entity_component_system key_value_system = system->_gamestate->_mainstate->_key_value_system;
  entity_component_system statusbar_system = system->_gamestate->_mainstate->_statusbar_system;
  random&                 rnd              = system->_gamestate->get_random();

  sys_u64 count = system->_entities.size();
  for ( sys_u64 i_ent = 0; i_ent < count; i_ent++ ) {
    entity                  ent                = system->_entities[i_ent];
    fluid_source_component& comp               = system->_components[i_ent];
    const gamevec3&         ent_pos            = positions[ent];
    ai_pos_t                ent_tile_pos       = { i32( ent_pos._x ), i32( ent_pos._y ) };
    u32                     tile_index         = m.get_index( ent_tile_pos );
    u32                     block_index        = m.get_block_index( ent_pos );
    BLOCK_TYPE              block_data         = m._block_data[block_index];
    u8                      biome_region_index = BLOCK_GET_BIOME_REGION( block_data );
    const biome_region&     br                 = m._biome_regions[biome_region_index];
    entity                  br_ent             = br._entity;
    // u32                     area               = br._area;
    if ( !comp._been_discovered &&
         time_now > 60 * 3) { // wait 3 minutes before logic with being discovered kicks in so the map
                           // fills in the start
      continue;
    }
    u32* tile = m._data + tile_index;
    if ( MAP_GET_IS_FRONT( *tile ) ) {
      continue;
    }

    if ( MAP_GET_FLUID_AMOUNT( *tile ) != 0 ) {
      continue;
    }

    u32 max_to_spawn = MAP_FLUID_AMOUNT_MAX;
    if ( br_ent ) {
      gamefp curr = 0;
      gamefp min  = 0;
      gamefp max  = 0;
      statusbar_system_get_statusbar_for_entity(
        &statusbar_system, br_ent, wc_hash_string( STATUSBAR_MAP_FLUID_BIOME ), &curr, &min, &max );
      ASSERT( min == 0 ); // Should always be zero?
      if ( curr <= min && comp._extra_fluid_on_discovery == 0 ) {
        continue;
      }

      max_to_spawn = wc::max<u32>( static_cast<u32>( curr + 1 ), comp._extra_fluid_on_discovery );
      ASSERT( max_to_spawn );
    }

    const u32 MIN_TO_SPAWN = 3u;
    if ( max_to_spawn < MIN_TO_SPAWN ) {
      continue;
    }

    u32 amount = wc::min( MIN_TO_SPAWN + rnd.get_bounded( 2u ), max_to_spawn );

    bool can_spawn = false;
    for ( i32 y = -2; y > -6; --y ) {
      for ( i32 x = -1; x < 2; ++x ) {
        u32 tile_index2 = m.get_index( ent_tile_pos._x + x, ent_tile_pos._y + y );
        u32 tile_data   = m._data[tile_index2];
        if ( MAP_IS_WALK_FILLED( tile_data ) ) {
          continue;
        }

        if ( MAP_FLUID_AMOUNT_MAX - MAP_GET_FLUID_AMOUNT( tile_data ) ) {
          can_spawn = true;
          break;
        }
      }
    }

    if ( !can_spawn ) {
      continue;
    }

    amount = wc::min<u32>( amount + MAP_GET_FLUID_AMOUNT( *tile ), MAP_FLUID_AMOUNT_MAX );
    comp._extra_fluid_on_discovery -= wc::min<u32>( amount , comp._extra_fluid_on_discovery );
    fluid_system_set_fluid_type_and_amount(
      &system->_gamestate->_mainstate->_fluid_system, tile, comp._fluid->_self_index, amount );
  }
}
        
	

I also for the first time worked with save/load game when I was working with fluid sources.

The most interesting take is to keep in mind that people might use different versions depending on when they saved their game. And if you add anything new into the mix you got to take that information into

consideration. 


Meaning, if you add anything in later version, you must make sure that data doesn't crash the previous versions. 

          		
namespace {
struct fluid_source_component_format_1 {
  i8       _output_level;
  wc_tag   _fluid;
  sys_bool _been_discovered;
  template <typename FUNC>
  void for_each_member( FUNC& func ) {
    func( _output_level );
    func( _fluid );
    func( _been_discovered );
  }
};
struct fluid_source_system_format_1 {
  array<entity>                          _entities;
  array<fluid_source_component_format_1> _components;
  template <typename FUNC>
  void for_each_member( FUNC& func ) {
    func( _entities );
    func( _components );
  }

  void push_to_system( fluid_source_system* system, const database* database ) {
    for ( u64 i_comp = 0; i_comp < _components.size(); ++i_comp ) {
      fluid_source_component comp = {};
      comp._been_discovered       = _components[i_comp]._been_discovered;
      comp._fluid =
        database_get_by_hash<fluid_record>( database, _components[i_comp]._fluid._hash );
      comp._output_level = _components[i_comp]._output_level;
      comp._extra_fluid_on_discovery = 0;
      system->_components.push( comp );
    }
    system->_entities = _entities;
  }
};
struct fluid_source_component_format_2 {
  i8       _output_level;
  wc_tag   _fluid;
  sys_bool _been_discovered;
  u32      _extra_fluid_on_discovery;
  template <typename FUNC>
  void for_each_member( FUNC& func ) {
    func( _output_level );
    func( _fluid );
    func( _been_discovered );
    func( _extra_fluid_on_discovery );
  }
};

struct fluid_source_system_format_2 {
  array<entity>                          _entities;
  array<fluid_source_component_format_2> _components;
  template <typename FUNC>
  void for_each_member( FUNC& func ) {
    func( _entities );
    func( _components );
  }
  void init_from_system( fluid_source_system* system ) {
    for ( u64 i_comp = 0; i_comp < system->_components.size(); ++i_comp ) {
      fluid_source_component_format_2 comp = {};
      comp._been_discovered                = system->_components[i_comp]._been_discovered;
      comp._fluid                          = system->_components[i_comp]._fluid->_tag;
      comp._output_level                   = system->_components[i_comp]._output_level;
      comp._extra_fluid_on_discovery       = system->_components[i_comp]._extra_fluid_on_discovery;

      _components.push( comp );
    }
    _entities = system->_entities;
  }
  void push_to_system( fluid_source_system* system, const database* database ) {
    for ( u64 i_comp = 0; i_comp < _components.size(); ++i_comp ) {
      fluid_source_component comp = {};
      comp._been_discovered       = _components[i_comp]._been_discovered;
      comp._fluid =
        database_get_by_hash<fluid_record>( database, _components[i_comp]._fluid._hash );
      comp._output_level = _components[i_comp]._output_level;
      comp._extra_fluid_on_discovery = _components[i_comp]._extra_fluid_on_discovery;
      system->_components.push( comp );
    }
    system->_entities = _entities;
  }
};
} // namespace
        
	
          		
sys_bool
ENTITY_PASS( fluid_source_system,
             SAVE,
             0,
             EPDT_EXECUTE_DEFAULT )( fluid_source_system* fluid_source_system,
                                     const char*          save_directory ) {

  save_version     version( 2, 0, 0 );
  save_file_writer writer( save_directory, SAVE_FILE, version );

  fluid_source_system_format_2 format;
  format.init_from_system( fluid_source_system );
  writer.write( format );

  return WC_TRUE;
}

sys_bool
ENTITY_PASS( fluid_source_system,
             LOAD,
             0,
             EPDT_EXECUTE_DEFAULT )( fluid_source_system* fluid_source_system,
                                     const wkz_archive_h* archive,
                                     const database*      database ) {

  if ( !wkz_archive_access_file( archive, SAVE_FILE, nullptr ) ) {
    return WC_TRUE;
  }

  save_version     version;
  save_file_reader reader( archive, SAVE_FILE, version, false );
  if ( version.get_major() == 1 ) {
    fluid_source_system_format_1 format;
    reader.read( format );
    format.push_to_system( fluid_source_system, database );
  }
  if ( version.get_major() >= 2 ) {
    fluid_source_system_format_2 format;
    reader.read( format );
    format.push_to_system( fluid_source_system, database );
  }

  return WC_TRUE;
}