trying to use a minimalist mod to do big things

antaine

King
Joined
Aug 1, 2005
Messages
737
I want to develop a mod of the standard BtS game on an Earth map. I've identified three mods that I'd like to do using Python only, and a few different ways I might get there. My goal is to mod it as little as possible so as not to interfere with multiplayer.

I
First, I'd like to make a mod that ensures correct city placement for certain key cities. I do NOT want to make a complex dynamic naming and renaming system like in Rhye's.

is it possible to define a Python action for an occurrence at the time the occurrence happens rather than simply at the beginning of the turn? If so, I'd like to define an event similar to OnCityFound where any time a city is founded its coordinates are compared to a master list of all coordinates on the map and the city name associated with that tile is selected, no matter who is doing the founding.

If it is not possible to define a Python event except at the beginning of a turn, I'd like to create one that searches every 25 turns for level 1 cities present on certain key tiles. If a city is found on that tile, I want the game to rename it. This way, a key city will only carry a wrong name for a few turns. For instance, I can have it check plot 30,47 every 25 turns, and if there is a level 1 city on that tile, rename it New York regardless of the owner. This way, even if a city is razed and later refounded it will still have the right name. If I have to, I can come up with a short list of perhaps 30 tiles worldwide to be defined like this.

Do either of those approaches seem like they might be possible?

II
If I can define Python events other than for the beginning of the turn, I'd also like to create something that forces certain civs to select certain religions when "choose religion" is turned on. But I need to figure out first if a Python event can be defined for other than the beginning of a turn.

I suppose that since I'd be modding it anyway, I can just mod the leaders to have a particular favorite religion (so that Ramesses selects Islam rather than Hinduism, for instance)

III
I also want to see if I can spawn a unit for a dead civ. I can use this to mimic dynamic starts in a way. For instance, I can start the Romans in 4000BC with only a work boat. That boat will either survive or not. I'd like to use something like Barbs.py to then spawn a Roman settler in 770BC. If I do that with worldbuilder, the Roman civ comes "back to life" even though it was destroyed before. How can I get Python to generate a unit for a civ that currently has no representation on the map? Civs that "start" that way will, of course, not be playable by humans from the beginning. I would also need to first perform a search for any cities on their start tile and either raze that city or (better yet) change the city owner to the intended civ (only spawning a settler if the tile is empty).

Before I embark upon trying to code these things amidst my heavy work schedule, I just want to know if there are things that are based on false principles and will not work at all.
 
1. Use CvEventManager.onCityBuilt.

2. Yes Python can be triggered from many different events. Check out CvEventManager.py for things that are triggered by events. CvGameUtils.py can be used for controlling things.

3. I think you can just use pPlayer.initUnit(....) to create a new unit and "revive" a civ. It works fine in my mod, but I'm not absolutely certain if it will work in a standard game.
 
Dune Wars has a simple but effective system for dynamic renaming using onCityBuilt. You may want to look into the python which was written by deliverator and myself. When the city is built, it looks at the terrain under the city and may add a suffix onto the name such as "Peaks" if on hills. Choosing a specific name when "near" certain coordinates would be a fairly simple change to that existing code.
 
my plan is to use a city name dictionary as a library of coords and associated names. I will (eventually, as I learn how), create a python call to look at the coords whenever a city is founded, and if that tile is in the list, to name the city the associated name. I didn't intend to play around with defining "near," I just listed multiple coords for the same name (acceptable adjacent tiles).
 
my plan is to use a city name dictionary as a library of coords and associated names. I will (eventually, as I learn how), create a python call to look at the coords whenever a city is founded, and if that tile is in the list, to name the city the associated name. I didn't intend to play around with defining "near," I just listed multiple coords for the same name (acceptable adjacent tiles).
It is quite possible to have the code look for adjacent tiles - instead of defining each city several times in the same dictionary. Firstly the code looks up the actual coordinates, and if no name entry is found, then it loops through all the adjacent tiles. If a entry is found for any of those then that tile's name will be used instead. All you pretty much need is a looping technique:
Code:
for x in range(x - 1, x + 2):
    for y in range(y - 1, y + 2):
        tCoords = (x, y)
        if tCoords in cityNameDict:
            newName = cityNameDict[tCoords]
            pCity.setName(newName, False)
            break
It should be noted that the "+ 2" is because the computer counts from zero and up. It'll make sense once you understand how the range() function works. (It actually creates a list, by the way.)

Also, you need the break command to quit the looping once a dictionary entry is found. Otherwise the name might change several times which is unnecessary. (Does it require 2 break to exit both loops?)

If you wanna have more control over which cities are more important than others (because a looping script is dumb) you could add a priority value to each dictionary entry. Then you could make both the key and the associated value a tuple:
Code:
cityNameDict = { (34, 45): ("London", 10) }
You could pretty much just have a priority of True/False if you don't wanna design an entire priority scale...

Then the loop wouldn't automatically execute once a name is found (lose the break command) but it would rather assign a new value to the newName variable if a tile entry with a higher value is found. The actual city naming would take place outside of the loop(s):
Code:
iHighestPriority = 0
for x in range(x - 1, x + 2):
    for y in range(y - 1, y + 2):
        tCoords = (x, y)
        if tCoords in cityNameDict:
            tEntry = cityNameDict[tCoords]
            iCurrentPriority = tEntry[1]
            if iCurrentPriority > iHighestPriority:
                iHighestPriority = iCurrentPriority
                newName = tEntry[0]
pCity.setName(newName, False)
Something like this could of course wait until you both have the basic functionality working and once you fully understand the examples above. You won't learn anything by just copy-pasting some code that someone else has posted here. :p
 
Alright, so I've read the rebels tutorial you posted as well as this guide ( http://www.greenteapress.com/thinkpython/thinkCSpy/html/index.html ). I've also been working my way through that general python e-book you linked, Baldyr. So as not to repeat everything twice (and to possibly serve as a help to others) I'd like to merge our message convo with this thread (if that's alright with you).

I think the thing that will be most difficult for me is bridging the gap between the general python principles and the specific elements of civ code. Mostly because someone else developed Civ and I don't know where to find all the stuff I need to reference (I've always been an example-oriented kinda guy...I couldn't build a harp following the directions until I'd seen a photo of the finished model, then the directions almost seemed redundant).

Still finishing the non-civ-specific textbook, but as it's almost midnight here, and I have to teach tomorrow, it will have to wait until later ;-)

I did go through and create a full list of city coords for that dictionary, now I just have to read far enough to know where to put it.

I also realized something that will probably send me back to the drawing board on part of this - you cannot get the AI to willingly put a city less than two tiles away from another city (although I imagine you can force a city to spawn there just like with the worldbuilder) through python or xml...no one-tile separations means no Spanish and Portuguese at the same time unless I force-spawn both Madrid and Lisbon simultaneously. I think I'm just going to have to cut the Portuguese out - I guess the Koreans are back in.

I've been giving some thought to the other end of the mod (although the map-naming thing is priority number one). I think I'm going to start the "later civs" with lone workboats in isolated, one-tile lakes as placeholders, then use Python for a city "flip" + garrison spawn combo to get them on the map. I'm thinking the process (but not the code) will run something like this:

1 - on 800AD, search tile for London as well as all adjacent tiles
2 - delete all units on those tiles (but not the cities, resources, improvements, etc) - or set UnitOwner to England
3 - set war between England and CityOwner
4 - set war between England and Celts (if Celts weren't the owner of the city, and if Celts are alive)
5 - spawn a garrison of English soldiers directly on the city, including a settler (the settler is for the unlikely event that the English decide to raze the city instead of capturing it, then they can refound it on the spot)
6 - if no city is found, destroy all units on those tiles (or make them English, as above)
7 - set war between England and Celts (if Celts are alive)
8 - spawn units and a settler on my preferred London tile


This will essentially mimic Rhye's spawn while eliminating the Autoplay and the need to destroy the city. In RFC, it always bothered me that a wonder could be built in London and it will be lost when the city "flips"...because it doesn't actually "flip" it is destroyed and replaced by another city of the same name on the same tile.

I also have plans for some colonial spawns dictated by date (for Celts and Americans) or tech acquisition (astronomy for most other civs)

The downside? The human player will have to <return> through all the turns manually while staring at a landlocked boat in the north pole. Still, that autoplay really messed with multiplayer, which I'd like to preserve. Actually, once I do the map spawn, I can kill the boat and re-hide those tiles...

The point to this mod (working title "Dawn of Time") is to get a 4000BC start with a reasonable approximation of city placement and general, "broad brush" historical accuracy while preserving multiplayer functionality. I suppose the "point" would be to play one of the earlier civs and have to deal with the later spawns, but I like to leave as many options as possible open for players. Knowing how annoyed I am when my favorite civ is included as a minor civ, why would I purposely do that to others?

Anyway, my most immediate goal is going to be drafting code that will search the tile on which a city is founded and pull the required name from the list which, truth be told, was really all I'd set out to do when I began. I'd like not to use the "priority" method, honestly, because not all tiles are created equal (for instance, Shanghai shouldn't be on any neighboring tile, but only coastal ones...and yes, I know that could be defined with code as well, but as I've already got the tile-by-tile list, I really don't need to complicate the code any more than I need to).
 
Assets/XML/GlobalDefines.xml has a setting that controls the minimum distance between any two cities on the same continent. You could change it to 2 (or 1 if it's 2 already) to allow closer founding. I have no idea if the AI will take advantage of that, however.
 
hm....all this time I was thinking that it was a C++ thing. I'm going to have to do a little with xml (city list for Hebrews, religion prefs for leaders, kill global warming, etc), so I'll just do that as well. Heck...xml is the easy part...
 
I think the thing that will be most difficult for me is bridging the gap between the general python principles and the specific elements of civ code. Mostly because someone else developed Civ and I don't know where to find all the stuff I need to reference (I've always been an example-oriented kinda guy...I couldn't build a harp following the directions until I'd seen a photo of the finished model, then the directions almost seemed redundant).
You need both the theory and practice - you can't learn programming in practice from a book.

Everything you can possibly learn from these sources are applicable to CivIV modding - except file I/O operations. But even those chapters contain important parts (pickling). You won't regret learning about the rest. (Well you can skip the last few chapters in the textbook, for starters.)

As for the CivIV Python Application Programming Interface - you can find it here. You of course need to understand how to use it and how to find what you need, but once you understand "Object-Oriented Programming" (covered in the textbook) it will make all the sense in the world. For simple Python scripting you hardly need to know how to use classes of your own, but the CivIV API is built around classes and consists of methods (class functions). So you only need to understand the concept in theory - and this will enable you to use the API for real.

You seem to be well on your way and you'll see that you will be able to do these things in no time at all once you begin. But don't be afraid to ask for help either - because once you know what you're doing it would actually make sense for others to help out. (Without having to do the whole thing for you. Or teach programming to you.)

There is also a way to name actual map tiles (CyPlot instances - this is what you need the OOP knowledge for) in a way that you don't even need to lookup names and coordinates in a dictionary. The tiles would be named with CyCity.setScriptData(string) at the beginning of the game and then it would be as easy as this to make the (re)naming happen:
Code:
pPlot = gc.getMap().plot(x, y)
plotName = pPlot.getScriptData()
if plotName != "":
    pCity.setName(plotName, False)
Just so you know what your options are. The names would be set by either reading a list or parsing a text file (which would also make the file I/O stuff useful for you ;)). You can use a dictionary also, if you already made one. :p
 
here's what I was going to do with the civs themselves. I was going to start with a blank map and the four ancient river valley civs to which I was also going to add a few important ancient civs (I added the Greeks because there were farming communities in the eastern Mediterranean at this time, the Celts because they did come to blanket most of the rest of the continent in this period, and the Hebrews because, well, they sat at the crossroads of the world and it would be exceedingly difficult for the player to start them later given their geography):
Celtic Confederacy
Egypt
Mesopotamia
India
China
Greeks
Hebrews

Now, I've yet to decide whether I want to start the other civs in large blocks or stagger them. I think there's good and bad in both. The civs that would be "spawned" (remember, they've actually been on the map all along) in the classical period (starting 770BC) are
Romans
Persians
Japanese
Koreans

The medieval civs would spawn 800AD
Arabs
English
French
Spanish
Germans
Russians

and finally, in 1775
Americans

With the exception of the Americans, these would not be RFC-style "area" flips, but rather the flip of a single city, the capital, along with a modest army and a ready-made war&#8230;it'll be up to the player or the AI to take over the rest of the territory around there. The US is going to be handled with a fundamentally different philosophy from the other civs in that they will (eventually) get two area flips (one in 1790 and one in 1900), but I justify that because the US player will basically miss most of the game (if human) and should be a strong challenge to the others (if AI)

There will be no option to take over new-spawning civs (they will be selectable from the start), and no option to refuse flips (just a note like "the Frankish citizens of Paris have declared their independence from the Roman Empire")
 
(there would also be massive barbarian spawns to represent the Vikings, Huns and Mongols)
 
Yeah, its a good thing you're learning Python for this. :)

My advice is to keep things as simple as you can and get the mod working. Once there is a working prototype you will inevitably start adding stuff - and it will never end. But try to work with fully functioning versions that only add one very specific new feature each time - along with the tweaks and edits that actual play testing is prompting you to do.
 
Regarding OOS - it would be the random stuff that is happening during autoplay that causes this? So if you take out the random value generation it should work then?

Also, I'm not sure if you need the Work Boat setup - at all. As long as at least one human player is in control from the first turn those other Civs can just be dead from start. They will be automatically revived once you spawn them a unit. (But I've have very little experience of multiplayer - and none of modding for multiplayer.)

I just made a work-around for this for use with PyScenario, by the way. It allows the scenario maker to revive a dead Civ and flip cities to it without spawning a unit first. (The units would normally be spawned after a city flips.) You're welcome to use my code if you think you need it. Just PM me when you do need it and I'll packet it nicely for you.
 
alright, I've worked up something preliminary-like. There are still gaps in the way I can see to do things, so I've commented out those sections and tried to explain in caps what I think needs to go there even though I don't know how to phrase it properly.

Now, what I'm working on here, when I get it straight, will give me something to test. It will, the way its set up, require a separate entry for each coordinate as it will only test one and force a name if the condition for that coordinate is correct. Still, if I can get it to work that way for just one city tile it will be a victory!

sorta where I'm heading in-process:

---------------------
Code:
import CvEventManager


#searches tile on which a city is built and either forces a keyname or allows the default process to continue



def namenorm
  onCityBuilt #CHECK COORDS
    #IF COORDS EQUAL X, Y
      return 1
    #IF NOT COORDS EQUAL X, Y
      return 0

  if namenorm == 1
    #FORCE THE NAME OF THE CITY TO BE xxx
  else:
    #USE THE DEFAULT NAMING PROCESS
 
Put your code between [code] and [/code] tags. You can do this using the # button above the advanced editor.
 
Alright, so I'm really not seeing the way I need to phrase the parts I've commented out. I wonder if I can use

Code:
  onCityBuilt
    if x = 51, y = 55
      return 1
    else
      return 0

will it automatically check the tile on which onCityBuilt is happening, or do I somehow need to identify which tile I want it to check when the trigger happens?
 
If you look at CvEventManager.py you'll see an existing function called onCityBuilt(). To start, copy that file into your My Games / Beyond the Sword / CustomAssets / Python folder. Then edit the copy, and add your code to that existing function.

The function should already be assigning the city that was founded to a variable called city or pCity. You can use that to get its X and Y coordinates:

Code:
def onCityBuilt(self, argsList):
    pCity = argsList[0]
    coord = (pCity.getX(), pCity.getY())
    if coord in dictionaryOfCoordsWithCityNames:
        name = dictionaryOfCoordsWithCityNames[coord]
        pCity.setName(name)
 
Top Bottom