Rearranging the order that appears in the city queue

Afforess

The White Wizard
Joined
Jul 31, 2007
Messages
12,239
Location
Austin, Texas
When I add modular units or buildings, they always end up appended to the end of the city build list, and it's really annoying, because they are being stuck where Wonders usually go. Is there any way to make Civ4, after loading the XML, to resort the buildings and units, based on the building type (Regular building, Wonder, ...etc) and the alphabetic order?

I know that WoC used a Arrange function that they made to rearrange leaders into an alphabetical order, which is fairly similar to what I want to do. However, I don't understand all the code in the WoC CvXMLLoadUtilitySet.cpp, and what they did there.

Can anyone shed some light on this?
 
There is no automatic method by which you can do this, at least, none already existing.

WoC's method for arranging the leaders and Civs is actually a pretty horrible thing. It works for base BtS code because those two files don't have a readPass2, IIRC the sorting happens as soon as they are loaded so other files which reference them aren't borked, but if someone adds a pass2 to either of those two XML files it'll hose them.

The better approach would be to slip the sorting method into the XML loading method right before the second readpass is triggered, as at that point there are NO items which refer to the XML data by number yet. Adding them in to that section of the code ought not to be too difficult though, you would just add another optional boolean to the LoadGlobalClassInfo command along the lines of bSortData and slip in a call to the sorting method which WoC wrote (the actual sorting itself is perfectly fine, just the oversight of not accounting for pass2 makes it potentially break).

I personally prefer to manually sort the XML, though that isn't an option once you introduce modules. Mostly so that when I am debugging I can work from the integers in the code instead of having to track down a full Infos object which will contain the Description textkey. (ie - unsorted XML means that I can go into Notepad++ and collapse the entries and count from 0 to 54 and find the bugging out unit #54, but sorted XML means I need to find a kUnitInfo object in the Debugger somewhere so I can see the <Description> or <Type> string and figure out which unit is bugging.
 
There is no automatic method by which you can do this, at least, none already existing.

WoC's method for arranging the leaders and Civs is actually a pretty horrible thing. It works for base BtS code because those two files don't have a readPass2, IIRC the sorting happens as soon as they are loaded so other files which reference them aren't borked, but if someone adds a pass2 to either of those two XML files it'll hose them.

Well, I can't think of any reason to add a pass2... so they're safe for now.

The better approach would be to slip the sorting method into the XML loading method right before the second readpass is triggered, as at that point there are NO items which refer to the XML data by number yet. Adding them in to that section of the code ought not to be too difficult though, you would just add another optional boolean to the LoadGlobalClassInfo command along the lines of bSortData and slip in a call to the sorting method which WoC wrote (the actual sorting itself is perfectly fine, just the oversight of not accounting for pass2 makes it potentially break).

Are your sure I could still use their function for sorting? I'd probably have to write a new one. That's my first difficulty, as I don't completely understand what they did.

Actually, after looking at it, it does look like they made it fairly generic. They called it with Civilizations and Leaders.

When you say "right before the second readpass", are you referring to the readpass2, or literally, the second readpass? And, if I understand correctly, I should add a new optional parameter to LoadGlobalClassInfo, that if true, the XML gets sorted alphabetically?

I personally prefer to manually sort the XML, though that isn't an option once you introduce modules. Mostly so that when I am debugging I can work from the integers in the code instead of having to track down a full Infos object which will contain the Description textkey. (ie - unsorted XML means that I can go into Notepad++ and collapse the entries and count from 0 to 54 and find the bugging out unit #54, but sorted XML means I need to find a kUnitInfo object in the Debugger somewhere so I can see the <Description> or <Type> string and figure out which unit is bugging.

Yeah, well I don't lose functionality by sorting them, because it's already a PITA if building # 401 is breaking, as anything above 350 is a module... and who knows which one loaded first... Yes, I could figure it out, given 15 minutes, but it would be easier just to follow the code...
 
Also, I was thinking of another area of modules that really bugged me. Civics.

If I add a new civic, it get's appended to the list of civics, and comes last in the list, even if it was meant for the first ancient era civic. If I add a new civic column, it gets put after all the other civic columns, in a funny spot.

I think that this could be fixed by giving civicoptioninfo a new XML tag for <ColumnOrder> and make it ascend from 1. Civic columns would be specified by this tag instead of the XML order.

Then, the civic issue could be fixed by a <Order> tag that ascends from 1 as well, listing the order the civics should appear in the column.

So, in the XML it would look like this:

CIV4CivicOptionInfos
Code:
		<CivicOptionInfo>
			<Type>CIVICOPTION_GOVERNMENT</Type>
			<Description>TXT_KEY_CIVICOPTION_GOVERNMENT</Description>
			<TraitNoUpkeeps/>
[COLOR="Red"]			<ColumnOrder>1</ColumnOrder>[/COLOR]
		</CivicOptionInfo>


CIV4CivicInfos

Code:
		<CivicInfo>
			<CivicOptionType>CIVICOPTION_GOVERNMENT</CivicOptionType>
			<Type>CIVIC_CHIEFDOM</Type>
			<Description>TXT_KEY_CIVIC_CHIEFDOM</Description>
			<Civilopedia>TXT_KEY_CIVIC_CHIEFDOM_PEDIA</Civilopedia>
			<Strategy>TXT_KEY_CIVIC_CHIEFDOM_STRATEGY</Strategy>
			<Button>,Art/Interface/Buttons/Civics/Chiefdom.dds,Art/Interface/Buttons/RoM_Civic_Atlas.dds,8,1</Button>
			<TechPrereq>NONE</TechPrereq>
			<iAnarchyLength>0</iAnarchyLength>
			<Upkeep>UPKEEP_LOW</Upkeep>
			[COLOR="Red"]<Order>1</Order>[/COLOR]
			...
		</CivicInfo>

Then, Civics would "truly" be modular...

Of course, behind the scenes, the SDK would just be re-arranging civics in the column and XML order listed above, similar to the way it arranges leaders by alphabet, except in this case, by a modders order.
 
Before Readpass2 meaning before the point where the generic call is made to readpass2, so right before the big IF chunk for the current boolean in the LoadGlobal call. That way it is available for all functions.

Though thinking about it again, IIRC the readpass2 runs for the base XML, then runs again for each module. So actually it wouldn't work even if placed in that location. You would have to change it so that readpass1 executes on all modules, THEN readpass2 executes on all loaded information.

Honestly I am curious why they chose to have pass2 and 3 instead of just doing pass3 for all files. Were I personally designing the load mechanism I would quite likely do a pass through all XML everywhere loading Type only, THEN come back and snag information, so that all enumerations exist already for all files before any real XML is loaded. But I imagine there is some basic programming reason not to approach it in such a manner (probably speed, though with how convoluted things are now, I imagine that the tradeoff isn't notable).


For the civics, it would actually be the job of python to set the order up visually. SDK sorting wouldn't be required. That one you can do without worrying about ordering issues and read passes.
 
If you reorder the CvInfos before readpass2(), won't it read into the wrong infos? Doesn't readpass2() read data in the same order it appears in the XML files? Or does it look up each CvInfo by type first?
 
Gah, you're right, you'd have to completely redesign pass2 to find the right info object to append in the first place. Though WoC does do that to some degree, if not completely, as I recall.


So in summary of the thread: If you want the DLL to auto-sort your XML, you have to completely rewrite how the DLL loads XML. :(

It'd be nice, but not nice enough that I expect anyone to go through the pain of coding it. If you WANT to invest some time, but what we have discussed is too much for you to comprehend, the "easy" solution would be to move all pass2 actions into pass3 actions. Then you can sort each XML file as soon as it finishes loading.
 
It'd be nice, but not nice enough that I expect anyone to go through the pain of coding it. If you WANT to invest some time, but what we have discussed is too much for you to comprehend, the "easy" solution would be to move all pass2 actions into pass3 actions. Then you can sort each XML file as soon as it finishes loading.

How exactly would I go about changing a readpass2 to a readpass3?
 
You would load all fields which need Pass2 as strings (m_aszExtraXMLForPass3 is the currently used one IIRC) during ::read(pXML), then in Pass3 you would go through those strings and finally convert them to Integers, clear the string and save the Integer.
 
EmperorFool, I can't believe you didn't tell me that I could have just re-arranged them in the main interface, then ran the loop like normal!:p

Here's what I am thinking, I create an array, and arrange the buildings in alphabetical order for it. (Does the Sevopedia use something similar for it's sorting? Is there any existing array I can hijack?), and then loop through the new sorted array instead of GC.getNumBuildingInfos(), and my construct queue will be perfectly sorted. Similar thing for Units too.

Here's my questions, is there a better way to sort than into a secondary array? I know some sorting techniques, but can't name any that would work better for this task.

Also, if I use a pre-sorted array, is there a way for me to sort it once, and be done until next time I load the game? That way I don't sort each time I select a city (Ug... the lag).
 
If you sort alphabetically, wonders will be mixed in with normal buildings. Otherwise, this would work.

To sort an array of BuildingTypes based on some other field from CvBuildingInfo, such as its description, create an array of tuples:

Code:
buildings = []
for i in range(gc.getNumBuildingInfos()):
    info = gc.getBuildingInfo(i)
    buildings.append((info.getDescription(), i))
sort(buildings)
for _, i in buildings:
    info = gc.getBuildingInfo(i)
    # add info.getButton() to button panel

Sevopedia has code that splits the buildings into the three types. You could use it to put a building type (0 normal, 1 wonder) as the first element of the tuples. When you sort the list, it will be sorted first by normal/wonder and then by description.
 
If you sort alphabetically, wonders will be mixed in with normal buildings. Otherwise, this would work.

To sort an array of BuildingTypes based on some other field from CvBuildingInfo, such as its description, create an array of tuples:

Code:
buildings = []
for i in range(gc.getNumBuildingInfos()):
    info = gc.getBuildingInfo(i)
    buildings.append((info.getDescription(), i))
sort(buildings)
for _, i in buildings:
    info = gc.getBuildingInfo(i)
    # add info.getButton() to button panel
Sevopedia has code that splits the buildings into the three types. You could use it to put a building type (0 normal, 1 wonder) as the first element of the tuples. When you sort the list, it will be sorted first by normal/wonder and then by description.

Okay, but where in the python should this go? I only want to sort these once, then re-use the pre-sorted array. Re-sorting seems inefficient.
 
You could put this into CvMainInterface's __init__() function and store the final array or just the IDs.

Code:
... sort ...
self.sortedBuildings = [for _, i in buildings]
 
Hmm, I'm not sure what I'm doing wrong here. I made g_buildings = [] a global variable, outside of the CvMainInterface class. Then I added this to the bottom of the init() function:
Code:
		for i in range(gc.getNumBuildingInfos()):
			info = gc.getBuildingInfo(i)
			g_buildings.append((info.getDescription(), i))
		g_buildings.sort()

And I edited the top part of the building construct loop:
Code:
				for i in g_buildings:
					info = gc.getBuildingInfo(i)
					if (not isLimitedWonderClass(info.getBuildingClassType())):
						eLoopBuilding = gc.getCivilizationInfo(pHeadSelectedCity.getCivilizationType()).getCivilizationBuildings(info.getBuildingClassType())

I get nothing in my python error logs, but my city interface is destroyed. The rest of the UI shows up fine though.
 
I see a couple problems. First, it may be that the XML information is not fully loaded at the time that __init__() is called. This is why BUG introduced the <init> tag: some information in CyPythonExtensions is not available when the Python modules are loaded. :( Check that your first loop finds buildings and they have descriptions.

Next, you are first looping over all buildings and later converting those buildings to their class to the civ's building. This will make each building show up N times where N = 1 + <# of UBs for it> for all civs.

Instead, loop over all building classes and look up the active player's civ's building to build the initial list that gets sorted. Then you can loop over these buildings without regard to civilization. Since the build icons are only shown for the active player, you don't need to worry about foreign cities.

If you still can't get it to work . . . debug output!
 
I see a couple problems. First, it may be that the XML information is not fully loaded at the time that __init__() is called. This is why BUG introduced the <init> tag: some information in CyPythonExtensions is not available when the Python modules are loaded. :( Check that your first loop finds buildings and they have descriptions.

Next, you are first looping over all buildings and later converting those buildings to their class to the civ's building. This will make each building show up N times where N = 1 + <# of UBs for it> for all civs.

Instead, loop over all building classes and look up the active player's civ's building to build the initial list that gets sorted. Then you can loop over these buildings without regard to civilization. Since the build icons are only shown for the active player, you don't need to worry about foreign cities.

If you still can't get it to work . . . debug output!

Yep, CvMainInterface loads before the XML. I through some debug logging in, and the XML arrays are empty. :sad:

So, how do I get this to run later?
 
Initialize g_buildings to None at the module level. Then where the icons are added you build the list:

Code:
global g_buildings # must have this!
if g_buildings is None:
    g_buildings = []
    for i in range(gc.getNumBuildingClassInfos()):
        ...
    sort(g_buildings)
... icons add to panel ...

This ensures that the list is created only once and rebuilt any time it's needed (e.g. after reloading Python).
 
Hmm, I'm getting a python exception I can't seem to fix. Probably a n00b error, here's my code:

Code:
				global g_buildings
				if g_buildings is None:
					g_buildings = []
					CvUtil.pyPrint("Sorting Buildings" )
					for i in range(gc.getNumBuildingClassInfos()):
						info =  gc.getBuildingClassInfo(i).getDefaultBuildingIndex()
						g_buildings.append((info.getDescription(), i))
						CvUtil.pyPrint("Found %s",info.getDescription() )
					g_buildings.sort()
					CvUtil.pyPrint("End Sort" )

Traceback (most recent call last):

File "CvScreensInterface", line 983, in forceScreenRedraw

File "CvMainInterface", line 3279, in redraw

File "CvMainInterface", line 4467, in updateSelectionButtons

AttributeError: 'int' object has no attribute 'getDescription'
ERR: Python function forceScreenRedraw failed, module CvScreensInterface
 
You call getDescription() on the info object, which is here info = gc.getBuildingClassInfo(i).getDefaultBuildingIndex() just an int.
Just gc.getBuildingClassInfo(i) instead should do it.

I'm confused, I changed it like you said, but I just get another exception, here's my code:

Code:
                global g_buildings
                if g_buildings is None:
                    g_buildings = []
                    CvUtil.pyPrint("Sorting Buildings" )
                    for i in range(gc.getNumBuildingClassInfos()):
                        info =  gc.getBuildingClassInfo(i).getDefaultBuildingIndex()
                        g_buildings.append(gc.getBuildingClassInfo(i).getDescription(), i))
                        CvUtil.pyPrint("Found %s",gc.getBuildingClassInfo(i).getDescription() )
                    g_buildings.sort()
                    CvUtil.pyPrint("End Sort" )

Traceback (most recent call last):
File "<string>", line 1, in ?
File "<string>", line 52, in load_module
File "CvEventInterface", line 17, in ?
File "<string>", line 52, in load_module
File "BugEventManager", line 96, in ?
File "<string>", line 52, in load_module
File "CvEventManager", line 12, in ?
File "<string>", line 52, in load_module
File "CvScreensInterface", line 3, in ?
File "<string>", line 35, in load_module
File "<string>", line 13, in _get_code
File "
CvMainInterface
", line
4467


g_buildings.append(gc.getBuildingClassInfo(i).getDescription(), i))


^
SyntaxError
:
invalid syntax
 
Back
Top Bottom