CSD
Noble on principle
- Joined
- Apr 6, 2023
- Messages
- 23
Spoiler Background :
I wanted to solve the infinite tech exploit without making changes to the DLL (and preserving Oracle/Liberalism mechanics) which meant python was needed.
After many failed attempts to prevent the exploit, I finally developed a viable alternative: add to a "freeTechsAllowed" counter per player for completed free tech granting buildings/projects/techs and check if it is greater than zero when a free tech is gained. If true, decrement counter; else, remove their ill-gained tech and recalculate the player's era.
Everything seemed to work perfectly after quick testing with the aid of worldbuilder, until I was struck with this horrible thought that my humble counter couldn't survive between sessions.
Spoiler :
It didn't.
"Up in the sky, look: It's a bird! It's a plane! It's ScriptData! And it can save the day, once per instance* (data serialization imported separately)!"
It's a persistent string intended for modding, which technically I could save my counter to directly, but if I want to later add a second persistent variable or more, I'm in a pickle.
Thankfully, I found a relatively simple solution with little overhead to setting multiple variables per ScriptData string which is clearly presented below for the benefit of my fellow python modders.
Spoiler Tutorial :
There is a single persistent variable (ScriptData) for each instance of several classes (i.e. CyCity, CyGame, CyPlayer, CyPlot, CyUnit) which has getter and setter methods:
STRING getScriptData()
VOID setScriptData(STRING)
The pickle library is already used for some data serialization in Civ4, so I've decided just to stick with it, but it's worth noting there are alternatives.
There are many methods in pickle but only two will be required here:
OBJECT loads(STRING)
STRING dumps(OBJECT)
But what object allows us to effortlessly map multiple variables? A dictionary!
First, we need to
import pickle
, or python will throw shade.Also, pickle needs to know what object type we want, so we pass an empty dictionary:
cyInstance.setScriptData(pickle.dumps({}))
.Then we load a copy of our dictionary into a local variable:
dLocal = pickle.loads(cyInstance.getScriptData())
.Now we can insert any variable we want into the dictionary and set its value in the same line of code:
dLocal["anyVariable"] = bar
.Furthermore, we can take the current or default value of our variable as an argument:
dLocal["anyVariable"] = foo(dLocal.get("anyVariable", bar))
.If we get the current value of our variable without a default, make sure to define it first or it will return None:
bar = dLocal.get("anyVariable")
.Finally, remember to save your dictionary:
cyInstance.setScriptData(pickle.dumps(dLocal))
.Spoiler Generic Example :
Python:
import pickle
game = gc.getGame()
# Initialize dictionary for game
game.setScriptData(pickle.dumps({}))
# Choose default values
key1Default = 0
key2Default = ""
# Load game dictionary
dGameVars = pickle.loads(game.getScriptData())
# Set values independent of current values
dGameVars["key1"] = someFunction()
dGameVars["key2"] = anotherFunction()
# Get values with defined defaults
iName = dGameVars.get("key1",key1Default)
szName = dGameVars.get("key2",key2Default)
# Get values with default to None
iName = dGameVars.get("key1")
szName = dGameVars.get("key2")
# Set values dependent on current values only (not recommended unless None is valid state)
dGameVars["key1"] = someFunction(dGameVars.get("key1"))
dGameVars["key2"] = anotherFunction(dGameVars.get("key2"))
# Set values dependent on its current values or defaults (recommended)
dGameVars["key1"] = someFunction(dGameVars.get("key1",key1Default))
dGameVars["key2"] = anotherFunction(dGameVars.get("key2",key2Default))
# Save game dictionary
game.setScriptData(pickle.dumps(dGameVars))
Spoiler Specific Example :
This is the relevant code from CvEventManager.py I used for solving the infinite tech bug. Note that only a single variable "freeTechsAllowed" is currently being mapped to each player dictionary, but more can easily be added now.
Python:
import pickle
class CvEventManager:
def onGameStart(self, argsList):
for iPlayer in range(gc.getMAX_PLAYERS()):
player = gc.getPlayer(iPlayer)
player.setScriptData(pickle.dumps({}))
def onBuildingBuilt(self, argsList):
iPlayer = pCity.getOwner()
player = gc.getPlayer(iPlayer)
if (gc.getBuildingInfo(iBuildingType).getFreeTechs() > 0):
dPlayerVars = pickle.loads(player.getScriptData())
dPlayerVars["freeTechsAllowed"] = dPlayerVars.get("freeTechsAllowed",0) + gc.getBuildingInfo(iBuildingType).getFreeTechs()
player.setScriptData(pickle.dumps(dPlayerVars))
def onProjectBuilt(self, argsList):
iPlayer = pCity.getOwner()
player = gc.getPlayer(iPlayer)
if (gc.getProjectInfo(iProjectType).getTechShare() > 0):
dPlayerVars = pickle.loads(player.getScriptData())
dPlayerVars["freeTechsAllowed"] = dPlayerVars.get("freeTechsAllowed",0) + gc.getNumTechInfos() # Placeholder for techs due
player.setScriptData(pickle.dumps(dPlayerVars))
def onTechAcquired(self, argsList):
team = gc.getTeam(iTeam)
player = gc.getPlayer(iPlayer)
if (gc.getGame().countKnownTechNumTeams(iTechType) == 0 and iPlayer == team.getLeaderID()):
dPlayerVars = pickle.loads(player.getScriptData())
dPlayerVars["freeTechsAllowed"] = dPlayerVars.get("freeTechsAllowed",0) + gc.getTechInfo(iTechType).getFirstFreeTechs()
player.setScriptData(pickle.dumps(dPlayerVars))
if (gc.getGame().isFinalInitialized() and not gc.getGame().GetWorldBuilderMode()):
if (team.getResearchLeft(iTechType) > 0):
dPlayerVars = pickle.loads(player.getScriptData())
if (dPlayerVars.get("freeTechsAllowed",0) > 0):
dPlayerVars["freeTechsAllowed"] = dPlayerVars.get("freeTechsAllowed",0) - 1
player.setScriptData(pickle.dumps(dPlayerVars))
else:
team.setHasTech(iTechType,False,iPlayer,False,False)
CyInterface().addImmediateMessage("UNAUTHORIZED FREE TECHNOLOGY REMOVED", "")
playerEra=0
for jTechType in range(gc.getNumTechInfos()):
if (team.isHasTech(jTechType)):
techEra = gc.getTechInfo(jTechType).getEra()
if (techEra > playerEra):
playerEra = techEra
player.setCurrentEra(playerEra)
return
Last edited: