Multiplayer Scoring


Tracks scores for players based on what they do in the world. Now you can see who really puts in the work!

Tweaks
1 year, 7 months ago
1.1
1.07K

g Compatibility with Autodrive

1 year, 10 months ago
(updated 1 year, 10 months ago)

Hi! I'm the maintainer of Autodrive, which allows you to control vehicles remotely. Most of the time, vehicles are moved by script; players can be driver or passenger, but can't steer a vehicle that's in automatic driving mode.

Vehicles can be equipped with "sensors" for different situation. For example, we have "gate sensors" (if equipped, gates will automatically open so the vehicle can pass through) and "biter alert sensors" (if the vehicle has weapons, it will shoot at enemies within range while driving). These will work out of the box if at least one player is riding in the vehicle. If the vehicle is empty, we put a dummy character inside the vehicle for as long as it is necessary. The dummy characters are neither connected to nor associated with a player.

On your info page, I've found this statement:

kills while in the tank go to the passenger unless you run over the enemy or are the sole operator

This is bound to crash when your mod is used together with Autodrive (AAI Programmable Vehicles may have issues, too). In your handler for on_entity_died, I've found this:

addScore(awardee.player.name, settings.global["MultiplayerScoring_Killing_Value"].value)

where

awardee = e.cause.get_driver() or e.cause.get_passenger()

Now, entity.get_driver() and entity.get_passenger() may return either LuaEntity (a character prototype) or LuaPlayer (see here). You should make sure that you can get a LuaPlayer for awardee before giving it a score:

awardee = e.cause.get_driver()

if awardee  then
  -- awardee is a character 
  if awardee.object_name ~= "LuaPlayer" then
    awardee = awardee.valid and awardee.player or awardee.associated_player
  end

  if awardee and awardee.valid then
    addScore(awardee.name, settings.global["MultiplayerScoring_Killing_Value"].value)
  end  
end
1 year, 10 months ago
(updated 1 year, 10 months ago)

Thanks I'll implement a fix for this potential bug. Done.

1 year, 10 months ago
(updated 1 year, 10 months ago)

Thank you! However, there still is room for failure:

if not awardee then
    return
elseif awardee.object_name == "LuaEntity" then
    awardee = awardee.player
    for _, player in pairs(game.players) do
        if player.name == awardee.name then

The check for awardee.object_name will always work because object_name can be read even if the object is not valid. The next line (awardee = awardee.player) will crash if awardee is not valid. Even if awardee was valid, awardee.player could still be nil, so you'd get a crash (trying to index awardee, a nil value) when checking for "player.name == awardee.name".

Also, looping over all players seems wrong:

for _, player in pairs(game.players) do
    if player.name == awardee.name then
        local multiplier = math.ceil(e.entity.prototype.max_health / 100)
        if e.entity.type == "unit" or e.entity.type == "turret" then
            addScore(awardee.name, settings.global["MultiplayerScoring_Killing_Value"].value * multiplier)
        elseif e.entity.type == "unit-spawner" then
           addScore(awardee.name, settings.global["MultiplayerScoring_Nest_Value"].value * multiplier)
        end
    else
     return
    end
end

Suppose awardee is player 2. The loop will start with player 1. In this case, player.name is not the same as awardee.name, so you take the "else" branch and return -- and will never get to check player 2.

You always want to add the score for just one player. So why don't you check directly if such a player exists? You can index game.players using player.index or player.name, so something like this would be more efficient:

if awardee and awardee.valid and game.players[awardee.name] and game.players[awardee.name].valid then
    -- Add score
else
    return
end
1 year, 10 months ago

Hrmmm. If there is no awardee it just returns, so there's no reason to validate it? If it returns an entity instead it just gets the player from the entity.

Yes the loop is very inefficient, but i couldn't find any reference to .valid in the API. is this a lua specific check?

On a side note. initially for the gui I was just updating the caption for each element, but for the life of me i couldn't get that to work properly with a dynamically ordered list. So now i'm destroying and recreating the table every time the score changes which is also very inefficient. Any ideas there?

1 year, 10 months ago
(updated 1 year, 10 months ago)

Hrmmm. If there is no awardee it just returns, so there's no reason to validate it?

OK, you're right: Whatever get_driver() returns should be valid.

If it returns an entity instead it just gets the player from the entity.

And that's right back where we started: If Autodrive is used, it's possible that the entity is independent of any player, so entity.player will be nil. :-)

Yes the loop is very inefficient, but i couldn't find any reference to .valid in the API. is this a lua specific check?

It's a property really every single object (entity, GUI element, technology, etc.) has. You always can access object.valid and object.object_name, but you only can access any other property (name, type, index, get_driver(), color, etc.) if object.valid is true.

On a side note. initially for the gui I was just updating the caption for each element, but for the life of me i couldn't get that to work properly with a dynamically ordered list. So now i'm destroying and recreating the table every time the score changes which is also very inefficient. Any ideas there?

Yes:

local column_count = 2
local f = main.add({type = "table", name = "scoretable", style = "mod_info_table", column_count = column_count})
f.add({type = "label", caption = "Player", style = "bold_label"})
f.add({type = "label", caption = "Score", style = "bold_label"})

There's no way you can distinguish the labels -- why? LuaGuiElement.add allows for optional name or index. I'm currently working on a GUI with a dynamic table myself. So I've defined a table with names for all my GUI elements. This is the table for the columns of my table:

table_columns = {
  -- Vehicle list
  icon                                  = p_vehicles_prefix.."vehicle_list_icon_",
  proto_name                            = p_vehicles_prefix.."vehicle_list_prototype_name_",
  id                                    = p_vehicles_prefix.."vehicle_list_id_",
  surface                         = p_vehicles_prefix.."vehicle_list_surface_",
  position                                = p_vehicles_prefix.."vehicle_list_position_",
  toggle_owned                          = p_vehicles_prefix.."vehicle_list_toggle_owned_",
  toggle_lock                           = p_vehicles_prefix.."vehicle_list_toggle_lock_",
  map_view                                = p_vehicles_prefix.."vehicle_list_toggle_map_",
}

When I add an entity to the table, it looks like this:

for v, vehicle in pairs(vehicles) do
  v_id = vehicle.unit_number

  -- Icon
  if not vehicle_list[gui_names.table_columns.icon..v_id] then
    element = vehicle_list.add({
      type = "sprite",
      name = gui_names.table_columns.icon..v_id,
      sprite = "item/"..GCKI.vehicle_prototypes_map[vehicle.type][vehicle.name].placeable_by,
    })
    GCKI.created_msg(element)
  end

  -- Prototype name
  if not vehicle_list[gui_names.table_columns.proto_name..v_id] then
    element = vehicle_list.add({
      type = "label",
      name = gui_names.table_columns.proto_name..v_id,
      caption = GCKI.loc_name(vehicle),
    })
    GCKI.created_msg(element)
  end
  …
end

So in the table, the column names will start with a fixed string and end with the entity's unit_number.

Now, it can happen that some other mod replaces entities I refer to. For example, AAI Programmable Vehicles will destroy a vehicle when it reaches a waypoint and then create a new one. It informs other mods via the remote interface when it has replaced an entity, so when AAI exchanges an entity, I update only the GUI elements for the old entity:

for c, column in pairs(column_names) do
  old_name = column..old_id
  new_name = column..new_id

  if gui_table[old_name] and gui_table[old_name].valid then
gui_table[old_name].name = new_name
  end
end

Similarly, to remove an entity, I use:

for v, vehicle in pairs(vehicles) do
  id = vehicle.unit_number

  -- Remove vehicle from GUI              --
  for c, column in pairs(columns) do
    col_name = gui_names.table_columns[column]..id
    if tab[col_name] and tab[col_name].valid then
      tab[col_name].destroy()
    end
  end
1 year, 10 months ago
(updated 1 year, 10 months ago)

If it returns an entity instead it just gets the player from the entity.

And that's right back where we started: If Autodrive is used, it's possible that the entity is independent of any player, so entity.player will be nil. :-)

Oh, yep. I missed that in my logic.

Yes the loop is very inefficient, but i couldn't find any reference to .valid in the API. is this a lua specific check?

It's a property really every single object (entity, GUI element, technology, etc.) has. You always can access object.valid and object.object_name, but you only can access any other property (name, type, index, get_driver(), color, etc.) if object.valid is true.

Oh so, I was trying to check if an entity was a ghost by doing if not entity.ghost_name but it errored with "entity is not a ghost". Could I have just done if not entity.ghost_name.valid ?

There's no way you can distinguish the labels -- why? LuaGuiElement.add allows for optional name or index. I'm currently working on a GUI with a dynamic table myself. So I've defined a table with names for all my GUI elements. This is the table for the columns of my table:

So to use the name parameter would require dynamic names, which I tried, but failed to do. The other issue I had with trying to update the caption is the key for the player name would always return nil, even though in the line before it had a value. spent 48 hours trying to debug just that one issue. gave up and rebuilt the table now.

1 year, 10 months ago
(updated 1 year, 10 months ago)

Oh so, I was trying to check if an entity was a ghost by doing if not entity.ghost_name but it errored with "entity is not a ghost". Could I have just done if not entity.ghost_name.valid ?

The properties listed here are all properties of entities, but not all entities will have all properties. For example, entity.neighbours makes sense for the wire connections of power poles, or for a piece of rail track, but not for assemblers; or get_driver() makes only sense for vehicles, but not a chest. Likewise, you only can access entity.ghost_name if entity.valid and entity.type == "entity-ghost".

There's no way you can distinguish the labels -- why? LuaGuiElement.add allows for optional name or index. I'm currently working on a GUI with a dynamic table myself. So I've defined a table with names for all my GUI elements. This is the table for the columns of my table:

So to use the name parameter would require dynamic names, which I tried, but failed to do.

So, you've got a table

local rank = Utils.sortedKeys(player_score)

that looks like

{ [1] = "Player 3", [2] = "Player 1", [3] = "Player 2"}

Then you could create your table like this:

local column_count = 2
if not main["scoretable"] then
  local f = main.add({type = "table", name = "scoretable", style = "mod_info_table", column_count = column_count})

  f.add({type = "label", caption = "Player", style = "bold_label", name = "Player-header"})
  f.add({type = "label", caption = "Score", style = "bold_label", name = "Score-header"})
end

local p, s
local tab = main["scoretable"]

for p_rank, p_name in pairs(rank) do
  p = "Player-"..tostring(p_rank)
  s = "Score-"..tostring(p_rank)

  if not (tab[p] and tab[p].valid) then
tab.add({type = "label", name = p, caption = p_name, style = "bold_label"})
  else
tab[p].caption = p_name
  end

  if not (tab[s] and tab[s].valid) then
tab.add({type = "label", name = s, caption = player_score[p_name], style = "bold_label"})
  else
tab[s].caption = p_name
  end
end

This will create new named labels or update existing labels, so it's no problem if a new player is added to the game. When a player is are permanently removed, you could remove the last line of the table, or clear the table and rebuild it. (By the way, destroy() is too radical -- clear() will keep the table itself, but remove all child elements, i.e. just the labels you've created.)

The other issue I had with trying to update the caption is the key for the player name would always return nil, even though in the line before it had a value. spent 48 hours trying to debug just that one issue. gave up and rebuilt the table now.

Hard to tell what's wrong there, without code to look at. I recommend to log all values that you set. Sometimes it's really something stupid, like a value has not the type you'd expected.

1 year, 10 months ago

I'm going to refactor all of that when I have time. I appreciate the help!

1 year, 10 months ago

You're welcome! (By the way, I've forgotten the parentheses in "if not (tab[p] and tab[p].valid)" and the other block, fixed that.)

1 year, 10 months ago

Thanks, I have worked in your suggestions for the vehicles and some gui things. I still cannot get updating the captions to work. It seems to not want to recognize keys in the table when I try this even though it has nothing to do with the gui. I must be missing something.

P.S. Sent you a PM on Factorio forums.

New response