Danmacsch's Lua (and general modding) inquiries

Danmacsch

Geheimekabinetsminister
Joined
Jan 14, 2014
Messages
1,316
Location
Copenhagen, Denmark
Hi,

Thought I'd start a thread where I can ask a bunch of questions - hopefully it won't drown as easily as posts in Lua Request Thread sometimes tend to do.

1) Research Agreements; how mod-able are they? If wanted to increase the Science bonus from Research Agreements with a percentage, is there a lua method that can be used, or am I overlooking some tag in either a buildings or policies subtable?

2) visibility of tiles; I have a unique unit that should be able to detect worker units within some radius. That is, if the unique unit moves to a plot, all plots within a (say) 3 tile radius are checked. If one of these plots has a unit of a specific type on it, the plot is revealed to the player (if it isn't already) and becomes visible. When either the unique unit or the unit of the specific type moves away, the tile isn't automatically visible anymore, but it keeps being revealed.
I've tried multiple things so far, but haven't gotten it to work correctly. I can get the plot to be permanently visible with SetVisibilityCount(team) but then the plot doesn't stop being visible again, at all.
I'll post my most recent coding attempt here later.

3) adding a dynamic to the combat bonus from having a great general nearby. The relevant traits table tag can't really be modified during the game, so can't use that. What I'm trying to do specifically is to increase the bonus by 5% for every original capital conquered. I've made some attempts here as well with a bunch of promotions, but it turned out the only great general bonus tag in the promotions table is one applied to stacking a unit with a great general. A similar problem was recently/is currently discussed in the lua request thread, but without the dynamic aspect. Any ideas?

4) this I also asked in another thread, but didn't receive an answer - hopefully will here. In have an UA, which allows military units to repair pillaged tiles. This is to my knowledge achievable by creating unique versions of all military units in the game supplying them with a non nil workrate and the repair build, then converting all the player's units to their repairing-able counterpart. This method however, is somewhat tedious SQL-wise and doesn't seem very efficient not to mention won't be very compatible with mods adding need military units. Can someone think of another method?
 
1) I thought <MedianTechPercentChange>25</MedianTechPercentChange> would achieve this, as it is used by the Porcelain Tower.

4) The only method I can think of is using UnitSetXY and plot:SetImprovementPillaged(false). If a unit would move over the tile, it should be automatically repaired, but this is not considered an "action".

Hope this helps.. somewhat? ;P
 
Hi,
4) this I also asked in another thread, but didn't receive an answer - hopefully will here. In have an UA, which allows military units to repair pillaged tiles. This is to my knowledge achievable by creating unique versions of all military units in the game supplying them with a non nil workrate and the repair build, then converting all the player's units to their repairing-able counterpart. This method however, is somewhat tedious SQL-wise and doesn't seem very efficient not to mention won't be very compatible with mods adding need military units. Can someone think of another method?

In regard to 4, are you open to implementing your mod using the Community Patch?
 
1) I thought <MedianTechPercentChange>25</MedianTechPercentChange> would achieve this, as it is used by the Porcelain Tower.
Ah of course. Silly me [emoji14]. Thanks.

4) The only method I can think of is using UnitSetXY and plot:SetImprovementPillaged(false). If a unit would move over the tile, it should be automatically repaired, but this is not considered an "action".

Hope this helps.. somewhat? ;P
Yes that probably work, but I very much like for it to be a " build". Thanks anyway.

In regard to 4, are you open to implementing your mod using the Community Patch?
No, not really. Whilst always playing with the CP myself, I don't want to rely on it when it comes to custom civs.

Anyone else have any ideas?
 
The only other thing I can think of is repairing by fortifying. Basically, this means moving the unit on the pillaged tile, fortifying it, then using PlayerDoTurn to check whether the unit is fortified (unit:GetFortifyTurns()), and repair the improvement through plot:SetImprovementPillaged(false). This could be considered a more 'active' approach. The 'problem' might be that the unit may be interrupted when an enemy unit approaches, or when it is attacked, though that would also happen if it were a worker repairing the improvement ;)

Other than that, I can't think of anything; I doubt there's an easy method. Good luck anyway! :)
 
Another question; is it possible to get the ID of the City in which a specific Unit is built? I can't find anything in the Unit methods. Is there another way to achieve this?

Thanks in advance?
 
Directly, no.

But you could hook the CityTrained event, find the new unit in the city and store the ID of the city against the new unit
 
That's unfortunate. Not in your custom DLL either? I thought that had everything :p. But thanks whoward69, I'll hook up data persistence of the unit paired with the city in which it is built.
 
If this is for some special ability for a UU near to its origin city, don't forget that military CSes can gift UUs ... so the ID of the city the unit was trained in could be blank/nil/invalid
 
Oh, didn't think of that. Thanks for pointing it out. It is for a special ability for a UU, which is dependent on the number of a specific improvement worked by the city in which it is built. Checking for it to be ~nil should be enough to avoid any problems I think.
 
  • Is the improvement common like a farm or is it likely to be 'rarer' around any one city? If 'rarer', you could use marker promotions for how many improvements were valid around the originating city. And instead of CityTrained you can use UnitCreated (or Machiavelli's UnitCreatedGood system).
  • When a unit is created:
    1. check it is the unit-type you are interested in.
    2. Then check is it in or adjacent to a city
    3. If in or adjacent to a city, count the number of improvements being worked by the city
    4. Add the promotion that corresponds to the number of improvements.
  • Make all your other code look for the specific unit-type, and then which of the marker-promotions that individual unit has.
  • The promotions would be markers only, and would be such as PROMOTION_MARKER_01, PROMOTION_MARKER_02, PROMOTION_MARKER_03, etc. The trailing "01", "02", "03" will correspond to the number of improvements the city was working when the unit was created.
 
Thanks for your answers both of you. I've figured something out.

Another question. I'm trying to determine if a city is (a) producing any unit and (b) producing a military unit. I've the following code:
Code:
function DMS_UpdateUbugabireWorkerExpend()
	Controls.UnitBackground:SetHide(true)
	Controls.UnitImage:SetHide(true)
	Controls.UnitButton:SetDisabled(true)
	Controls.UnitButton:LocalizeAndSetToolTip(nil)
	local pCity = UI.GetHeadSelectedCity()
	if (pCity and (not pCity:IsPuppet())) then
		Controls.UnitBackground:SetHide(false)
		local playerID = pCity:GetOwner()
		local pPlayer = Players[playerID]
		local garrisonedUnit = pCity:GetGarrisonedUnit()
		local bIsProducingUnit = pCity:IsProductionUnit()
		local iProductionUnit = pCity:GetProductionUnit()
		[COLOR="SandyBrown"]local pProductionUnit = pPlayer:GetUnitByID(iProductionUnit)[/COLOR]
		local iProductionNeeded = pCity:GetUnitProductionNeeded()
		local iGrantedProduction = math.min(JFD_GetEraAdjustedValue(playerID, 40), iProductionNeeded)
		
		[COLOR="Red"]if (not bIsProducingUnit) or (bIsProducingUnit and (not pProductionUnit:IsCombatUnit())) then[/COLOR]
			local buttonToolTipDisabled = Locale.ConvertTextKey("TXT_KEY_TRAIT_DMS_UBUGABIRE_EXPEND_WORKER_HELP_DISABLED_NO_MILITARY_UNIT")
			Controls.UnitButton:SetText(buttonText)
			Controls.UnitButton:LocalizeAndSetToolTip(buttonToolTipDisabled)
		elseif (bIsProducingUnit and pProductionUnit:IsCombatUnit() and ((not garrisonedUnit or (garrisonedUnit:GetUnitType() ~= unitWorkerID)))) then
			local buttonToolTipDisabled = Locale.ConvertTextKey("TXT_KEY_TRAIT_DMS_UBUGABIRE_EXPEND_WORKER_HELP_DISABLED_NO_WORKER")
			Controls.UnitButton:SetText(buttonText)
			Controls.UnitButton:LocalizeAndSetToolTip(buttonToolTipDisabled)
		elseif (bIsProducingUnit and pProductionUnit:IsCombatUnit() and garrisonedUnit and garrisonedUnit:GetUnitType() == unitWorkerID) then
			local buttonToolTip = Locale.ConvertTextKey("TXT_KEY_TRAIT_DMS_UBUGABIRE_EXPEND_WORKER_HELP", iGrantedProduction)
			Controls.UnitButton:SetDisabled(false)
			Controls.UnitButton:LocalizeAndSetToolTip(buttonToolTip)
		end

		IconHookup(0, 64, "DMS_BURUNDI_ATLAS", instance.IconImage)
		Controls.UnitImage:SetHide(false)
	end
end
but I'm receiving this error in the red line:
Code:
attempt to index local 'pProductionUnit' (a nil value)
The error occurs both when a military unit is being produced and when one isn't.
 
pPlayer:GetUnitByID() gets an existing unit for the player by the unit's unique ID.

A unit in production doesn't exist yet.

iProductionUnit is the ID of the unit that will be produced in the Units table, that is, you would use GameInfo.Units[iProductionUnit] to get the unit details and then check those for Combat > 0 or RangedCombat > 0 to ascertain if it was a combat unit
 
  1. pCity:GetProductionUnit() will be -1 when the city is not training a unit. Otherwise, as whoward mentioned, it will be the ID# of the unit from the "Units" gametable.
  2. In general you should do something like:
    Code:
    function DMS_UpdateUbugabireWorkerExpend()
    	Controls.UnitBackground:SetHide(true)
    	Controls.UnitImage:SetHide(true)
    	Controls.UnitButton:SetDisabled(true)
    	Controls.UnitButton:LocalizeAndSetToolTip(nil)
    	local pCity = UI.GetHeadSelectedCity()
    	if (pCity and (not pCity:IsPuppet())) then
    		Controls.UnitBackground:SetHide(false)
    		local playerID = pCity:GetOwner()
    		local pPlayer = Players[playerID]
    		local garrisonedUnit = pCity:GetGarrisonedUnit()
    		local iGrantedProduction = 0
    		local bIsProducingUnit, bIsCombatUnit = false, false
    		if pCity:GetProductionUnit() > -1 then
    			local tUnitInfo = GameInfo.Units{ID=pCity:GetProductionUnit()}()
    			bIsProducingUnit, bIsCombatUnit = true, (tUnitInfo.Combat > 0) or (tUnitInfo.RangedCombat > 0)
    			iGrantedProduction = math.min(JFD_GetEraAdjustedValue(playerID, 40), pCity:GetUnitProductionNeeded())
    		end
    		if (not bIsProducingUnit) or (bIsProducingUnit and (not bIsCombatUnit)) then
    			local buttonToolTipDisabled = Locale.ConvertTextKey("TXT_KEY_TRAIT_DMS_UBUGABIRE_EXPEND_WORKER_HELP_DISABLED_NO_MILITARY_UNIT")
    			Controls.UnitButton:SetText(buttonText)
    			Controls.UnitButton:LocalizeAndSetToolTip(buttonToolTipDisabled)
    		elseif (bIsProducingUnit and bIsCombatUnit and ((not garrisonedUnit or (garrisonedUnit:GetUnitType() ~= unitWorkerID)))) then
    			local buttonToolTipDisabled = Locale.ConvertTextKey("TXT_KEY_TRAIT_DMS_UBUGABIRE_EXPEND_WORKER_HELP_DISABLED_NO_WORKER")
    			Controls.UnitButton:SetText(buttonText)
    			Controls.UnitButton:LocalizeAndSetToolTip(buttonToolTipDisabled)
    		elseif (bIsProducingUnit and bIsCombatUnit and garrisonedUnit and garrisonedUnit:GetUnitType() == unitWorkerID) then
    			local buttonToolTip = Locale.ConvertTextKey("TXT_KEY_TRAIT_DMS_UBUGABIRE_EXPEND_WORKER_HELP", iGrantedProduction)
    			Controls.UnitButton:SetDisabled(false)
    			Controls.UnitButton:LocalizeAndSetToolTip(buttonToolTip)
    		end
    	
    		IconHookup(0, 64, "DMS_BURUNDI_ATLAS", instance.IconImage)
    		Controls.UnitImage:SetHide(false)
    	end
    end

----------------------------------------------------------------------------------------------------------------------------

The same basic methodology is needed (ie, checking for "-1") when using pCity:GetProductionBuilding()

Although for both the "building" and the "unit" method you can do as
Code:
local iVariableName = pCity:GetProductionBuilding()
if iVariableName > -1 then
	-- do stuff
end
which throws the data into a variable, and then you refer to the variable thereafter. Not sure this really allows for any more efficiency in terms of processing times.
 
Code:
local tUnitInfo = GameInfo.Units{ID=pCity:GetProductionUnit()}()

Just do a direct lookup as a table
Code:
local tUnitInfo = GameInfo.Units[pCity:GetProductionUnit()]
 
Changing the code fixed the error from before. So now I'm having the correct text when not producing a unit at all, BUT when producing any unit (combat or not combat) breaks something. I'm getting the following:
Code:
Runtime Error: Failed to iterate table due to query error.  Check Database.log for more details.
but there's nothing in the Database.log as far as I can tell. Any ideas?
 
Changing the code back the LeeS's original code below fixed the issue:
Code:
local tUnitInfo = GameInfo.Units{ID=pCity:GetProductionUnit()}()
The query error (above post) occurred when using the direct lookup:
Code:
local tUnitInfo = GameInfo.Units[pCity:GetProductionUnit()]
.
That was the only thing I changed in the code, but I did restart Civ, so maybe that could have influenced it as well.

__________________________________________________

Now, a third question (not counting the original four), I just had a CTD with the following code. Short, the function is suppose to allow for a couple of units (mounted units more specifically) to repair improvements, by converting the regular units to units which have the BUILD_REPAIR and a non-zero workrate. The thing is, it worked yesterday to the degree that the unit was converted (and converted back) when entering (or leaving) a pillage tile, but I'd forgot to include a unit:kill() statement in the ConvertUnit function, so I'd ended with two units. After adding the Kill() in the Convert file I got the CTD. First, here's the code:
Code:
function DMS_UbugabireMountedUnitsRepair(playerID, unitID, unitX, unitY)
	local pPlayer = Players[playerID]
	if pPlayer and pPlayer:IsEverAlive() and pPlayer:GetCivilizationType() == civilisationBurundiID then
		local pUnit = pPlayer:GetUnitByID(unitID)
		if pUnit and pUnit:GetUnitCombatType() == unitCombatTypeMountedID and (pUnit:GetUnitType() ~= unitGanwaID) then
			print("DMS_UbugabireMountedUnitsRepair initializing for unit on plot (" .. unitX .. "," .. unitY .. ")..")
			local pPlot = Map.GetPlot(unitX, unitY)
			if pPlot and pPlot:GetOwner() == playerID then
				print("DMS_UbugabireMountedUnitsRepair: mounted unit is on owned land..")
				local eImprovementType = pPlot:GetImprovementType()
				if eImprovementType and pPlot:IsImprovementPillaged() then
					print("DMS_UbugabireMountedUnitsRepair: mounted unit is on pillage improvement..")
					print("DMS_UbugabireMountedUnitsRepair: calling DMS_GetUnitConversion..")
					local pNewUnit = DMS_GetUnitConversion(pUnit)
					if pNewUnit ~= nil then -- to avoid conversion twice..
						DMS_convertUnit(pUnit, pNewUnit)
						print("DMS_UbugabireMountedUnitsRepair: mounted unit converted to unit with repair ability..")
					else
						print("DMS_UbugabireMountedUnitsRepair: returned nil from DMS_GetUnitConversion..")
					end		
				else
					if (pUnit:GetUnitType() == unitBurundiHorsemanID) or (pUnit:GetUnitType() == unitBurundiLancerID) or (pUnit:GetUnitType() == unitBurundiCavalryID) then
						print("DMS_UbugabireMountedUnitsRepair: calling DMS_GetUnitReconversion..")
						local pReconvertUnit = DMS_GetUnitReconversion(pUnit)
						DMS_convertUnit(pUnit, pReconvertUnit)
						print("DMS_UbugabireMountedUnitsRepair: mounted unit re-converted to old unit without repair ability..")
					end
				end				
			end
		end
	end
end

GameEvents.UnitSetXY.Add(DMS_UbugabireMountedUnitsRepair)
where the relevant functions called are defined elsewhere:
Code:
--------------------------------------------------------------------------------------------------------------------------
-- DMS_convertUnit
--------------------------------------------------------------------------------------------------------------------------
function DMS_convertUnit(pUnit, pNewUnitType)
	local pPlayer = Players[pUnit:GetOwner()]
	local pNewUnit = pPlayer:InitUnit(pNewUnitType, pUnit:GetX(), pUnit:GetY()) --initialize the data for the new unit
	pNewUnit:SetDamage(pUnit:GetDamage())
	pNewUnit:SetExperience(pUnit:GetExperience())
	pNewUnit:SetLevel(pUnit:GetLevel())
	for unitPromotion in GameInfo.UnitPromotions() do
		local iPromotionID = unitPromotion.ID;
		if (pUnit:IsHasPromotion(iPromotionID)) then
			if (pNewUnit:IsPromotionValid(iPromotionID)) then
				pNewUnit:SetHasPromotion(iPromotionID, true)
			end
		end
	end
	-- kill off the old unit
	pUnit:Kill() 
end
--------------------------------------------------------------------------------------------------------------------------
-- DMS_GetUnitConversion
--------------------------------------------------------------------------------------------------------------------------
function DMS_GetUnitConversion(pUnit)
	print("DMS_GetUnitConversion initializing..")
	local pNewUnitType = nil
	if pUnit then
		if pUnit:GetUnitType() == unitHorsemanID then
			pNewUnitType = unitBurundiHorsemanID
			print("DMS_GetUnitConversion: Horseman unit, return Burundi Horseman..")
		elseif pUnit:GetUnitType() == unitCavalryID then
			pNewUnitType = unitBurundiCavalryID
			print("DMS_GetUnitConversion: Cavalry unit, return Burundi Cavalry..")
		elseif pUnit:GetUnitType() == unitLancerID then
			pNewUnitType = unitBurundiLancerID
			print("DMS_GetUnitConversion: Lancer unit, return Burundi Lancer..")
		end
	end

	return pNewUnitType
end
--------------------------------------------------------------------------------------------------------------------------
-- DMS_GetUnitReconversion
--------------------------------------------------------------------------------------------------------------------------
function DMS_GetUnitReconversion(pUnit)
	print("DMS_GetUnitReconversion initializing..")
	local pNewUnitType = pUnit -- set the new unit to be equal to old, to avoid returning nil
	if pUnit then
		if pUnit:GetUnitType() == unitBurundiHorsemanID then
			pNewUnitType = unitHorsemanID
			print("DMS_GetUnitReconversion: Burundi Horseman unit, return Horseman..")
		elseif pUnit:GetUnitType() == unitBurundiCavalryID then
			pNewUnitType = unitCavalryID
			print("DMS_GetUnitReconversion: Burundi Cavalry unit, return Cavalry..")
		elseif pUnit:GetUnitType() == unitBurundiLancerID then
			pNewUnitType = unitLancerID
			print("DMS_GetUnitReconversion: Burundi Lancer unit, return Lancer..")
		end
	end

	return pNewUnitType
end
This is from my lua Log when moving a Horseman unit to a pillaged tile. As you can see, it looks like the function fire three times, and the third time, it crashes:
Code:
DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair initializing for unit on plot (65,12)..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: mounted unit is on owned land..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: mounted unit is on pillage improvement..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: calling DMS_GetUnitConversion..
 DMS_Burundi_Functions: DMS_GetUnitConversion initializing..
 DMS_Burundi_Functions: DMS_GetUnitConversion: Horseman unit, return Burundi Horseman..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair initializing for unit on plot (65,12)..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: mounted unit is on owned land..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: mounted unit is on pillage improvement..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: calling DMS_GetUnitConversion..
 DMS_Burundi_Functions: DMS_GetUnitConversion initializing..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: mounted unit converted to unit with repair ability..
 DMS_Burundi_Functions: DMS_GanwaRutaganzwaRugambaHeal: bDelay: false
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair initializing for unit on plot (-2147483647,-2147483647)..
 DMS_Burundi_Functions: DMS_UbugabireMountedUnitsRepair: mounted unit converted to unit with repair ability..
 
That is never going to work reliably.

The C++ DLL that triggers the UnitSetXY() event doesn't allow for a modder deleting (killing) the unit it triggers the event for, so continues on its merry way after firing the event using a now invalid unit pointer. Which will eventually cause a CTD

Edit: Consider a unit that is moved 3 tiles in one order (move pointer and right click) - start to A, A to B, B to C - you'll get three UnitSetXY() events - one for A, one for B and one for C - if tile B has a pillaged improvement, your code deletes the moving unit as it enters tile B and replaces it with a new unit that has no concept of what the old unit was doing. Much more importantly, the C++ code is still using the thing you've just deleted!!! BOOOOOOMMMM!!!!
 
Top Bottom