CivSmith - Mod Editor

Senap

Chieftain
Joined
Feb 22, 2009
Messages
81
CIVSMITH - MOD EDITOR
version 0.3.1, updated 2009-05-17


CivSmith is an editor that aims to be extremely simple to use and gathers relevant data for you before you start editing. This means that there are no hardcoded pre-defined values to speak of in the various controls (As in other editors :undecide:). Let's say you have just started a mod and don't even have a CIV4UnitInfos.xml file in your mod. CivSmith will read the data from your mod's base to allow you to browse the available units for your mod (despite the fact that you don't have this file in your mod). As soon as you try to edit a unit, it will copy the file to your mod and then apply the changes immediately. All the aspects of the software revolve around this idea, that it should automatically do things (and do them correctly) rather than have the user worry too much about files and locations. The software completely lacks save buttons as the data is saved on-the-fly to XML.

For existing mods, it automatically gathers the relevant data from a bunch of XML files. For this reason, the application will always display relevant pre-defined values that will work in your mod. For instance, if your mod uses a completely custom set of unit classes, then CivSmith will gather the unit class data and allow you to select from any of your classes when editing units. If you mod lacks unit class data, it will get the data from the mod's base i.e. the default game.

When you're ready to test, you can use the in-editor functions to launch Civilization 4 with your mod or launch Windows Explorer to browse your mod's files.

The application is written in two languages, first in the Delphi language/environment to create a pleasing GUI and in Python for reliability, its easy-to-write nature and openness. It relies heavily on Python scripts to allow for customization. My aim is that it will become an established mod editor in the community and that eventually, other devs will contribute to the project. It uses a simple plugin system that allows Python programmers to add their own functionality to the application. I have used this system to create two plugins of my own:


Unit Editor
This editor allows you to edit CIV4UnitInfos.xml through an interface that is easy to use. Various drop-down lists, checkboxes and edit boxes allow you edit different aspects of the file. It gathers data from 11 other XML files in your mod or base. The data is used to fill up drop-down lists with pre-defined values.


Tech Editor
The Tech Editor edits CIV4TechInfos.xml similarly to the Unit Editor. It works the same way but for this preview, a lot of time and focus went into creating the Tech Grid. This graphical representation of a tech tree allows for easy customization of tech positions and has a mode for editing a tech's prerequisites as well.


Screenshots from version 0.3

Unit Editor
civsmith0.3_uniteditor_general.jpg


Tech Editor with Tech Grid
civsmith0.3_techeditor_techgrid.jpg


Tech Grid
civsmith_0.3_gridinaction.jpg



Download
You can download it from my site (Note that it requires Python 2.5 and one additional package for it to run, see the README.txt for more info).
This preview has support for Beyond the Sword only.
http://thirdpilot.net/projects/civsmith/download


Documentation
This is just a simple description of what the two editors do. You might also want to take a look at the documentation, as it describes in great detail what CivSmith does and does not do, it also serves as a walkthrough for the newbies or curious :)
http://thirdpilot.net/projects/civsmith/documentation


Next version
I am currently working on the next version, v0.4. It's another beta but will include documentation in PDF form. Apart from the basic information about features, the documentation will also include a PDF with programming examples that will teach you how to program for CivSmith with Python. New features are sprinkled everywhere and the two editors will be meatier with more functions and more fields for editing. Currently there is no deadline for this version, but I'm thinking that it will be finished around June or July.


Thanks for reading and I hope you will provide feedback if you decide to try it out. I have worked for two months to get this far and I appreciate feedback! Just don't forget to backup your files before trying this software! It's a preview, which means that it is basically in a beta stage and should not be used unless you have backups.
 
Hey this looks awesome...the thing I started writing is similar but nowhere near as good :)

Does it support modular mods currently? - and if you don't mind asking how did you implement it?

I think it is a great idea with the modular nature of the program allowing anyone to create plug ins so if you get bored as people do :D - any ETA when the docs showing us how to do this will be ready / when the program is ready? As I would probably love to give one a go :)

(I dont know any python right now :P)

Edit - so I downloaded python 25 and installed, then DL'd the other XML thing but no matter what I did it would only find my python 24 install folder to pick during the install. then that had a few errors, now when I run the program I get:

exceptions.importerror: no module named lxml.

Then the program opens without the menus / buttons etc :(

Anyone got any idea how I can install Lxml (As I guess thats whats at fault?) without removing python 24 as that is used for some other apps. (I do GIS work if anyone knows what that is :P?) On Vista 64.

Thanks
 
Hey this looks awesome...the thing I started writing is similar but nowhere near as good :)

Thanks. It's a funny coincidence that we both released similar tools on the same day :lol:. I'll take a look at your software later, as I have to go to work soon :cry:

Does it support modular mods currently? - and if you don't mind asking how did you implement it?

No sorry, it only reads "normal" mods. I was thinking about this myself and could be added in the future.

I think it is a great idea with the modular nature of the program allowing anyone to create plug ins so if you get bored as people do :D - any ETA when the docs showing us how to do this will be ready / when the program is ready? As I would probably love to give one a go :)

I'll start writing some tutorials tomorrow. It's not really hard to do a simple plugin without an interface (only code). Making some windows and buttons take a little more time since it has to be done through code, but is very flexible once one gets the hang of it.

I dont know any python right now :P)

No worries! I knew next to no Python before starting this project, yet it is based on it! :blush: It's easy to learn, and fast to write with a good IDE.


Edit - so I downloaded python 25 and installed, then DL'd the other XML thing but no matter what I did it would only find my python 24 install folder to pick during the install. then that had a few errors, now when I run the program I get:

Did you download lxml for Python 2.4? Make sure that you have this file: lxml-2.2.win32-py2.5.exe which is for Python 2.5. Other than that, I can't think of why it wouldn't find Python 2.5. Perhaps try re-installing Python 2.5 if all else fails.

exceptions.importerror: no module named lxml.

Then the program opens without the menus / buttons etc :(

Yeah, it won't run without the necessary libraries, or in this case lxml.
 
yeah i DL'd and installed 2.5 sucessfully. It lets me in fine now but when I try open mod I get this text in the text bit:

Traceback (most recent call last):
File "<string>", line 290, in <module>
File "<string>", line 199, in menuOpenMod
File "<string>", line 24, in __init__
AttributeError: A component named frmOpenMod already exists

I havent set up the base paths just yet would that be it?
 
You probably have a file called config.xml in the CivSmith folder, this file stores all the base paths, so I suggest that you delete that file and try restarting the application again, it will automatically search for your base paths and create the file again. I suspect this error occurs since CivSmith crashed on you before when lxml wasn't installed.

The error you got is that the Open Mod window (frmOpenMod) wasn't successfully freed from memory the last time you ran it. Please reply if you get any other errors or weird behaviour :)
 
Hey Senap,

This is a really neat (and beautiful) editor. I had some initial problems due to $#*&Vista really requiring this program to be run in Administrator mode, but now it's all cool. What are your plans for developing specific plugins? For instance, I could actually use the unit plugin if it had a few more entries, and looking at the Python it seems reasonably straightforward, but I don't want to duplicate effort.
 
I'm glad you got it working and thanks. I've been a little nervous as it requires a bunch of settings and libraries (I should've mentioned that it needs admin rights in Vista :().

Yes, it's fairly straightforward, so what I'll be doing until the next update (sunday 10th) is adding more fields to the unit and tech editor, hopefully it will be a little more useful by then :)

I haven't yet thought about other plugins beyond the Unit- and Tech Editor, but I want to make editors for all the pre-defined values that already appear in the editors (Unit Class, Era, Domain) and so on. So I think I'll be focusing on those things, to have more complete editors overall.
 
Fixed...if running in Vista it must be run as admin as said above. And if you have an error:

Traceback (most recent call last):
File "<string>", line 290, in <module>
File "<string>", line 199, in menuOpenMod
File "<string>", line 24, in __init__
AttributeError: A component named frmOpenMod already exists

Then exit the program, delete Config.xml and restart as an Admin.
 
this looks awsome ! However I have python 2.6 installed and your programms checks for a Python25.dll :-/ is there any chance, that your programm might run with 2.6 too ? I guess, almost everything, which was created with Python 2.5 runs also with 2.6

EDIT 1
I've installed Python 2.5 parallel. seems to work ;)

EDIT 2
Fixed...if running in Vista it must be run as admin as said above. And if you have an error:

Traceback (most recent call last):
File "<string>", line 290, in <module>
File "<string>", line 199, in menuOpenMod
File "<string>", line 24, in __init__
AttributeError: A component named frmOpenMod already exists

Then exit the program, delete Config.xml and restart as an Admin.

Well, I'm an Administrator User on Vista and I still get this error :sad:
 
Are you sure you have installed the library lxml for Python 2.5 as well? Once you have done that, "start fresh" by shutting down the application and deleting the auto-generated file config.xml.

If you still get the error, try opening config.xml and check that the paths are correct for Civ, War, and BtS. If the application can't locate your games, can you tell me which version of Civ you're running (retail, download, complete, gold, etc). I hope this works :)

This problem drives me crazy as I can't reproduce the problem, no matter what I do :cry:
 
Updated CivSmith to version 0.3.1.
It's a stand-alone version so you can overwrite your old files to get the new version with your previous settings.

It's mostly the Unit Editor that has been getting some attention this time around, it has more fields for editing units.

From now on, I think I will be focusing on the planned auto-update features. This way, I'll be able to update the plugins more frequently and release new plugins without having to release the entire application.

Please report any problems (and/or suggestions) that you might have.
 
First up, this is certainly the best attempt at a Civ editor I have seen to date, so kudos on that one. I have a lot of comments, not all of which I can fit here, or that I fully remember. However, having messed around, here are a few thoughts on the tech editor:
  • The "click one then another to swap" is great, but I didn't know it was doing it the first couple of times. As your "I like diving in without reading things thoroughly" that coders have trouble with, you can see how it happened. ;) I would suggest that the tips are presented more clearly, as I see you mention it in there, or that the behaviour was edited slightly. Maybe right-click to switch, and left click to just select the new one?
    [Related to this, where exactly are the hovers? I found the XML files with the text, but never saw them while editing. I'm probably being blind.]
  • The same thing as I talked about above applies with the and/or part of the plug-in. Not knowing what I was doing, I made several techs "OR" reqs before realising, although I must say that I do like the cyclic method. Nice touch.
  • Just aesthetically, I would like to see the TECH's replaced with the relevant text tags on the tree if at all possible. I'm being a bit picky, though. A really nice thing if that isn't possible would be to just remove the TECH_ prefix, as was done in the other new editor. Not perfect, but a reasonable compromise. This also applies to all the other items in the editor.
And the unit editor:
  • I would like to see the variable modifier button-thingies (the ^ and reverse) for the integer values, as seen in Cival's editor. That would be neat.
  • Another thing which sticks out as missing that Cival's has is the ability to add more than one value for those fields that allow it, such as prerequisites, see invisibles and the like. That's one of the few things I would need to dig into raw files for as it is.
  • Other than a few things like some switches not having info and the previously mentioned UNIT_ thing, the only other thing I would love to see, but I would not recommend spending a lot of time on doing it if you'd rather build other plugins, is a unit upgrade tree. This can be done dynamically, thanks to a pre-written algorithm (handily in Python). It's in a lot of interface mods, so if you'd like me to dig it up, feel free to ask.
If you're really committed to making this an all-in-one editor, could I suggest including a map editor of some kind, either using your plug-ins, or just bundling it with a pre-existing one. As there are quite a few (non-Python), I would recommend the latter, for now at least.

I'd also love to give making my own plug-in a try, but there is a distinct lack of documentation on that front, which is completely understandable given that this is so new. I did have a quick mess around, but I didn't really understand what I was doing.

Again, I'm picking away at very tiny holes here. This really is superb for such an early release, and I commend you on your work thus far: it is intuitive, aesthetically pleasing and very polished. If you ever want some little graphics (small icons are my speciality, as it happens), then just drop me a message. Otherwise, I hope my comments have been helpful, and I'll try and make some more if you'd like.
 
NikNaks, thanks so much for your feedback. I really appreciate it.

First up, this is certainly the best attempt at a Civ editor I have seen to date, so kudos on that one.

Thank you!

The "click one then another to swap" is great, but I didn't know it was doing it the first couple of times. As your "I like diving in without reading things thoroughly" that coders have trouble with, you can see how it happened. ;) I would suggest that the tips are presented more clearly, as I see you mention it in there, or that the behaviour was edited slightly. Maybe right-click to switch, and left click to just select the new one?

I had a balloon tip for the tech grid when I was writing it but I found that it was getting in the way all the time and it annoyed me. I thought, "if it annoys me, a pretty tolerant guy, it will probbly drive other people crazy" :lol:. I have no clue how to provide instructions for the tech grid without cluttering the interface.

The intention is of course to supply a PDF file with proper instructions on how to use the application, along with complete instructions for the tech grid. I am not even sure if I have documented this on my site but you can unselect a selected tech with the Escape key.

Unfortunately I can't do any advanced mouse detection on the grid due to the Delphi-To-Python library I am using. It is awesome but has its limitations and allows me to only detect left clicks. It's a compromise I will have to live with. :sad:

[Related to this, where exactly are the hovers? I found the XML files with the text, but never saw them while editing. I'm probably being blind.]

Now I just haven't had the time to write balloon tips for all the controls and the system isn't even 100% finished. Some of the Unit Editor controls support balloon tips (try the first few checkboxes).

The same thing as I talked about above applies with the and/or part of the plug-in. Not knowing what I was doing, I made several techs "OR" reqs before realising, although I must say that I do like the cyclic method. Nice touch.

The cycle thing is, as mentioned above, a compromise due to the lack of advanced mouse detection :lol:

I know the AND/OR mode is confusing and will be in the PDF documentation.

Just aesthetically, I would like to see the TECH's replaced with the relevant text tags on the tree if at all possible. I'm being a bit picky, though. A really nice thing if that isn't possible would be to just remove the TECH_ prefix, as was done in the other new editor. Not perfect, but a reasonable compromise. This also applies to all the other items in the editor.

Yes, this will be included at some point. I actually got it working once. CivSmith successfully searched for the proper text but the implementation was incredible slow and unacceptable. I figured out a possible solution for this and will hopefully be included in the next version.

I didn't think anyone would be bothered by the TECH_ part so I haven't included the option to strip it. But yes, of course, I can add this :)

I would like to see the variable modifier button-thingies (the ^ and reverse) for the integer values, as seen in Cival's editor. That would be neat.

You lost me here. I have no clue what you're talking about but I'll check out Cival's editor.

Oh wait, are you talking about up and down arrows? I suppose I could add this, I am really tired as I'm writing this :)

EDIT: On second thought, I'd prefer to not have up/down arrows but instead add the ability simulate up/down arrows by using the up and down arrow keys on the keyboard while Page Up and Page Down could be used for larger increments.

Another thing which sticks out as missing that Cival's has is the ability to add more than one value for those fields that allow it, such as prerequisites, see invisibles and the like. That's one of the few things I would need to dig into raw files for as it is.

Yes it's coming :) I just haven't gotten into it yet as I am experimenting with other methods for adding multiple values for prerequisites etc (the AND/OR mode of the Tech Grid is such an example). If I don't come up with anything spectacular, I'll have to go with standard boxes like in Cival's editor :sad:

the only other thing I would love to see, but I would not recommend spending a lot of time on doing it if you'd rather build other plugins, is a unit upgrade tree. This can be done dynamically, thanks to a pre-written algorithm (handily in Python). It's in a lot of interface mods, so if you'd like me to dig it up, feel free to ask.

Please, could you provide more info on this? I haven't looked at the unit upgrades yet as it looks like a scary block of XML :lol:.

If you're really committed to making this an all-in-one editor, could I suggest including a map editor of some kind, either using your plug-ins, or just bundling it with a pre-existing one. As there are quite a few (non-Python), I would recommend the latter, for now at least.

You know, I was thinking about making a map editor as well. (Un)fortunately, Gr3yhound's MapView is really good and it feels like I would be wasting my time. Still, I will probably make a simple WBS editor as I'm interested in map making as well but I can't guarantee that it will be great with tons of features.

We'll see what happens here. Another approach could be to write an entirely new stand-alone application for this (with only some Python support) using a game engine. Could turn out to be powerful, but would at the same time defeat the purpose of CivSmith's plugin concept.

I'd also love to give making my own plug-in a try, but there is a distinct lack of documentation on that front, which is completely understandable given that this is so new. I did have a quick mess around, but I didn't really understand what I was doing.

Please hold on to that thought! I was going to write some documentation for the previous version but real life hindered me from doing so. The creation of custom plugins is something that I will focus on for the next version (since the application relies on them). I am already writing examples (documentation and source code) that will be available later on.

Again, I'm picking away at very tiny holes here. This really is superb for such an early release, and I commend you on your work thus far: it is intuitive, aesthetically pleasing and very polished.

Thanks again!!

If you ever want some little graphics (small icons are my speciality, as it happens), then just drop me a message. Otherwise, I hope my comments have been helpful, and I'll try and make some more if you'd like.

I'll keep that in mind. Good button graphics are hard to come by.
 
NikNaks, thanks so much for your feedback. I really appreciate it.

Thank you!
No problem :)
I had a balloon tip for the tech grid when I was writing it but I found that it was getting in the way all the time and it annoyed me. I thought, "if it annoys me, a pretty tolerant guy, it will probbly drive other people crazy" :lol:. I have no clue how to provide instructions for the tech grid without cluttering the interface.
Maybe just a little question mark alongside the other icons would suffice for now.
The intention is of course to supply a PDF file with proper instructions on how to use the application, along with complete instructions for the tech grid. I am not even sure if I have documented this on my site but you can unselect a selected tech with the Escape key.
Yes, I did read that, but only after some digging ;) A PDF would be great!
Unfortunately I can't do any advanced mouse detection on the grid due to the Delphi-To-Python library I am using. It is awesome but has its limitations and allows me to only detect left clicks. It's a compromise I will have to live with. :sad:
:sad:
Now I just haven't had the time to write balloon tips for all the controls and the system isn't even 100% finished. Some of the Unit Editor controls support balloon tips (try the first few checkboxes).
Do you want a hand with that? If it's something that I can do in a few short minutes throughout a day, I might be able to handle that for you.
The cycle thing is, as mentioned above, a compromise due to the lack of advanced mouse detection :lol:
I still like it ;)
I know the AND/OR mode is confusing and will be in the PDF documentation.
Sweet :goodjob:
Yes, this will be included at some point. I actually got it working once. CivSmith successfully searched for the proper text but the implementation was incredible slow and unacceptable. I figured out a possible solution for this and will hopefully be included in the next version.

I didn't think anyone would be bothered by the TECH_ part so I haven't included the option to strip it. But yes, of course, I can add this :)
Cool. No rush with this, though. I'd much rather see you experiment with some other editor plugins first :mischief:
You lost me here. I have no clue what you're talking about but I'll check out Cival's editor.

Oh wait, are you talking about up and down arrows? I suppose I could add this, I am really tired as I'm writing this :)

EDIT: On second thought, I'd prefer to not have up/down arrows but instead add the ability simulate up/down arrows by using the up and down arrow keys on the keyboard while Page Up and Page Down could be used for larger increments.
Yes, that is what I meant. I still think a small arrow by each one would be a nice touch, but you're the designer here, so I'll let you make the decisions.
Yes it's coming :) I just haven't gotten into it yet as I am experimenting with other methods for adding multiple values for prerequisites etc (the AND/OR mode of the Tech Grid is such an example). If I don't come up with anything spectacular, I'll have to go with standard boxes like in Cival's editor :sad:
Well, it's not so bad. If you want to run some ideas past me in beta format, then I'll be happy to play with them and pass on my thoughts.
Please, could you provide more info on this? I haven't looked at the unit upgrades yet as it looks like a scary block of XML :lol:.
Absolutely. I'll block-paste the entire code. I'm sure some of it will be relevant to actually drawing the screen, or even to pull unit buttons out. Here goes:
Spoiler :
Code:
## Sid Meier's Civilization 4
## 
## This file is part of the UnitUpgradesPediaMod by Vovan
## Automatic layout algorithm by Progor
##

import string

from CvPythonExtensions import *

import CvUtil

# globals
gc = CyGlobalContext()
ArtFileMgr = CyArtFileMgr()

#The exception list allow you to completely hide a unit from the upgrade graph.
#This is useful for mods that use an unreachable unit to keep others from expiring.
#unitExceptionList = [UNIT_UNREACHABLE]
unitExceptionList = []

#The split lists let you split a particular unit into several different graphs, so it doesn't hold several
#upgrade paths together that would look better separate.  For instance, you might have a Ranger unit that
#upgrades from both a Scout and Warrior unit, but otherwise Scouting and Fighting units are in their own
#trees.  In this case, you would place the Ranger in the SplitIncoming list, to get the program to split up
#the incoming upgrades to Ranger so that Ranger shows up in both trees without holding them together.
#unitSplitIncoming = [UNIT_RANGER, UNIT_STYGIAN_GUARD]
unitSplitIncoming = []

#SplitOutgoing is similar, but for a unit that holds together two trees by what it upgrades to.  For instance,
#you might have a commoner unit that upgrades to Scout, Warrior, and Worker, while otherwise those units have
#their own trees.  Using the SplitOutgoing list will just put a commoner at the start of each of the 3 trees
#unitSplitOutgoing = [UNIT_DWARVEN_SOLDIER]
unitSplitOutgoing = []

#Default is 64, but if your graph is too large to fit in the Pedia window, specify a smaller button size here.
unitButtonSize = 64

#Margin refers to space from the edge and space between graphs
unitHorizontalMargin = 20
unitVerticalMargin = 20

#Spacing refers to the space between unit icons
unitHorizontalSpacing = 40
unitVerticalSpacing = 5

#The same concepts apply to the promotions graphs as to the unit graphs
promotionsExceptionList = []
promotionsSplitIncoming = []
promotionsSplitOutgoing = []
promotionsButtonSize = 64
promotionsHorizontalMargin = 20
promotionsVerticalMargin = 20
#promotionsHorizontalSpacing = 50
#promotionsVerticalSpacing = 5
promotionsHorizontalSpacing = 75
promotionsVerticalSpacing = 5


################################### BEGIN CLASS DEFINITIONS ###########################################
#Don't change below unless you know what you're doing

class Node:
	"This node holds all necessary information for a single unit"
	
	def __init__(self):
		self.x = 999
		self.y = 999
		self.upgradesTo = set()
		self.upgradesFrom = set()
		self.seen = False
		
	def __repr__(self):
		return "Node<x: %i, y: %i, to: %s, from: %s, seen: %s>"%(self.x, self.y, self.upgradesTo, self.upgradesFrom, self.seen)

class MGraph:
	"This Graph is a collection of unit Node's with multiple access methods for fast topological sorting"
	
	def __init__(self):
		self.graph = {}
		self.matrix = []
		self.depth = 0
		self.width = 0
	
	def __repr__(self):
		return "MGraph<depth: %i, width: %i, graph: %s, matrix: %s>"%(self.depth, self.width, self.graph, self.matrix)

## NOTE: PromotionsGraph subclass located at bottom of file.
class UnitUpgradesGraph:
	"The graph of unit upgrades"

	# Rearranged so that methods that need to be overridden by promotions graph are placed first.
	
	def __init__(self, pediaScreen):
		self.mGraphs = []
		self.horizontalMargin = unitHorizontalMargin
		self.verticalMargin = unitVerticalMargin
		self.horizontalSpacing = unitHorizontalSpacing
		self.verticalSpacing = unitVerticalSpacing
		self.pediaScreen = pediaScreen
		self.upgradesList = pediaScreen.UPGRADES_GRAPH_ID
		self.buttonSize = unitButtonSize
		self.exceptionList = unitExceptionList
		self.splitIncoming = unitSplitIncoming
		self.splitOutgoing = unitSplitOutgoing

	def getNumberOfUnits(self):
		return gc.getNumUnitClassInfos()
		
	def getUnitNumber(self, k):
		"Gets the id for the kth unit for the current active player"
		if (self.getActivePlayer() == -1):
			result = gc.getUnitClassInfo(k).getDefaultUnitIndex()
		else:
			result = gc.getCivilizationInfo(gc.getGame().getActiveCivilizationType()).getCivilizationUnits(k)
		return result
	
	def getUnitType(self, e):
		"Returns the type of the units with the specified id"
		return gc.getUnitInfo(e).getType()

	def getGraphEdges(self, graph):
		for unitA in graph.iterkeys():
			for numB in range(gc.getNumUnitClassInfos()):
				unitB = self.getUnitNumber(numB)
				if gc.getUnitInfo(unitA).getUpgradeUnitClass(numB):
					self.addUpgradePath(graph, unitA, unitB)

	def placeOnScreen(self, screen, unit, xPos, yPos):
		screen.setImageButtonAt(self.pediaScreen.getNextWidgetName(), self.upgradesList, gc.getUnitInfo(unit).getButton(), xPos, yPos, self.buttonSize, self.buttonSize, WidgetTypes.WIDGET_PEDIA_JUMP_TO_UNIT, unit, 1)
			
	def unitToString(self, unit):
		return gc.getUnitInfo(unit).getDescription() + ":%d"%(unit, )
	
	################## Stuff to generate Unit Upgrade Graph ##################

	def addUpgradePath(self, graph, unitFrom, unitTo):

		# Check if unit numbers are valid
		if (unitFrom >= 0 and graph.has_key(unitFrom) and unitTo >= 0 and graph.has_key(unitTo)):
			graph[unitFrom].upgradesTo.add(unitTo)
			graph[unitTo].upgradesFrom.add(unitFrom)
			CvUtil.pyPrint(self.unitToString(unitFrom) + " upgrades to " + self.unitToString(unitTo) + ".")			

	def getActivePlayer(self):
		"Gets the id of the active player for UU upgrades"
		return gc.getGame().getActivePlayer()

	def getMedianY(self, mGraph, unitSet):
		"Returns the average Y position of the units in unitSet"
		
		if (len(unitSet) == 0):
			return -1
		sum = 0.0
		num = 0.0
		for unit in unitSet:
			sum += mGraph.graph[unit].y
			num += 1
		return sum/num
	
	def swap(self, mGraph, x, yA, yB):
		"Swaps two elements in a given row"
	
		unitA = mGraph.matrix[x][yA]
		unitB = mGraph.matrix[x][yB]
		if (unitA != "E"):
			mGraph.graph[unitA].y = yB
		if (unitB != "E"):
			mGraph.graph[unitB].y = yA
		mGraph.matrix[x][yA] = unitB
		mGraph.matrix[x][yB] = unitA
		return

	def getGraph(self):
		"Goes through all the units and adds upgrade paths to the graph.  The MGraph data structure is complete by the end of this function."
		
		self.mGraphs.append(MGraph())
		graph = self.mGraphs[0].graph
		
		for k in range(self.getNumberOfUnits()):
			unit = self.getUnitNumber(k)
			if (unit == -1):
				continue
			if (self.getUnitType(unit) not in self.exceptionList):
				graph[unit] = Node()
		
		self.getGraphEdges(graph)
		#CvUtil.pyPrint("1:\n" + str(graph))
		
		#remove units that don't upgrade to or from anything
		for unit in graph.keys():
			if (len(graph[unit].upgradesTo) == 0 and len(graph[unit].upgradesFrom) == 0):
				del(graph[unit])
		#CvUtil.pyPrint("2:\n" + str(graph))
		
		#split the graph into several disconnected graphs, filling out the rest of the data structure as we go
		mGraphIndex = 0
		while (len(self.mGraphs) > mGraphIndex):
			mGraph = self.mGraphs[mGraphIndex]
			self.mGraphs.append(MGraph())
			newMGraph = self.mGraphs[mGraphIndex + 1]
			
			#Pick a "random" element and mark it as order 0, then make all its successors higher and predecessors lower
			#We can fix that to the range (0..depth) in a moment, and we've already marked everything that's connected
			unit = mGraph.graph.iterkeys().next()
			mGraph.graph[unit].x = 0
			map = {}
			map[0] = set([unit])
			for iterlimit in range(10):
				for level in range(min(map.keys()), max(map.keys())+1):
					for unit in map[level].copy():
						node = mGraph.graph[unit]
						if (node.x != level):
							continue
						if (self.getUnitType(unit) in self.splitOutgoing):
							continue
						for u in node.upgradesTo:
							if (not mGraph.graph.has_key(u)):
								for i in range(mGraphIndex - 1, -1, -1):
									if (self.mGraphs[i].graph.has_key(u)):
										mGraph.graph[u] = Node()
										mGraph.graph[u].upgradesFrom = self.mGraphs[i].graph[u].upgradesFrom.copy()
										mGraph.graph[u].upgradesTo = self.mGraphs[i].graph[u].upgradesTo.copy()
										break
							nodeB = mGraph.graph[u]
							nodeB.x = level + 1
							if (not map.has_key(nodeB.x)):
								map[nodeB.x] = set()
							map[nodeB.x].add(u)
				for level in range (max(map.keys()), min(map.keys()) - 1, -1):
					for unit in map[level].copy():
						node = mGraph.graph[unit]
						if (node.x != level):
							continue
						if (self.getUnitType(unit) in self.splitIncoming):
							continue
						for u in node.upgradesFrom:
							if (not mGraph.graph.has_key(u)):
								for i in range(mGraphIndex - 1, -1, -1):
									if (self.mGraphs[i].graph.has_key(u)):
										mGraph.graph[u] = Node()
										mGraph.graph[u].upgradesFrom = self.mGraphs[i].graph[u].upgradesFrom.copy()
										mGraph.graph[u].upgradesTo = self.mGraphs[i].graph[u].upgradesTo.copy()
										break
							nodeB = mGraph.graph[u]
							nodeB.x = level - 1
							if (not map.has_key(nodeB.x)):
								map[nodeB.x] = set()
							map[nodeB.x].add(u)
			highOrder = max(map.keys())
			lowOrder = min(map.keys())
			map = 0
			mGraph.depth = highOrder - lowOrder + 1
						
			#Now we can move anything that isn't marked with an order to the next MGraph
			#if there's nothing to move, we're done after this iteration
			for (unit, node) in mGraph.graph.items():
				if (node.x == 999):
					newMGraph.graph[unit] = node
					del(mGraph.graph[unit])
				else:
					node.x -= lowOrder
					
			if (len(newMGraph.graph) == 0):
				del(self.mGraphs[mGraphIndex + 1])

			mGraphIndex += 1
		
		#CvUtil.pyPrint("3:\n" + str(self.mGraphs))
			
		for mGraph in self.mGraphs:
			#remove links that would otherwise have to jump 
			for (unit, node) in mGraph.graph.iteritems():
				for u in node.upgradesTo.copy():
					if (not mGraph.graph.has_key(u)):
						node.upgradesTo.remove(u)
				for u in node.upgradesFrom.copy():
					if (not mGraph.graph.has_key(u)):
						node.upgradesFrom.remove(u)
			

			nextDummy = -1
			#For any upgrade path that crosses more than one level, insert dummy nodes in between
			for (unitA, nodeA) in mGraph.graph.items():
				for unitB in nodeA.upgradesTo.copy():
					nodeB = mGraph.graph[unitB]
					if (nodeB.x - nodeA.x > 1):
						nodeA.upgradesTo.remove(unitB)
						nodeB.upgradesFrom.remove(unitA)
						n = nodeA.x + 1
						nodeA1 = nodeA # original node A
# Begin Promotions Graph fix for Warlords by Gaurav
						while (n < nodeB.x):
							nodeA.upgradesTo.add(nextDummy)
							mGraph.graph[nextDummy] = Node()
							nodeA = mGraph.graph[nextDummy]
							if n == nodeA1.x + 1:
								nodeA.upgradesFrom.add(unitA)
							else:
								nodeA.upgradesFrom.add(nextDummy + 1)
# End Promotions Graph fix for Warlords by Gaurav
							nodeA.x = n
							n += 1
							nextDummy -= 1
						nodeA.upgradesTo.add(unitB)
						nodeB.upgradesFrom.add(nextDummy + 1)
						

			#Now we can build the matrix from the order data
			#make sure the matrix is <depth> deep
			while(len(mGraph.matrix) < mGraph.depth):
				mGraph.matrix.append([])
			
			#fill out node.y and the matrix
			for (unit, node) in mGraph.graph.iteritems():
				node.y = len(mGraph.matrix[node.x])
				mGraph.matrix[node.x].append(unit)
				if (node.y >= mGraph.width):
					mGraph.width = node.y + 1

			#make all rows of the matrix the same width
			for row in mGraph.matrix:
				row.extend(["E"] * (mGraph.width - len(row)))
			
			#finally, do the Sugiyama algorithm: iteratively step through layer by layer, swapping
			#two units in layer i, if they cause fewer crosses or give a shorter line length from
			#layer i-1, then work back from the other end.  Repeat until no changes are made
			
			doneA = False
			iterlimit = 8
			while (not doneA and iterlimit > 0):
				doneA = True
				iterlimit -= 1
				for dir in [1, -1]:
					start = 1
					end = mGraph.depth
					if (dir == -1):
						start = mGraph.depth - 2
						end = -1
					for x in range(start, end, dir):
						doneB = False
						while (not doneB):
							doneB = True
							for y in range(mGraph.width - 1, 0, -1):
								medA = -1.0
								medB = -1.0
								unitA = mGraph.matrix[x][y-1]
								unitB = mGraph.matrix[x][y]
								nodeA = 0
								nodeB = 0
								setA = 0
								setB = 0
								if (unitA != "E"):
									nodeA = mGraph.graph[unitA]
									setA = nodeA.upgradesFrom
									if (dir == -1):
										setA = nodeA.upgradesTo
									medA = self.getMedianY(mGraph, setA)
								if (unitB != "E"):
									nodeB = mGraph.graph[unitB]
									setB = nodeB.upgradesFrom
									if (dir == -1):
										setB = nodeB.upgradesTo
									medB = self.getMedianY(mGraph, setB)

								if (medA < 0 and medB < 0):
									continue
								if (medA > -1 and medB > -1):
									if (medA > medB):
										self.swap(mGraph, x, y-1, y)
										doneB = False
								if (medA == -1 and medB < y):
									self.swap(mGraph, x, y-1, y)
									doneB = False
								if (medB == -1 and medA >= y):
									self.swap(mGraph, x, y-1, y)
									doneB = False
							if (doneB == False):
								doneA = False
						doneB = False
						while (not doneB):
							doneB = True
							for y in range(1, mGraph.width):
								unitA = mGraph.matrix[x][y-1]
								unitB = mGraph.matrix[x][y]
								if (unitA == "E" or unitB == "E"):
									continue
								nodeA = mGraph.graph[unitA]
								nodeB = mGraph.graph[unitB]
								setA = nodeA.upgradesFrom
								setB = nodeB.upgradesFrom
								if (dir == -1):
									setA = nodeA.upgradesTo
									setB = nodeB.upgradesTo
								crosses = 0
								crossesFlipped = 0
								for a in setA:
									yA = mGraph.graph[a].y
									for b in setB:
										yB = mGraph.graph[b].y
										if (yB < yA):
											crosses += 1
										elif (yB > yA):
											crossesFlipped += 1
								if (crossesFlipped < crosses):
									self.swap(mGraph, x, y-1, y)
									doneB = False
							if (doneB == False):
								doneA = False
						
						#this is a fix for median float->int conversions throwing off the list
						if (mGraph.matrix[x][-1] == "E"):
							sum = 0.0
							num = 0.0
							for y in range(mGraph.width - 1):
								unit = mGraph.matrix[x][y]
								if (unit != "E"):
									node = mGraph.graph[unit]
									seto = node.upgradesFrom
									if (dir == -1):
										seto = node.upgradesTo
									sum += y - self.getMedianY(mGraph, seto)
									num += 1
							if (num > 0 and sum / num < -0.5):
								for y in range(mGraph.width - 1, 0, -1):
									unit = mGraph.matrix[x][y-1]
									mGraph.matrix[x][y] = unit
									if (unit != "E"):
										mGraph.graph[unit].y = y
								mGraph.matrix[x][0] = "E"

		#CvUtil.pyPrint("4:\n" + str(self.mGraphs))
						
		#one final step: sort the graphs with the biggest one at top
		done = False
		while (done == False):
			done = True
			for i in range(1, len(self.mGraphs)):
				if (len(self.mGraphs[i-1].graph) < len(self.mGraphs[i].graph)):
					done = False
					temp = self.mGraphs[i]
					self.mGraphs[i] = self.mGraphs[i-1]
					self.mGraphs[i-1] = temp
		return
		
	################## Stuff to lay out the graph in space ###################

	def getPosition(self, x, y, verticalOffset):
		xPos = self.horizontalMargin + x * (self.buttonSize + self.horizontalSpacing)
		yPos = self.verticalMargin + y * (self.buttonSize + self.verticalSpacing) + verticalOffset
		return (xPos, yPos)
	
	def drawGraph(self):
		screen = self.pediaScreen.getScreen()
		offset = 0
		for mGraph in self.mGraphs:
			#draw arrows first so they'll go under the buttons if there is overlap
			self.drawGraphArrows(mGraph, offset)
			for x in range(mGraph.depth):
				for y in range (mGraph.width):
					unit = mGraph.matrix[x][y]
					(xPos, yPos) = self.getPosition(x, y, offset)
					if unit != "E" and unit > -1:
						self.placeOnScreen(screen, unit, xPos, yPos)
			offset = self.getPosition(0, mGraph.width, offset)[1]

	####################### Stuff to draw graph arrows #######################
	
	def drawGraphArrows(self, mGraph, offset):
		matrix = mGraph.matrix
		for x in range(len(matrix) - 1, -1, -1):
			for y in range(len(matrix[x])):
				unit = matrix[x][y]
				if unit != "E":
					self.drawUnitArrows(mGraph, offset, unit)
		return
	
	def drawUnitArrows(self, mGraph, offset, unit):
		toNode = mGraph.graph[unit]
		for fromUnit in toNode.upgradesFrom:
			fromNode = mGraph.graph[fromUnit]
			posFrom = self.getPosition(fromNode.x, fromNode.y, offset)
			posTo = self.getPosition(toNode.x, toNode.y, offset)
			self.drawArrow(posFrom, posTo, fromUnit < 0, unit < 0)
		return
	
	def drawArrow(self, posFrom, posTo, dummyFrom, dummyTo):
		screen = self.pediaScreen.getScreen()
		
		LINE_ARROW = ArtFileMgr.getInterfaceArtInfo("LINE_ARROW").getPath()
		LINE_TLBR = ArtFileMgr.getInterfaceArtInfo("LINE_TLBR").getPath()
		LINE_BLTR = ArtFileMgr.getInterfaceArtInfo("LINE_BLTR").getPath()
		LINE_STRAIT = ArtFileMgr.getInterfaceArtInfo("LINE_STRAIT").getPath()
		
		if (dummyFrom):
			xFrom = posFrom[0] + self.buttonSize / 2
		else:
			xFrom = posFrom[0] + self.buttonSize
		if (dummyTo):
			xTo = posTo[0] + self.buttonSize / 2
		else:
			xTo = posTo[0] - 8
		yFrom = posFrom[1] + (self.buttonSize / 2)
		yTo = posTo[1] + (self.buttonSize / 2)
		
		if (yFrom == yTo):
			screen.addDDSGFCAt( self.pediaScreen.getNextWidgetName(), self.upgradesList, LINE_STRAIT, xFrom, yFrom - 3, xTo - xFrom, 8, WidgetTypes.WIDGET_GENERAL, -1, -1, False )
		else:
			xDiff = float(xTo - xFrom)
			yDiff = float(yTo - yFrom)

			iterations = int(max(xDiff, abs(yDiff)) / 80) + 1
			if (abs(xDiff/yDiff) >= 2 or abs(xDiff/yDiff) < 0.5):
				iterations = int(max(xDiff, abs(yDiff)) / 160) + 1

			line = LINE_TLBR
			if (yDiff < 0):
				line = LINE_BLTR
			for i in range(iterations):
				xF = int((xDiff / iterations) * max(i-0.1, 0)) + xFrom
				yF = int((yDiff / iterations) * max(i-0.1, 0)) + yFrom
				xT = int((xDiff / iterations) * (i + 1)) + xFrom
				yT = int((yDiff / iterations) * (i + 1)) + yFrom
				if (yT < yF):
					temp = yT
					yT = yF
					yF = temp
				screen.addDDSGFCAt(self.pediaScreen.getNextWidgetName(), self.upgradesList, line, xF, yF, xT-xF, yT-yF, WidgetTypes.WIDGET_GENERAL, -1, -1, False)
		
		if (dummyTo == False):
			screen.addDDSGFCAt( self.pediaScreen.getNextWidgetName(), self.upgradesList, LINE_ARROW, xTo, yTo - 6, 12, 12, WidgetTypes.WIDGET_GENERAL, -1, -1, False )
		return

########################### PROMOTION GRAPH IMPLEMENTATION #############################

#The promotion graph works exactly like the unit upgrade graph.  Just a few functions need to
#be changed to make it access promotions instead of unit infos.  These functions do that.
#This is a subclass inherited directly from UnitUpgradesGraph, so all the functions that are
#not listed here are above.

class PromotionsGraph(UnitUpgradesGraph):

	def __init__(self, pediaScreen):
		self.mGraphs = []
		self.horizontalMargin = promotionsHorizontalMargin
		self.verticalMargin = promotionsVerticalMargin
		self.horizontalSpacing = promotionsHorizontalSpacing
		self.verticalSpacing = promotionsVerticalSpacing
		self.pediaScreen = pediaScreen
		self.upgradesList = pediaScreen.UPGRADES_GRAPH_ID
		self.buttonSize = promotionsButtonSize
		self.exceptionList = promotionsExceptionList
		self.splitIncoming = promotionsSplitIncoming
		self.splitOutgoing = promotionsSplitOutgoing

	def getNumberOfUnits(self):
		return gc.getNumPromotionInfos()

	def getUnitNumber(self, k):
		"Gets the id for the kth unit for the current active player"
		
		# No unique promotions, so very simple
		return k

	def getUnitType(self, e):
		"Returns the type of the units with the specified id"
		return gc.getPromotionInfo(e).getType()

	def getGraphEdges(self, graph):
		for unitA in graph.iterkeys():
			unitD = gc.getPromotionInfo(unitA).getPrereqPromotion()
			self.addUpgradePath(graph, unitD, unitA)
			unitB = gc.getPromotionInfo(unitA).getPrereqOrPromotion1()
			self.addUpgradePath(graph, unitB, unitA)
			unitC = gc.getPromotionInfo(unitA).getPrereqOrPromotion2()
			self.addUpgradePath(graph, unitC, unitA)
		
	def unitToString(self, unit):
		return gc.getPromotionInfo(unit).getDescription() + ":%d"%(unit, )


	def placeOnScreen(self, screen, unit, xPos, yPos):
		screen.setImageButtonAt(self.pediaScreen.getNextWidgetName(), self.upgradesList, gc.getPromotionInfo(unit).getButton(), xPos, yPos, self.buttonSize, self.buttonSize, WidgetTypes.WIDGET_PEDIA_JUMP_TO_PROMOTION, unit, 1)
You know, I was thinking about making a map editor as well. (Un)fortunately, Gr3yhound's MapView is really good and it feels like I would be wasting my time. Still, I will probably make a simple WBS editor as I'm interested in map making as well but I can't guarantee that it will be great with tons of features.

We'll see what happens here. Another approach could be to write an entirely new stand-alone application for this (with only some Python support) using a game engine. Could turn out to be powerful, but would at the same time defeat the purpose of CivSmith's plugin concept.
I agree that it would be a little foolish to create one now, especially with the quality we already have.
Please hold on to that thought! I was going to write some documentation for the previous version but real life hindered me from doing so. The creation of custom plugins is something that I will focus on for the next version (since the application relies on them). I am already writing examples (documentation and source code) that will be available later on.
Excellent! I'll keep my eyes peeled.
I'll keep that in mind. Good button graphics are hard to come by.
:D
 
i have a question: in the documentation on your website, it says to download Python 2.5.4. however, the latest version is 3.1. do we have to download 2.5.4, or are we able to use the latest version?

thanks, and i can't wait to get started using CivSmith!
 
i have a question: in the documentation on your website, it says to download Python 2.5.4. however, the latest version is 3.1. do we have to download 2.5.4, or are we able to use the latest version?

The application is designed to work with Python 2.5. I would support the latest version but the main library I am using supports Python up to 2.5 only. Unfortunately, I don't have the time to modify the source of that library, I'd rather spend the time on the actual tool. However, Python 2.5 can co-exist with other Python releases so this shouldn't be a problem :)
 
This looks like a great utility.

Unfortunately it doesn't work for me.

I have all of the proper components downloaded. I cleared the config file and when I start up the program it can't find the proper folders for the game. :sad:

They are stored in the usual /Program_Files directory - shouldn't be difficult to find.

Running on Windows Vista Home.
 
What version of Civ do you have (normal retail disc, download, civ complete, civ gold)?

Are you running it as admin? If you aren't, it might not be able to retrieve the path to Civ from the registry.

You could just open the config.xml file and put the correct path for each base (game and expansions).
 
Back
Top Bottom