New features via Lua script

Can you help improve your favourite game? Hardcore C mages, talented artists, and players with any level of experience are welcome!
Ignatus
Elite
Posts: 648
Joined: Mon Nov 06, 2017 12:05 pm
Location: St.Petersburg, Russia
Contact:

Re: New features via Lua script

Post by Ignatus »

Molo_Parko wrote: Sat Nov 25, 2023 9:14 pm AiTribesCityDefender Forces computer-controlled tribes to keep a unit in every city. If the only unit in a city moves out of the city, it is moved back into the city.
What about :movement_disallow() method?
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

^ I use Freeciv 2.6.4 which does not have that feature. It's only available from Freeciv 3.0 and later. I could add it as alternate method when Freeciv version is 3 or more though.

EDIT: Now that I think about it, that might hinder the computer-controlled tribe from choosing a better defender -- since the Warriors can't leave, they might not also build a Phalanx, Pikemen, Musketeer, or Rifleman unit to defend the city when those options are available. The way the current script handles it is to only prevent the -last- unit in a city from leaving, so if a Warriors is in the city, and a Musketeers is built, then the Warriors can leave or be disbanded without triggering the script since the city isn't empty because a better unit is in it.
Last edited by Molo_Parko on Thu Nov 30, 2023 8:21 pm, edited 1 time in total.
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

RailMoveLimit limits land units to maximum 9 tile moves per turn -- even on Railroad.

Code: Select all

--##############################################################################
function Lua_RailMoveLimitRefresh(turn, year)

	-- Clear and re-create the global table of unit RailMoves
	RailMoves=nil RailMoves={}

end
signal.connect("turn_started", "Lua_RailMoveLimitRefresh")


--##############################################################################
function Lua_RailMoveLimit(unit, src_tile, dst_tile)

	-- All land units limited to maximum 9 tiles of movement on Railroad

	if ( RailMoveLimit == nil ) then RailMoveLimit=true end
	if not ( RailMoveLimit ) then return end

	-- Prevents looping forever
	if ( RailMoveLimitInProgress == nil ) then RailMoveLimitInProgress=false end
	if ( RailMoveLimitInProgress ) then return end
	RailMoveLimitInProgress=true

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","RailMoveLimit BEGIN") end

	-- Freeciv 2.6.4 Lua can't determine whether a unit's class is land, sea, or air
	if ( unit.utype.id == find.unit_type("Fighter").id ) 
	or ( unit.utype.id == find.unit_type("Bomber").id ) 
	or ( unit.utype.id == find.unit_type("Helicopter").id ) 
	or ( unit.utype.id == find.unit_type("Stealth Fighter").id ) 
	or ( unit.utype.id == find.unit_type("Stealth Bomber").id ) 
	or ( unit.utype.id == find.unit_type("Cruise Missile").id )
	or ( unit.utype.id == find.unit_type("Nuclear").id )
	or ( unit.utype.id == find.unit_type("AWACS").id ) then 
		return
	end

	if ( RailMoves[(unit).id] == nil ) then RailMoves[(unit).id]=0 end

	if ( (src_tile):has_extra("Railroad") ) 
	and ( (dst_tile):has_extra("Railroad") ) then 

		-- Each rail-to-rail tile movement counts toward 9 total
		RailMoves[(unit).id]=( RailMoves[(unit).id] + 1 )

		-- Every 3rd rail-to-rail move should use 1 fragment of unit movement
		if ( RailMoves[(unit).id] % 3 == 0 ) then 
			edit.unit_move(unit,dst_tile,1)
		end

		-- Upper limit
		if ( RailMoves[(unit).id] >= 9 ) then 
			-- Remove all unit movement points
			edit.unit_move(unit,dst_tile,99)
		end

	else 
		-- Every move counts toward 9 maximum moves on Railroad
		RailMoves[(unit).id]=( RailMoves[(unit).id] + 1 )
	end

	if ( debugLuaScripts ) then log.error("%s","RailMoveLimit END") end
	RailMoveLimitInProgress=false
end
signal.connect("unit_moved", "Lua_RailMoveLimit")
Attached Scenario file "RandoMap" has only the RailMoveLimit Lua scripts for testing.
Attachments
RandoMap with RailMoveLimit ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(8.89 KiB) Downloaded 416 times
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

ExtrasUpkeep charges each player for tile extras within the radius of the player's cities. I'm still experimenting with the costs (in gold) per tile-extra-type. Here's the current list:
-- Upkeep cost per tile extra
cityExtraCosts={
["Airbase"]=1,
["Buoy"]=.1,
["Fallout"]=0,
["Farmland"]=.3,
["Fortress"]=.6,
["Hut"]=0,
["Irrigation"]=.07,
["Mine"]=.09,
["Oil Well"]=.3,
["Pollution"]=0,
["Railroad"]=.5,
["River"]=0,
["Road"]=.05,
["Ruins"]=-.1
} -- Set fee rate per extra, per turn in list above, example Road=.1 (gold)
Image
Image

Code: Select all

--##############################################################################
function Lua_ExtrasUpkeep(turn, year)

	if ( ExtrasUpkeep == nil ) then ExtrasUpkeep=true end
	if not ( ExtrasUpkeep ) then return end

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","ExtrasUpkeep BEGIN") end

	if ( _G.xSize == nil ) then

		-- declare local variables
		local i, topology, count, field, topoFields

		topology=(server.setting.get("topology"))
		_G.xSize=tonumber(server.setting.get("xsize"))
		_G.ySize=tonumber(server.setting.get("ysize"))
		_G.dispersion=(server.setting.get("dispersion"))
		_G.citymindist=(server.setting.get("citymindist"))
		_G.topo="classic"
		_G.wrapX=false
		_G.wrapY=false
		topoFields={}

		-- Split topology field (from scenario settings) to a table
		local count=1
		for field in string.gmatch(topology, "[^|]+") do
			topoFields[count]=field
			count=(count+1)
		end

		-- Evaluate topology settings
		if ( #topoFields > 0 ) then
			for i=1,#topoFields,1 do
				if ( topoFields[i] == "WRAPX" ) then _G.wrapX=true 
				elseif ( topoFields[i] == "WRAPY" ) then _G.wrapY=true 
				elseif ( topoFields[i] == "ISO" ) then _G.topo="ISO" 
				elseif ( topoFields[i] == "HEX" ) then _G.topo="HEX" end
			end
		end
	end

	-- Declare global function
	function _G.is_XY_map_location_valid(x,y)
		if ( _G.wrapX and _G.wrapY ) 
		or ( _G.wrapX and ( y >= 0 ) and ( y < _G.ySize ) ) 
		or ( _G.wrapY and ( x >= 0 ) and ( x < _G.xSize ) ) 
		or ( y >= 0 and y < _G.ySize and x >= 0  and x < _G.xSize ) then
			return true
		else
			return false
		end
	end

	local i, h, v, x, y, player, cityTile, city, tile, label, value, upkeepCostTotal, upkeepCost, payment, previousDebtBalance
	local cityExtras={}
	if ( _G.Debt == nil ) then _G.Debt={} end

	-- Skip turn 0
	if ( turn < 1 ) then 
		if ( debugLuaScripts ) then log.error("%s","\tExtrasUpkeep Not active yet END") end
		return
	end

	-- Upkeep cost per tile extra
	cityExtraCosts={ 
		["Airbase"]=1,
		["Buoy"]=.1,
		["Fallout"]=0,
		["Farmland"]=.3,
		["Fortress"]=.6,
		["Hut"]=0,
		["Irrigation"]=.07,
		["Mine"]=.05,
		["Oil Well"]=.3,
		["Pollution"]=0,
		["Railroad"]=.03, 
		["River"]=0,
		["Road"]=.01,
		["Ruins"]=-.1
	} -- Set fee rate per extra, per turn in list above, example Road=.1 (gold)

	for player in players_iterate() do

		-- Reset some variables for each player
		previousDebtBalance=0
		payment=0
		upkeepCost=0
		upkeepCostTotal=0
		if ( _G.Debt[player] == nil ) then _G.Debt[player]=0 end
		cityExtras={ ["Irrigation"]=0,["Mine"]=0,["Oil Well"]=0,["Pollution"]=0,["Hut"]=0,
		["Farmland"]=0,["Fallout"]=0,["Fortress"]=0,["Airbase"]=0,["Buoy"]=0,["Ruins"]=0,
		["Road"]=0,["Railroad"]=0,["River"]=0 }

		for city in (player):cities_iterate() do
			cityTile=(city).tile
			tile=cityTile
			x=(tile).x
			y=(tile).y

			-- Check 21 tiles in city radius for tile extras
			h={-1,-0, 1,-2,-1, 1, 2, 0,-2, 2,-1, 1,-2,-1, 1, 2, 0,-1,-0, 1, 0}
			v={-2,-2,-2,-1,-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 0}
			for i=1,#h,1 do
				if ( _G.is_XY_map_location_valid( ( h[i] + x ),( v[i] + y ) ) ) then
					tile=find.tile( ( h[i] + x ),( v[i] + y ) )
					for label, value in pairs(cityExtras) do
						if ( (tile):has_extra(label) ) then
							cityExtras[label]=( cityExtras[label] + 1 )
						end
					end
				end
			end
		end

		if ( (player):is_human() ) and ( _G.Debt[player] > 0 ) then
			log.error("%s","\tCity tile extras upkeep expense for:" .. tostring(player) .. " gold=" .. (player):gold() .. " debt=" .. _G.Debt[player] )
		end

		for label, value in pairs(cityExtras) do
			if ( cityExtras[label] > 0 ) then
				upkeepCost=( (cityExtras[label]) * cityExtraCosts[label] )
				if ( (player):is_human() ) then
					log.error("%s","\t" .. label .. "\tqty:" .. cityExtras[label] .. " @ " .. cityExtraCosts[label] .. "=" .. upkeepCost )
				end
				upkeepCostTotal=( upkeepCostTotal + upkeepCost )
			end
		end

	-- Add the cost to the player's account
	previousDebtBalance=_G.Debt[player]
	_G.Debt[player]=( upkeepCostTotal + _G.Debt[player] )

	-- If player has gold and debt, deduct gold for debt payment
		if ( (player):gold() > 1 ) and ( _G.Debt[player] > 1 ) then
			if ( (player):gold() > _G.Debt[player] ) then
				payment=math.floor( _G.Debt[player] )
			else
				payment=math.floor( (player):gold() )
			end
			_G.Debt[player]=( _G.Debt[player] - payment )
			-- the next command uses "change by" amount rather than "change to" amount
			edit.change_gold( player, -payment )
		end

		if ( (player):is_human() ) and ( _G.Debt[player] > 0 ) then
			message="\n\tINVOICE - Turn " .. turn .. "- Tile extra upkeep costs for cities owned by: " .. tostring(player) .. " gold=" .. (player):gold() .. " debt=" .. _G.Debt[player] .. "\n\t\tPrior balance:\t\t\t" .. previousDebtBalance .. "\n\t\tNew charges:\t\t\t+" .. upkeepCostTotal .. "\n\t\tPayment extracted:\t-" .. payment .. "\n\t\tNew balance:\t\t\t" .. _G.Debt[player] .. " gold owed.\n"
			log.error("%s",message)
    			--notify.event(player, nil, E.SCRIPT,message.."\n\tItemization available in Chat pane.")
		end


	end

	if ( debugLuaScripts ) then log.error("%s","\tExtrasUpkeep END") end

end
signal.connect("turn_started", "Lua_ExtrasUpkeep")
^ Updated 2023-12-04 -- Set math.floor to ensure that payments extracted are in whole units of gold, since decimals don't actually work!

One issue is that tiles that exist in more than one city's radius are charged against the player once per each city. Instead of specifying cost in gold, it might be interesting to specify it in terms of labor-hours, and then calculate gold per labor hour, while considering government type. So in Despotism, a job that could be done in 1 turn might take 3 turns in Democracy (due to 8 hour workdays) and also might cost 100x as much per labor hour...

Attached is a playable scenario file "RandoMap with ExtrasUpkeep only" for Freeciv 2.6.4 and later for testing the routine, and which contains only this script and no others.
Attachments
RandoMap with ExtrasUpkeep ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(11.24 KiB) Downloaded 436 times
Last edited by Molo_Parko on Mon Dec 04, 2023 3:48 pm, edited 5 times in total.
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

DecayingExtras -- extras on tiles which are not currently within any city radius decay over time. This is a companion script to "ExtrasUpkeep" which charges for upkeep of extras within any city radius.

Code: Select all

--##############################################################################
function Lua_DecayingExtras(turn, year)

	if ( DecayingExtras == nil ) then DecayingExtras=true end
	if not ( DecayingExtras ) then return end

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","DecayingExtras BEGIN") end

	if ( turn == 0 ) and ( _G.xSize == nil ) then

		-- declare local variables
		local i, topology, count, field, topoFields

		topology=(server.setting.get("topology"))
		_G.xSize=tonumber(server.setting.get("xsize"))
		_G.ySize=tonumber(server.setting.get("ysize"))
		_G.dispersion=(server.setting.get("dispersion"))
		_G.citymindist=(server.setting.get("citymindist"))
		_G.topo="classic"
		_G.wrapX=false
		_G.wrapY=false
		topoFields={}

		-- Split topology field (from scenario settings) to a table
		local count=1
		for field in string.gmatch(topology, "[^|]+") do
			topoFields[count]=field
			count=(count+1)
		end

		-- Evaluate topology settings
		if ( #topoFields > 0 ) then
			for i=1,#topoFields,1 do
				if ( topoFields[i] == "WRAPX" ) then _G.wrapX=true 
				elseif ( topoFields[i] == "WRAPY" ) then _G.wrapY=true 
				elseif ( topoFields[i] == "ISO" ) then _G.topo="ISO" 
				elseif ( topoFields[i] == "HEX" ) then _G.topo="HEX" end
			end
		end
	end

	-- Declare global function
	function _G.is_XY_map_location_valid(x,y)
		if ( _G.wrapX and _G.wrapY ) 
		or ( _G.wrapX and ( y >= 0 ) and ( y < _G.ySize ) ) 
		or ( _G.wrapY and ( x >= 0 ) and ( x < _G.xSize ) ) 
		or ( y >= 0 and y < _G.ySize and x >= 0  and x < _G.xSize ) then
			return true
		else
			return false
		end
	end

	-- declare local variables
	local i, j, h, v, x, y, ib, jb, tile

	-- The order matters. Railroad before road prevents leaving a tile with an Extra missing a prerequisite
	local decayingExtras={"Railroad", "Road", "Farmland", "Irrigation", "Mine", "Oil Well", "Airbase", "Buoy", "Fortress"}

	-- Scenario setting "borders" defaults to Enabled (in some Fc version?)
	-- Possible values are nil, SEE_INSIDE, EXPAND, ENABLED, DISABLED
	_G.Borders=server.setting.get("borders")
	if ( _G.Borders == nil ) then
		_G.Borders="ENABLED"
	end

	-- Select a random tile within each 5x5 area of the map (non-overlapping)
	for ib=0,(_G.ySize-1),5 do
		for jb=0,(_G.xSize-1),5 do
			x=random(jb,(jb+5))
			y=random(ib,(ib+5))

			if ( x > _G.xSize-1 ) or ( y > _G.ySize-1 ) then
				goto continue_1
			end

			-- Check 21 tiles radius for any city
			h={-1,-0, 1,-2,-1, 1, 2, 0,-2, 2,-1, 1,-2,-1, 1, 2, 0,-1,-0, 1, 0}
			v={-2,-2,-2,-1,-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 0}
			for i=1,#h,1 do
				if ( _G.is_XY_map_location_valid( ( h[i] + x ),( v[i] + y ) ) ) then
					tile=find.tile( ( h[i] + x ),( v[i] + y ) )
					if ( (tile):city() ) then goto continue_1 end
				end
			end

			-- If not within any city radius
			tile=find.tile(x,y)
			for i=1,#decayingExtras,1 do
				if ( (tile):has_extra(decayingExtras[i]) ) then
					-- It should be VERY likely that tile extras outside city radius will fall apart
					if ( random(1,10) > 1 ) then
						-- ^ 90% chance
						-- Remove the first matching extra
						edit.remove_extra(tile,decayingExtras[i])
						goto continue_1
					end
				end
			end
		::continue_1::
		end
	end

	if ( debugLuaScripts ) then log.error("%s","\tDecayingExtras End") end

end
signal.connect("turn_started", "Lua_DecayingExtras")
^ Updated 2023-12-04

The attached playable scenario for Freeciv 2.6.4 or later "RandoMap with DecayingExtras only" has only this script for testing. To test, start the scenario game and click "Turn done" repeatedly while watching areas of the map outside of any city radius.
Attachments
RandoMap with DecayingExtras ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(11.48 KiB) Downloaded 447 times
Last edited by Molo_Parko on Mon Dec 04, 2023 3:44 pm, edited 2 times in total.
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

CamelSprint allows a caravan's camel to rest at a distance, and then to sprint forward as many tiles as turns rested, to a target tile. This allows a caravan to reach a foreign city to create a trade-route without being in the tribe's territory at turn end. After the camel sprints forward to the target tile, select the caravan and establish the trade route.

Code: Select all

--##############################################################################
function Lua_CamelSprint(x, y, x2, y2)

	-- This routine is activated manually by user calling it
	CamelSprint=true

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","CamelSprint BEGIN") end

	local i, unit, tile1, tile2, distance

	-- Prepare table for up to 100 camels max
	if ( _G.camels == nil ) then
		_G.camels={}
	end

	tile1=find.tile(x,y)
	tile2=find.tile(x2,y2)
	distance=math.sqrt((tile1):sq_distance(tile2))

	if ( (tile2).terrain:class_name() ~= "Land" ) then
		log.error("%s","\tCamelSprint: Destination tile must be of class Land")
		return	
	end

	log.error("%s","\tCamelSprint: "..tostring(tile1).." "..tostring(tile2).." "..distance)

	if ( (tile1):num_units() > 0 ) then
		for unit in (tile1):units_iterate() do
			if ( (unit).utype == find.unit_type("Caravan") ) then
				i=( #_G.camels + 1 )
				_G.camels[i]={["unit"]=unit, ["tile1"]=tile1, ["tile2"]=tile2, ["turns"]=(distance-1)}
			end
		end
	end

	if ( debugLuaScripts ) then log.error("%s","CamelSprint END") end
end


--##############################################################################
function Lua_CamelSprintGo(turn, year)

	-- If this routine is disabled then just return
	if ( CamelSprint == nil ) then return end
	if not ( CamelSprint ) then return end

	if ( debugLuaScripts == nil ) then debugLuaScripts=false end
	if ( debugLuaScripts ) then log.error("%s","CamelSprintGo BEGIN") end

	local i, unit, tile1, tile2, turns, x, y, x2, y2, removeFromList

	for i=1,#_G.camels,1 do

		if ( _G.camels[i].unit == nil ) then break end

		removeFromList=false

		unit=_G.camels[i].unit
		tile1=_G.camels[i].tile1
		tile2=_G.camels[i].tile2
		turns=_G.camels[i].turns
		log.error("%s","\tCamelSprintGo: unit=" .. tostring(unit) .. " tile1=" .. tostring(tile1) .. " tile2=" .. tostring(tile2) .. " turns=" .. tostring(turns))

		if ( (unit).tile ~= tile1 ) then
			log.error("%s","\tCamelSprintGo CANCELLED: Camel " .. tostring(unit) .. " no longer resting at tile: " .. tostring(tile1) )
			removeFromList=true
		end

		if ( ( turns > 0 ) and ( removeFromList == false ) ) then
			_G.camels[i]={["unit"]=unit, ["tile1"]=tile1, ["tile2"]=tile2, ["turns"]=(turns-1)}
		else
			edit.unit_move(unit,tile2,0)
			removeFromList=true
		end

		if ( removeFromList ) then
			_G.camels[i]=nil
			_G.camels=(table.pack(table.unpack(_G.camels)))
		end
	end

	if ( debugLuaScripts ) then log.error("%s","CamelSprintGo END") end

end
signal.connect("turn_started", "Lua_CamelSprintGo")
^ Updated 2023-12-05

Image
^ Caravan is 6 tiles from Dire Dawa and wishes to avoid being in the other tribe's territory during any "turn end" to preserve peaceful relations.

Image
^ CamelSprint Lua script called with the x,y values of the Caravan's current tile and the target tile.

Image
^ Caravan rests for 6 turns (Sentry the unit if you like) and then sprints forward to Dire Dawa in a single turn!

Image
^ Select the Caravan within Dire Dawa and establish a trade route.

Image
^ Trade route established without the Caravan being in any foreign tribe's territory at turn end.
Attachments
RandoMap with CamelSprint ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
(10.3 KiB) Downloaded 440 times
Last edited by Molo_Parko on Tue Dec 05, 2023 4:51 pm, edited 2 times in total.
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

Emulate some commands from newer versions of Freeciv implements some commands from Freeciv 3.2 in Freeciv version 2.4 or later so that those commands work in either version. Currently limited to Freeciv version related stuff.

Code: Select all

--##############################################################################
-- Emulate some commands from newer versions of Freeciv
if ( tonumber(string.sub(fc_version(),17,19)) >= 2.4 ) and ( tonumber(string.sub(fc_version(),17,19)) < 3.2 ) then

	-- Emulate Fc 3.2 "version_string()"
	if ( version_string == nil ) then
		function version_string()
			return string.sub(fc_version(),17)
		end
	end

	-- Emulate Fc 3.2 "name_version()"
	if ( name_version == nil ) then
		function _G.name_version()
			return fc_version()
		end
	end

	-- Emulate Fc 3.2 "comparable_version()"
	if ( comparable_version == nil ) then
		function _G.comparable_version()
			local text, count, field, fields
			fields={}
			text=string.sub(fc_version(),17)
			count=1
			for field in ( string.gmatch(text, "[^.]+") ) do
				fields[count]=field
				count=(count+1)
			end
			if ( #fields > 0 ) then
				return (fields[1].."."..fields[2])
			else
				return nil
			end
		end
	end

	-- Emulate Fc 3.2 "versions_compare()"
	if ( versions_compare == nil ) then
		function _G.versions_compare(v1,v2)
			if ( v1 == v2 ) then return 0
			elseif ( v1 > v2 ) then return -1 -- Totally guessing at what "returns a negative" meant
			else return 1
			end
		end
	end

end
Image
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

I have some catching-up to do in this topic!

This is the Lua script included in the scenario MiniMap Islands, shared at this link: https://forum.freeciv.org/f/viewtopic.p ... 04#p110004

Code: Select all

o = "\n\n[c fg=\"#0000ff\"][ b ]Welcome to MiniMap Islands[ /b ][ /c ]" ..
"[c fg=\"#ff0000\"][ b ]\n\n\n" ..
"Select an island nation for your tribe via right-click\n" .. 
"on the player shown near the top of this window. Then\n" ..
"select 'Pick Nation' in the pop-up list. Click the 'All'\n" ..
"button for all nations and select an island nation\n" ..
"such as Hawaiian.\n\n[ /b ][ /c ]"
notify.event( nil, nil, E.CHAT_MSG, o )

function s_setup()
	if not _G.setupDone then

		local tileIDs = { 34, 45, 210, 221 }

		local island_nations = { "Acehnese", "Ainu", "Alander", "Aleut", "Antiguan and Barbudan", "Antillean", "Bahamian", "Bahraini", "Barbadian", "Cape Verdean",  "Chinese", "Chumash", "Comorian", "Cretan", "Cuban", "Cypriot", "Dominican", "Dominicano", "Ecuadorian", "Equatoguinean", "Faroese", "Fijian", "Formosan", "Greenlander", "Grenadian", "Guanche", "Icelandic", "Illyrian", "Indonesian", "Jamaican", "Kiribati", "Malagasy", "Maldivian", "Maltese", "Manx", "Maori", "Marshallese", "Mauritian", "Moluccan", "Nauruan", "Nestorian", "Newfoundland", "New Zealand", "Nuu-chah-nulth", "Papuan", "Papua New Guinean", "Polynesian", "Puerto Rican", "Rapa Nui", "Saint Lucian", "Samoan", "Santomean", "Sardinian", "Seychellois", "Sicilian", "Sinhalese", "Solomon Islander", "Tahitian", "Taino", "Taiwanese", "Tongan", "Trinidadian and Tobagonian", "Turkish Cypriot", "Vanuatuan", "Venetian", "Vincentian", "West Indian" }

		local rulerNames = { "Curly", "Larry", "Moe" }

		for i = 1, 3, 1 do
			tribe = find.nation_type( island_nations[ random( 1, #island_nations ) ] )
			if tribe then
				player = edit.create_player( rulerNames[ i ], tribe, nil )

				for j = 1, 4, 1 do
					tile = find.tile( tileIDs[ j ] )
					if not tile:city() then
						city = edit.create_city( player, tile, nil )

						local startunits = server.setting.get( "startunits" )
						for i = 1, #startunits do
							local c = startunits:sub(i,i)
							if c == "c" then unit = find.unit_type( "Settlers" )
							elseif c == "f" then unit = find.unit_type( "Trireme" )
							elseif c == "d" then unit = find.unit_type( "Warriors" )
							end
							edit.create_unit_full( player, tile, unit, 0, nil, 0, -1, nil )
						end
						break
					end
				end
			end
		end

		for player in players_iterate() do
			edit.give_tech( player, find.tech_type( "Map Making" ), 0, nil, nil )
		end

		_G.setupDone = true

	end
end
signal.connect( "turn_started", "s_setup" )
Creating this Lua script in Freeciv 2.6.4 included a few frustrations...

Lua has no way to determine the startpos values included in the scenario file's [map] section. If the normal process of allowing aifill to create all players initially is used then it is fairly simple to get a tile on which a player's unit is standing at the beginning of turn 0 -- a prior script LateArrivalStartunits does just that. Also if startcity is enabled then it is very easy to get the tile where each tribe's startcity is built. But in this scenario I wanted to limit the computer-controlled tribes to using only island nations -- so aifill had to be disabled since it can't be restricted to solely island nations, and therefore there are no "AI" tribe startunits at the startpos tiles listed in the scenario. And there are no startcities for the computer-controlled tribes since the players don't yet exist when the game begins. So I had to duplicate the startpos values included in the scenario, by adding them manually in the script.

Since aifill is disabled, each of computer-controlled tribes had to be created manually, and the first bit of data needed is "leader name" -- which Lua can't get despite that the names are in the nation's ruleset files. So I used as leader names, Curly, Larry, and Moe instead.

Lua also can't determine which nations are or aren't island nations, so I had to do that manually by grep'ing for "island" in the nations files, sorting out those that aren't actually island nations, correcting the names to exactly match the nation ruleset names, and including the final list in the script itself. A flag for nation geographical type (island, coastal, inland or land-locked) in the ruleset files might be helpful.

After creating each player, the player's startcity must be manually created, and the startunits must be manually created.

All that just to limit nations to island nations. But it works and if it helps someone trying to make a more serious island based scenario, then I'm happy.
Last edited by Molo_Parko on Sat Nov 09, 2024 5:44 am, edited 2 times in total.
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

This is the "fast_start" Lua script included in the scenario Pieland, shared at this link: https://forum.freeciv.org/f/viewtopic.p ... 96#p109996

Code: Select all

log.error( "%s", "fast_start can be disabled via the chat pane by entering the next line (followed by return or enter.)\n\n/lua fast_start = false\n" )

if _G.fast_start == nil then _G.fast_start = true end

-- Emulate Fc 3.0 game.current_turn()
if game.current_turn == nil then
	function _G.game.current_turn()
		return math.floor( game.turn() )
	end
end

function s_fast_start()
	if game.current_turn() == 0 then
		if find.tile( 4, 43 ):has_extra( "River" ) then
			if _G.fast_start == true then
				log.error("%s", "fast_start: creating 11 cities for each tribe")
				local city_tiles = {}
				for player in players_iterate() do
					local x, y, tile
					for unit in player:units_iterate() do
						tile = unit.tile
						break
					end
					if tile.id == 985 then
						city_tiles = {  797,  792,  655,  602,  515,  462,  409,  371,  271,  267,  121 }
					elseif tile.id == 987 then
						city_tiles = {  987, 1039,  897, 1044,  707,  854, 1049,  566,  711,  859, 1004 }
					elseif tile.id == 1227 then
						city_tiles = { 1227, 1184, 1189, 1194, 1374, 1379, 1384, 1520, 1573, 1578, 1716 }
					elseif tile.id == 1321 then
						city_tiles = { 1370, 1517, 1562, 1802, 1757, 1664, 2041, 1949, 1856, 2000, 1909 }
					elseif tile.id == 1317 then
						city_tiles = { 1317, 1557, 1458, 1598, 1649, 1797, 2037, 1889, 1838, 1883, 2031 }
					elseif tile.id == 1219 then
						city_tiles = { 1266, 1406, 1166, 1206, 1258, 1498, 1250, 1398, 1642, 1541, 1735 }
					elseif tile.id == 979 then
						city_tiles = {  979, 1023,  880,  971,  780,  968,  775,  586, 1060,  915,  583 }
					elseif tile.id == 981 then
						city_tiles = {  835,  644,  687,  496,  453,  493,  394,  301,  306,  309,  117 }
					end
					for i = 1, #city_tiles, 1 do
						tile = find.tile( city_tiles[ i ] )
						edit.create_city( player, tile, nil )
					end
				end
			end
			edit.remove_extra( find.tile( 4, 43 ), "River" )
		end
	end
end
signal.connect( "turn_started", "s_fast_start" )
The script runs at the beginning of the game and creates 11 cities per tribe to "skip" past some of the earliest activities in the game which includes scouting the local area, and building cities. In this particular case, since each tribe's "homeland" area is shaped like a piece of pie or pizza, I just manually specified in the script which tiles to build the cities on -- and I alternated selecting tiles of one tribe with their neighbor across the water so that no tribe would have all the best spots and leave their neighbor all the worst spots for building cities.

It is possible to instead create a routine to deduce which tiles would be the best (by some criteria) city build sites, but the scenario is not large and it was easier to gather the tile IDs.

In this script too, the inability of Lua in Freeciv 2.6.4 to read the startpos that are included in the scenario file's [map] section, created a bit of extra work, but since startunits are provided it was easy to find them, get the tileID of their location, and use that tileID to determine in which pie-slice the tribe is located.

I used a single tile of river in the scenario's map as an indicator of whether the script had already run (the river is removed by the script) -- same as in another scenario "The Shadow Vales", so that during a reload of a saved game, the script won't try to run again and re-create all the cities, any of which may have already been destroyed.
Last edited by Molo_Parko on Sat Nov 09, 2024 5:47 am, edited 2 times in total.
Molo_Parko
Hardened
Posts: 177
Joined: Fri Jul 02, 2021 4:00 pm

Re: New features via Lua script

Post by Molo_Parko »

Scripts from the scenario The Shadow Vales, which is shared at this link: https://forum.freeciv.org/f/viewtopic.php?p=109990

The one thing I am most happy about having changed in scripting within this scenario is that I stopped using tables and started using "field strings". What a pain in the butt it is to use tables, backup the tables to strings since tables are lost on reloads, then restore from the string back to the tables. Why not just use a string the whole time? So I did, and it works well! Here's what a "field string" looks like and a simple Lua routine to extract a value.

Below is the maxCitiesFields string. Field #'s are from 0 because player 0 has the first field. Player zero has held a maximum of 16 cities at the start of any turn, so the first field is "16". Player's 3 and 4 have each had a maximum of 18 cities. This method is simple to use and since the field is a string rather than a table, no backing-up and restoring the values after reloads is required at all.

maxCitiesFields="16|16|17|18|18|16|16|15|50|5"

Code: Select all

function s_extract_field( combinedFieldsString, fieldNumber )
	local count = 0
	for field in string.gmatch( combinedFieldsString, "[^|]+" ) do
		if count == fieldNumber then return field end
		count = count + 1
	end
end
My next favorite change is a consistent construct used to identify a player (to the human, in text output.) It includes the player number, nation name, and the ruler name so that those who can't recall which player is which ( namely, ME! ) see the #, tribe, and ruler together which greatly increases the liklihood of recognizing the player/tribe/ruler.

Code: Select all

function s_number_nation_ruler_name( player )
	local playerName
	if player and player ~= "" then
		playerName = string.sub( tostring( player.nation ), 14, -2 )
		playerName = playerName:gsub( "^.-%s", "", 1 )
		playerName = ( "#" .. math.floor( player.id ) .. " " ..
				playerName .. " ( " .. player.name .. " )"
				)
		return playerName
	end
	return false
end
^ Ouput is consistently like this "#1 Mongol (Genghis)" instead of sometimes just a player number, sometimes just a tribe name, or sometimes just a ruler name.

Another one which was particularly helpful is below, it is used for Suburban Sprawl when creating roads outward from cities, and for determining to whom the bounty for a Barbarian Leader will be paid (in the scenario), and for Weatherizer_Winds when units are blown away to a distance corresponding to wind strength. It is also used in a routine to check whether a given tile is next to a specified type of terrain (as in a Mountains tile next to any Grassland tile.)

Code: Select all

function s_list_ring_of_tileIDs_around( centerTile, ringNumber )
	local tileIDs = {}
	local x = centerTile.x
	local y = centerTile.y
	for tile in ( centerTile ):square_iterate( ringNumber ) do
		if math.abs( tile.x - x ) == ringNumber or math.abs( tile.y - y ) == ringNumber then
			tileIDs[ #tileIDs + 1 ] = tile.id
		end
	end
	return tileIDs
end
Next is one to check whether a city exists within citymindist of a given tile. I tried using the Freeciv Lua function "(unit):is_on_possible_city_tile" but it does not appear to take into account cities within citymindist, but rather only whether the tile terrain would accommodate a city. Next was "(tile):city_exists_within_city_radius and (tile):city_exists_within_max_city_map -- neither of which actually indicated whether or not a city was within citymindist of a given tile. I also tried various uses of (city):map_sq_radius, but became annoyed about having to manually screen-out some tiles (corners just outside city radius and the city tile itself) every time.

Code: Select all

function s_is_a_city_within_citymindist( tile )
	local citymindist = tonumber( server.setting.get( "citymindist" ) )
	for h = ( tile.x - ( citymindist - 1 ) ), ( tile.x + ( citymindist - 1 ) ), 1 do
		for v = ( tile.y - ( citymindist - 1 ) ), ( tile.y + ( citymindist - 1 ) ), 1 do
			if find.tile( h, v ):city() then return true end
		end
	end
	return false
end
Suburban Sprawl gradually produces roads emanating outward from cities. The computer-controlled tribes won't produce workers for quite some time in The Shadow Vales, so I wrote this routine to create roads to make unit travel faster. This one is quite portable and can use signal.connect with turn_started ( Freeciv 2 ) or turn_begin (Freeciv 3), or just call it from another per turn routine. This does also need the "s_list_ring_of_tileIDs_around" routine which is posted above. Also the settings are flexible as to how often it will create a road segment per city, and also how far out from the city the roads will go (it works out to at least 10 rings which covers the entire map with roads by about turn 100, provided the frequnecy of creating a road segment is 1 per turn.)

Code: Select all

function s_suburban_sprawl( turn, year )
	local output="[ b ]Suburban sprawl creates roads emanating from cities[ /b ]"
	for player in players_iterate() do
		local roads = ""
		local playerName = s_number_nation_ruler_name( player )
		for city in player:cities_iterate() do
			if random( 1, 100 ) > ( city.size + 1 ) * 10 then
				goto loop_exit
			end
			local tile
			local tileIDs = {}
			for ring = 1, 4, 1 do
				tileIDs = s_list_ring_of_tileIDs_around( city.tile, ring )
				for i = 1, #tileIDs, 1 do
					local t = tileIDs[ i ]
					local tile = find.tile( t )
					if tile.terrain:class_name() == "Land"
					and tile:has_extra( "Road" ) == false
					and tile:has_extra( "River" ) == false
					and tile.terrain.id ~= find.terrain( "Inaccessible" ).id
					then
						edit.create_extra( tile, "Road" )
						roads = ( roads .. math.floor( tile.x ) .. "," .. math.floor( tile.y ) .. " " )
						goto loop_exit
					end
					::offmap::
				end
			end
			::loop_exit::
		end
		if roads ~= "" then
			if output ~= "" then
				chat( "\t" .. output )
				output=""
			end
			chat( "\t\t" .. playerName .. " at " .. roads )
		end
	end
	chat()
end
Roving Rivers Creates random rivers from a fixed start point ( the isolated lake tile in each vale ) to a fixed shoreline ( the outer edges of the map ). In "The Shadow Vales", the routine is called once every 17 turns. When called it removes old rivers that it had created, and randomly plots new rivers -- while also adding tile irrigation and destroying roads on flat land tiles. This routine is a little less portable, but provided the scenario has fixed start tiles for the source of the river, and fixed shore lines, it wouldn't be too hard to port it into another scenario.

Code: Select all

function s_roving_rivers( turn, year )

	local valeBoundsStartX	= {  0, 22, 44,  0,  0, 48,  0, 22, 44 }
	local valeBoundsEndX	= { 16, 38, 60, 12,  0, 60, 16, 38, 60 }
	local valeBoundsStartY	= {  0,  0,  0, 22,  0, 22, 44, 48, 44 }
	local valeBoundsEndY	= { 16, 12, 16, 38,  0, 38, 60, 60, 60 }
	local valeLakesX	= { 12, 30, 49, 12,  0, 48, 12, 30, 50 }
	local valeLakesY	= { 10, 12, 10, 30,  0, 30, 50, 48, 49 }

	if _G.riverPathsFieldString == nil then
		_G.riverPathsFieldString = ""
	else
		for field in string.gmatch( _G.riverPathsFieldString, "[^|]+" ) do
			local tile = find.tile( field )
			edit.remove_extra( tile, "River" )
		end
		_G.riverPathsFieldString = ""
	end

	local vale, coast, atShoreline, x, y, tile
	for vale = 1, 9, 1 do
		if vale == 1 then
			coast = "x-"
			if random( 1, 2 ) == 1 then
				coast = "y-"
			end
		elseif vale == 2 then
			coast = "y-"
		elseif vale == 3 then 
			coast = "x+"
			if random( 1, 2 ) == 1 then
				coast = "y-"
			end
		elseif vale == 4 then
			coast = "x-"
		elseif vale == 5 then
			goto vale_done
		elseif vale == 6 then
			coast = "x+"
		elseif vale == 7 then
			coast = "x-"
			if random( 1, 2 ) == 1 then
				coast = "y+"
			end
		elseif vale == 8 then
			coast = "y+"
		else
			coast = "x+"
			if random( 1, 2 ) == 1 then
				coast = "y+"
			end
		end

		x, y = valeLakesX[ vale ], valeLakesY[ vale ]
		atShoreline = 0
		repeat
			local ok = 0
			repeat
				local h, v = 0, 0
				if coast == "x-" then
					if random( 1, 2 ) == 1 then
						h = h - 1
					else
						if random( 1, 2 ) == 1 then
							v = v + 1
						else
							v = v - 1
						end
					end
				elseif coast == "x+" then
					if random( 1, 2 ) == 1 then
						h = h + 1
					else
						if random( 1, 2 ) == 1 then
							v = v + 1
						else
							v = v - 1
						end
					end
				elseif coast == "y-" then
					if random( 1, 2 ) == 1 then
						v = v - 1
					else
						if random( 1, 2 ) == 1 then
							h = h + 1
						else
							h = h - 1
						end
					end
				else
					if random( 1, 2 ) == 1 then
						v = v + 1
					else
						if random( 1, 2 ) == 1 then
							h = h + 1
						else
							h = h - 1
						end
					end
				end

				tile = find.tile( x + h, y + v )
				if tile.terrain:rule_name() ~= "Mountains"
				and tile.terrain:rule_name() ~= "Inaccessible" then
					x, y = x + h, y + v
					ok = 1
				end
			until ok > 0

			if x < 1 or x > 59 or y < 1 or y > 59 then goto vale_done end

			if tile.terrain:rule_name() ~= ( "Lake" ) then
				edit.create_extra( tile, "River" )
			end

			if tile.terrain:rule_name() ~= "Lake"
			and tile.terrain:rule_name() ~= "Jungle"
			and tile.terrain:rule_name() ~= "Swamp"
			and tile.terrain:rule_name() ~=  "Forest"
			and not tile:city() then
				edit.create_extra( tile, "Irrigation" )
			end

			if not tile:city()
			and tile.terrain:rule_name() ~=  "Forest" then
				edit.remove_extra( tile, "Road" )
			end

			_G.riverPathsFieldString = ( _G.riverPathsFieldString .. math.floor( tile.id ) .. "|" )

			if x < 2 or x > 58 or y < 2 or y > 58 then
				atShoreline = 1
			end
		until atShoreline > 0
		::vale_done::
	end
	_G.riverPathsFieldString = string.sub( _G.riverPathsFieldString, 1, -2 )

	if game.current_turn() > 1 then
		local output = "[ b ]Vale Echo News:[ /b ]\t\tMeltwater and heavy rains caused rivers to overrun their banks, washing-out roads."
		if not _G.ReloadDetected then notify.event( nil, nil, E.REPORT, "__" .. output ) end
		notify.event( nil, nil, E.CHAT_MSG, "\t" .. output )
	end
end
Last edited by Molo_Parko on Sat Nov 09, 2024 5:25 am, edited 4 times in total.
Post Reply