Using on_research_finished to spawn a cargo wagon on a train

Place to get help with not working mods / modding interface.
Post Reply
Doctor_Willis
Burner Inserter
Burner Inserter
Posts: 16
Joined: Tue Sep 26, 2023 11:45 am
Contact:

Using on_research_finished to spawn a cargo wagon on a train

Post by Doctor_Willis »

Hi all,
I'm at a bit of a loss with this one.
Is it possible to spawn a wagon and attach it to locomotive in response to research finishing?
The locomotive already exists in the scenario, and may already have wagons attached to it, and I would like to use research to add an extra wagon.
I think I need to use find_entities_filtered to get the train, and then maybe use cargo_wagons to add one to it? Maybe?

I was also thinking, if that won't work, perhaps there would be a way to wait until the train has reached a station, then spawn the wagon with the necessary coordinates? For example, once the event happens, I use go_to_station to make sure the train goes to the right one, and once it's there spawn the wagon.
I'm just not sure if this approach would stop the game from responding to other on_research_finished events until this one is complete.

I'm just not sure what the best way is.

If it helps, there are only two stations.
Sorry I can't add any example code for this one. I'm completely stumped on it.

Natha
Fast Inserter
Fast Inserter
Posts: 183
Joined: Sun Mar 15, 2015 1:48 pm
Contact:

Re: Using on_research_finished to spawn a cargo wagon on a train

Post by Natha »

You have to spawn the wagon and then use https://lua-api.factorio.com/latest/cla ... ling_stock.
But I don't know if creating wagons on the right spots on rails is doable.
Doctor_Willis wrote:
Sun Apr 21, 2024 8:19 am
I'm just not sure if this approach would stop the game from responding to other on_research_finished events until this one is complete.
The game does not wait for the train to reach the station.

Pi-C
Smart Inserter
Smart Inserter
Posts: 1654
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Using on_research_finished to spawn a cargo wagon on a train

Post by Pi-C »

Natha wrote:
Sun Apr 21, 2024 12:07 pm
The game does not wait for the train to reach the station.
But you can schedule your action for a later time.

I didn't try this, so there is a chance it doesn't work quite as I imagine. But I'd go about it something like this:
  • When the research is finished, locate the train.
  • If you can read LuaTrain:station, (i.e. if it doesn't return nil), the train is stopped at a station, so you can get LuaTrain:back_rail and use it to place + connect the new wagon.
  • If LuaTrain:station is nil, store LuaTrain::id in your global.table: e.g. global.wait_for_stop[train.id] = true. Add a handler for defines.events.on_train_changed_state.
  • When the event triggers and you find event.train.id is stored in global.wait_for_stop, and if event.train.state == defines.train_state.wait_station, the train has arrived and you can then try to create/connect a wagon. (Don't forget to remove the ID from global.wait_for_stop or whatever you've named the table!)
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

Doctor_Willis
Burner Inserter
Burner Inserter
Posts: 16
Joined: Tue Sep 26, 2023 11:45 am
Contact:

Re: Using on_research_finished to spawn a cargo wagon on a train

Post by Doctor_Willis »

Thanks for the assistance. Both suggestions put me on the right track. I didn't end up needing a global table, but I got it to work:

Code: Select all

local function createCargoWagon(new_carriage_coordinates)
    local cargo_wagon = game.surfaces[1].create_entity{name = "cargo-wagon", position = new_carriage_coordinates, move_stuck_players = true, force = game.forces.neutral}
    cargo_wagon.connect_rolling_stock(defines.rail_direction.front)
    
    local new_trains = game.surfaces[1].find_entities_filtered({type = "locomotive"})
    if #new_trains > 0 then
        local new_train = new_trains[1].train
        new_train.manual_mode = false
    end
end

local function onTrainStateChanged(event, train, new_carriage_coordinates)
    local changed_train = event.train
    if changed_train == train and changed_train.state == defines.train_state.wait_station then
        createCargoWagon(new_carriage_coordinates)
        script.on_event(defines.events.on_train_changed_state, nil)
    end
end

script.on_event(defines.events.on_research_finished, function(event)
    local researchFinished = {
        ["train-carriage-2"] = function()
            local trains = game.surfaces[1].find_entities_filtered({type = "locomotive"})

            if #trains > 0 then
                local train = trains[1].train
                local loading_station = 1
                local current_station = train.schedule.current
                local new_carriage_coordinates = {-129.5,-83}

                if current_station == loading_station and train.state == defines.train_state.wait_station then
                    createCargoWagon(new_carriage_coordinates)
                elseif current_station == loading_station and train.state ~= defines.train_state.wait_station then
                    script.on_event(defines.events.on_train_changed_state, function(event)
                        onTrainStateChanged(event, train, new_carriage_coordinates)
                    end)
                elseif current_station ~= loading_station then
                    train.go_to_station(1)
                    script.on_event(defines.events.on_train_changed_state, function(event)
                        onTrainStateChanged(event, train, new_carriage_coordinates)
                    end)
                end
            end
        end
    }
  
    if researchFinished[event.research.name] then
        researchFinished[event.research.name]()
    end
end)
It's probably a bit clunky in a few places, but it works for me so I'm happy

Pi-C
Smart Inserter
Smart Inserter
Posts: 1654
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Using on_research_finished to spawn a cargo wagon on a train

Post by Pi-C »

Doctor_Willis wrote:
Thu Apr 25, 2024 12:52 am
Thanks for the assistance. Both suggestions put me on the right track. I didn't end up needing a global table, but I got it to work:
Congrats to your success! However, I think your work isn't finished yet. While it may work in single-player mode, you should expect desyncs in multiplayer games if a player joins after the event has run.

All players must have the same game state. Keep a list of the optional events in your global table; add the name of an event to the list when you enable the event, and remove it from the list when you disable it. Then, in script.on_load, go over the list and add the handler for each stored event. This way, joining players will listen to the same events as players who already were connected.

Code: Select all

script.on_event(defines.events.on_research_finished, function(event)
    local researchFinished = {
        ["train-carriage-2"] = function()
            local trains = game.surfaces[1].find_entities_filtered({type = "locomotive"})

            if #trains > 0 then
                local train = trains[1].train
                local loading_station = 1
                local current_station = train.schedule.current
                local new_carriage_coordinates = {-129.5,-83}

                if current_station == loading_station and train.state == defines.train_state.wait_station then
                    createCargoWagon(new_carriage_coordinates)
                elseif current_station == loading_station and train.state ~= defines.train_state.wait_station then
                    script.on_event(defines.events.on_train_changed_state, function(event)
                        onTrainStateChanged(event, train, new_carriage_coordinates)
                    end)
                elseif current_station ~= loading_station then
                    train.go_to_station(1)
                    script.on_event(defines.events.on_train_changed_state, function(event)
                        onTrainStateChanged(event, train, new_carriage_coordinates)
                    end)
                end
            end
        end
    }
  
    if researchFinished[event.research.name] then
        researchFinished[event.research.name]()
    end
end)
Do you only have the technology "train-carriage-2"], or are there more levels? It seems your script would never advance to "train-carriage-3" or higher.

In (most?) vanilla scenarios, all players will belong to game.forces["player"]. But in modded or in multiplayer games, there may be several forces, and each force has its own set of researched technologies. You should make sure that you update the train of the force that finished the research.

You always set new_carriage_coordinates to the same hard-coded position. That may work in your specific setup, where you've made sure that there are rails at {-129.5,-83}, but what about players who have built there base differently? What happens if the rails at that position are destroyed?

Code: Select all

local trains = game.surfaces[1].find_entities_filtered({type = "locomotive"})
if #trains > 0 then
  local train = trains[1].train
  …
end
That will fail if the train is on another surface. It may also break if there are several different trains on the same surface: the first locomotive found is not guaranteed to belong to the train you want.
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

Doctor_Willis
Burner Inserter
Burner Inserter
Posts: 16
Joined: Tue Sep 26, 2023 11:45 am
Contact:

Re: Using on_research_finished to spawn a cargo wagon on a train

Post by Doctor_Willis »

Thank you for the input!
I'm still new to modding and still finding my feet.
I wasn't aware of the multiplayer issue. Would you mind going over it with a more concrete example/how to? I don't fully understand it.

And thanks for the heads up about the force. I'll make sure to change that to "player".

As for the other issues, they won't be a problem. This mod is never going public. I'm making it for a local tech-school, and using a very specific scenario where there will only be one train with the rails and train stops in specific coordinates.
And yes, there are additional research levels, and I'm currently in the process of factoring out the repeated code into functions, so it will be able to adapt to new research.

Pi-C
Smart Inserter
Smart Inserter
Posts: 1654
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Using on_research_finished to spawn a cargo wagon on a train

Post by Pi-C »

Doctor_Willis wrote:
Thu Apr 25, 2024 8:00 am
Thank you for the input!
I'm still new to modding and still finding my feet.
I wasn't aware of the multiplayer issue. Would you mind going over it with a more concrete example/how to? I don't fully understand it.
An example from my latest mod:

Code: Select all

-- Map event names to event handlers
local event_handlers = {}

-- Map event names to event filters
local event_filters = {}

LCOE.optional_events = {
  chunks = {
    on_chunk_charted = LCOE_surfaces.on_chunk_charted,
  },
  chart_tags = {
    on_chart_tag_modified = LCOE_charttags.on_chart_tag_changed,
    on_chart_tag_removed = LCOE_charttags.on_chart_tag_changed,
  },
  portals = {
    on_entity_damaged = LCOE_surfaces.on_entity_damaged,
    on_entity_destroyed = LCOE_portals.restore_exit_portal,
    on_gui_opened = LCOE_player.player_used_portal
  },
}

-- Add filters for optional events
event_filters.on_entity_damaged = {
  {filter = "damage-type", type = "explosion"},
}

  event_handlers.on_player_joined_game = LCOE_player.on_player_joined_game
  event_handlers.on_pre_player_left_game = LCOE_player.on_pre_player_left_game
  event_handlers.on_pre_player_removed = LCOE_player.remove_player
  event_handlers.on_player_died = LCOE_player.on_player_died
  event_handlers.on_post_entity_died = LCOE_player.on_post_entity_died
  event_filters.on_post_entity_died = {
    {filter = "type", type = "character"}
  }

  event_handlers.on_character_corpse_expired = LCOE_corpse.remove_corpse_data

  event_handlers.on_pre_player_mined_item = LCOE_corpse.remove_corpse_data
  event_filters.on_pre_player_mined_item = {
    {filter = "type", type = "character-corpse"}
  }
  …
  
------------------------------------------------------------------------------------
--                            REGISTER EVENT HANDLERS!                            --
------------------------------------------------------------------------------------
LCOE_events.attach_events = function(event_list)

    LCOE.assert(event_list, {"table", "nil"},
                              "table of event names and handlers or nil")

    local id, filters
    for event_name, event_handler in pairs(event_list or event_handlers) do
      id = defines.events[event_name]

      -- Register event
      script.on_event(id, function(event)
        event_handler(event)
      end)

      -- Set event filters?
      filters = event_filters[event_name]
      if filters and next(filters) then
        script.set_event_filter(id, filters)
      end
    end
end


------------------------------------------------------------------------------------
--                        REGISTER OPTIONAL EVENT HANDLERS!                       --
------------------------------------------------------------------------------------
LCOE_events.register_optional_events = function(group_name, event_name)

  LCOE.assert(group_name, {"string", "nil"}, "name of table in LCOE.optional_events or nil")
  LCOE.assert(event_name, {"string", "nil"}, "event_name or nil")

  local event_list = LCOE.optional_events[group_name]

  -- Attach single event
  if event_name then
    if event_list and event_list[event_name] then
      LCOE_events.attach_events({[event_name] = event_list[event_name]})
      global.optional_events[event_name] = group_name
    end

  -- Attach group of events
  elseif event_list then
    LCOE_events.register_optional_events(group)
    for e_name, e_handler in pairs(event_list) do
      global.optional_events[e_name] = group_name
    end
    
  -- Re-attach all events that are currently stored in global.optional_events
  else
    for e_name, group in pairs(global.optional_events) do
      LCOE_events.register_optional_events(group, e_name)
    end
  end
end

------------------------------------------------------------------------------------
--                           UNREGISTER EVENT HANDLERS!                           --
------------------------------------------------------------------------------------
LCOE_events.detach_events = function(event_list, keep_events)
  LCOE.assert(event_list, {"table", "nil"},
                            "table of event names/handlers or nil")
  LCOE.assert(keep_events, {"table", "nil"},
                            "table of event names to keep attached, or nil")

  local keep_names = {}
  for n, name in pairs(keep_events or {}) do
    keep_names[name] = true
  end

  for event_name, event_handler in pairs(event_list or event_handlers) do
    if keep_names[event_name] then
	-- Ignore event
    elseif script.get_event_handler(event_names[event_name]) then
      -- Unregister event
      script.on_event(event_names[event_name], nil)

      -- Remove optional event from global.optional_events?
      if event_list then
        global.optional_events[event_name] = nil
      end
    end
  end
end

-- Mod is running for the first time in this game, or
-- mods have been added/updated/removed
local function init()
  …
  LCOE_events.attach_events()
  LCOE_events.register_optional_events()
  …
end
script.on_init(init)
script.on_configuration_changed(init)

-- Saved game is loaded, we have read access to table "global", but 
-- can't write to it!
script.on_load(function()
  …
  -- Attach events that we always need
  LCOE_events.attach_events()

  -- Attach all optional events that are stored in global.optional_events
  LCOE_events.register_optional_events()
  …
end)

-- Example: When a chart_tag has been added or removed by the mod,
-- this function is called to register/unregister handlers for the optional 
-- events defined in LCOE.optional_events["chart_tags"].
LCOE_charttags.check_optional_events = function()
  local need_event = false

  -- We need events if at least one chart_tag has been stored for at least
  -- one force
  for f_name, f_data in pairs(global.force_data) do
    for t_name, t_data in pairs(f_data.chart_tags or {}) do
      if t_data.tag and t_data.tag.valid then
        need_event = true
        break
      end
    end
  end

  if need_event then
    LCOE_events.register_optional_events("chart_tags")
  else
    LCOE_events.detach_events(LCOE.optional_events["chart_tags"])
  end
end
This is perhaps a bit more complicated than what you may need because I've made the code flexible enough to handle single events as well as a group of events. The general idea is: If you turn on the handler for an optional event, store it in the global table, if you turn it off, remove the event from the global table. When a game is loaded (this will be triggered when a player is joining a running multiplayer game), register the basic events that are always needed, as well as the optional events that are stored in the global table.

And thanks for the heads up about the force. I'll make sure to change that to "player".

As for the other issues, they won't be a problem. This mod is never going public. I'm making it for a local tech-school, and using a very specific scenario where there will only be one train with the rails and train stops in specific coordinates.
OK, you'll probably get along with fixed values for force and surface in this case! I'm still doubtful about the position, though. Even if the position of the train stop will never change, your train will get longer, so after you've added a wagon, that position will be blocked. Shouldn't you try to determine the position for the new wagon based on the actual length of the train?
And yes, there are additional research levels, and I'm currently in the process of factoring out the repeated code into functions, so it will be able to adapt to new research.
Yes, I guess one function that is called with different arguments (e.g. the tech-level) would be enough …
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

Post Reply

Return to “Modding help”