Making Scenarios Independent of Mod's Maximum Civs

Temudjin

Chieftain
Joined
Oct 16, 2007
Messages
90
Recently I modded Thomas' War by tsentom1 to be able to play it with up to 40 civs. Unfortunately this change made using the scenarios impossible, as the scenario-file needs to account for every possible player and team. The player number for the Barbarians also changes, as it is always the maximum number of players. Any additional Barbarian factions are one more number higher.

The following code changes just one file 'CvWBInterface.py' to enable the use of any scenario regardless of the number of civs allowed in the mod.
Basically it adds or deletes the necessary empty teams and players, adjusts for the new Barbarian faction number(s) and changes the description to note the new civ maximum. Only empty players or teams are deleted, so any fully developed scenarios with too many players should be save from downward adjustment.

As it is, the changes are just written to the scenario-file and the only thing the user should notice are the log entries in the log-file (PythonDbg.log) and perhaps the changed description of the scenario.

New: Version: 15.Feb.2010
Now it is possible to load Civ4 or Warlords scenarios.

Shown here are all the changes made to 'CvWBInterface.py', as you can see they are all at the beginning of the file. The full file is attached.
Spoiler :

Code:
## Sid Meier's Civilization 4
## Copyright Firaxis Games 2005
from CvPythonExtensions import *
import CvWBPopups
import CvUtil
import CvWBDesc

# globals
WBDesc = CvWBDesc.CvWBDesc()
lastFileRead = None
gc = CyGlobalContext()

########## Temudjin START 15.Feb.2010
# CHANGELOG
# 15.Feb.2010
# - fix: allow Civ4 and Warlords scenario files
#   NOTE: Renaming them beforehand to .CivBeyondSwordWBSave files is recommended.
# 06.Feb.2010
# - fix: allow for missing team infos
# - fix: allow for missing description in game info
# - fix: allow for changes in player info sequence
# - minimize default player info
# 02.Feb.2010
# - fix: allow for multiple Barbarian factions (like 'Fall Further')
# 30.Jan.2010
# - initial release
##########
import os
BACKUP = False				# usually backups are a good idea

lastFileChecked = None
addTeam = [
 'BeginTeam\n',
 '\tTeamID=#\n',
 '\tContactWithTeam=#\n',
 '\tRevealMap=0\n',
 'EndTeam\n'
]
addPlayer = [
 'BeginPlayer\n',
 '\tTeam=#\n',
 '\tLeaderType=NONE\n',
 '\tCivType=NONE\n',
 'EndPlayer\n'
]
addDescription = [ '\tDescription=#\n' ]

def adjustMap( fileName ):
	print "--- adjustMap(fileName) --- %s" % (fileName)
	global lastFileChecked
	global addTeam, addPlayer
	maxTeams = gc.getMAX_CIV_TEAMS()
	maxPlayers = gc.getMAX_CIV_PLAYERS()

	# test if file already adjusted
	if fileName == lastFileChecked:
		print "# %r has already been checked" % (fileName)
		return True		# success

	# normalize filename
	fileName = os.path.normpath( fileName )
	filePath,ext = os.path.splitext( fileName )
	if len(ext) == 0: ext = getWBSaveExtension()
	if (not os.path.isfile(filePath+ext)):
		CvUtil.pyPrint("Error: file %s does not exist" %(filePath+ext,))
		return False	# failed

	# read file
	f = file(filePath+ext, "r")		# open text file
	mapFile = f.readlines()
	f.close()

	# check if adjustment is needed
	t = [ i for i in range(len(mapFile)) if mapFile[i].find('EndTeam')>-1 ]
	p = [ i for i in range(len(mapFile)) if mapFile[i].find('EndPlayer')>-1 ]
	if ( maxTeams == len(t) ) and ( maxPlayers == len(p) ):
		print "# Number of teams and players ok"
		lastFileChecked = fileName

	# adjust file - Scenario was designed for different maxPlayers
	else:
		sprint = ""
		sprint += "# Scenario %s was designed for\n" % (filePath)
		sprint += "# a maximum of %i players, but this Mod has a maximum of %i players.\n" % (len(p),maxPlayers)
		if BACKUP:
			sprint += "# Accordingly the original scenario file will be saved under a different name,\n"
			sprint += "# adjusted for %i players and then saved under the original name.\n" % (maxPlayers)
		else:
			sprint += "# Accordingly the scenario file will be adjusted for %i players.\n" % (maxPlayers)
		print sprint

		# backup original file
		if BACKUP:
			# get new file-name - fileName is actually set to lowercase most times for some reason
			# NOTE: The backups from Civ4 and Warlords will not be automatically included into the
			#       new list of BtS scenarios that might be choosen during the same session.
			#       >>> There will be Error-Messages about this <<<
			#       However this error can be savely ignored.
			if ext.lower() == '.civ4worldbuildersave':
				ext = '.Civ4WorldBuilderSave'
			elif ext.lower() == '.civwarlordswbsave':
				ext = '.CivWarlordsWBSave'
			elif ext.lower() == '.civbeyondswordwbsave':
				ext = '.CivBeyondSwordWBSave'

			oldFile = filePath + ( " [maxPlayers %2i]" % (len(p)) ) + ext
			print "# Backup original scenario file as: %s" %( oldFile )
			try:
				f = open( oldFile, "w" )
				f.writelines( mapFile )
				f.close()
			except:
				CvUtil.pyPrint("Error: Unable to backup file as: %r" %( oldFile, ))
				return False		# failed

		# find actual maxTeam
		for i in range( p[-2]+1, p[-1] ):
			tea = mapFile[i].find("Team=")
			if tea >= 0:
				actMaxTeam = int( mapFile[i][tea+5:] )
				break

		# delete/insert Players
		if len(p) > maxPlayers:
			for pl in range( len(p)-1, maxPlayers-1, -1 ):
				fnd = False
				for i in range( p[pl]-1, p[pl-1], -1 ):
					if mapFile[i].find('LeaderType=') >= 0:
						if mapFile[i].find('NONE') >= 0:
							fnd = True
							break
						else:
							CvUtil.pyPrint("Error: Unwilling to kill named leader in line %i, when adjusting to %i players" %( i+1, maxPlayers, ))
							return False		# failed
				if not fnd:
					CvUtil.pyPrint("Error: Unable to find 'LeaderType' when adjusting to %i players" %( maxPlayers, ))
					return False		# failed
			mapFile = mapFile[:(p[maxPlayers-1]+1)] + mapFile[(p[len(p)-1]+1):]
		else:
			for i in range( maxPlayers-1, len(p)-1, -1 ):
				if len(t) > 0:
					sCnt = "%i" % ( i - (len(t)-1-actMaxTeam) )
				else:
					sCnt = "%i" % ( i )
				ins = p[-1] + 1
				mapFile = mapFile[:ins] + addPlayer + mapFile[ins:]
				mapFile[ ins+1 ] = mapFile[ ins+1 ].replace("#", sCnt)

		# delete/insert Teams
		if len(t) > maxTeams:
			mapFile = mapFile[:(t[maxTeams-1]+1)] + mapFile[(t[len(t)-1]+1):]
		else:
			if len(t) > 0:
				ins = t[-1] + 1
			else:														# no teams have been specified
				for j in range( len(mapFile) ):
					if mapFile[j].find( "EndGame" ) >= 0:
						ins = j + 1
						break
			for i in range( maxTeams-1, len(t)-1, -1 ):
				sCnt = "%i" % (i)
				mapFile = mapFile[:ins] + addTeam + mapFile[ins:]
				mapFile[ ins+1 ] = mapFile[ ins+1 ].replace("#", sCnt)
				mapFile[ ins+2 ] = mapFile[ ins+2 ].replace("#", sCnt)

		# adjust Barbarians; maximum 5 factions
		for n in range( 5 ):
			ownerOld = "Owner=%2i" % ( len(p) + n )
			ownerNew = "Owner=%2i" % ( maxPlayers + n )
			for i in range( len(mapFile) ):
				mapFile[i] = mapFile[i].replace( ownerOld, ownerNew )

		# adjust Description
		sMax = "%2i" % (maxPlayers)
		iDescription = -1
		iEndGame = -1
		for i in range( len(mapFile) ):
			if mapFile[i].find( "Description=" ) > -1:
				iDescription = i
				break
		for i in range( len(mapFile) ):
			if mapFile[i].find( "EndGame" ) > -1:
				iEndGame = i
				break
		if (iDescription > -1) and (iDescription < iEndGame):
			# change original description
			sprint = "# Old: %r\n" % mapFile[iDescription]
			adj = mapFile[iDescription].find( "[Adjusted for " )
			if adj > -1:
				mapFile[iDescription] = mapFile[iDescription][:(adj+14)] + sMax + mapFile[iDescription][(adj+16):]
			elif mapFile[iDescription].find( "TXT_KEY_" ) == -1:
				e = mapFile[iDescription].rfind( "\n" )
				mapFile[iDescription] = mapFile[iDescription][:e] + " [Adjusted for " + sMax + " Civs]\n"
			sprint += "# New: %r\n" % mapFile[iDescription]
		else:
			# there is no original description
			sprint = "# Old: - No Description -\n"
			mapFile = mapFile[:iEndGame] + addDescription + mapFile[iEndGame:]
			mapFile[ iEndGame ] = mapFile[ iEndGame ].replace("#", "[Adjusted for " + sMax + " Civs]")
			sprint += "# New: %r\n" % mapFile[iEndGame]
		print sprint

		# save modified file
		print "# Save adjusted scenario file as: %s" %( filePath+ext )
		try:
			f = open( filePath+ext, "w" )
			f.writelines( mapFile )
			f.close()
		except:
			CvUtil.pyPrint( "Error: Unable to save new scenario-file: %r" % ( filePath+ext, ) )
			CvUtil.pyPrint( "-----> Printing scenario-file to log" )
			# print file to log
			sprint = ""
			sprint += "# UNSAVED SCENARIO FILE: %r\n" % (filePath+ext)
			sprint += "# ----------------------\n"
			cnt = 1
			for line in mapFile:
				sprint += "# %5i: %r\n" % (cnt, line)
				cnt += 1
			print sprint
			return False		# failed
	return True		# success
########## Temudjin END

def writeDesc(argsList):
	"Save out a high-level desc of the world, for WorldBuilder"
	fileName = argsList[0]
	global lastFileRead
	lastFileRead=None
	return WBDesc.write(fileName)

def readAndApplyDesc(argsList):
	"Read in and apply a high-level desc of the world.  In-game load only"
	fileName = argsList[0]

########## Temudjin START
	print "--- readAndApplyDesc(argsList) ---"
	if not adjustMap( fileName ): return -1
########## Temudjin END

	if WBDesc.read(fileName) < 0:
		return -1
	if (WBDesc.applyMap() < 0):
		return -1
	return WBDesc.applyInitialItems()

def readDesc(argsList):
	"Read in a high-level desc of the world, for WorldBuilder.  Must call applyMap and applyInitialItems to finish the process"
	global lastFileRead
	fileName = argsList[0]

########## Temudjin START
	print "--- readDesc(argsList) ---"
	if not adjustMap( fileName ): return -1
########## Temudjin END

	if (fileName!=lastFileRead):
		ret=WBDesc.read(fileName)
		if (ret==0):
			lastFileRead=fileName
	else:
		ret=0
	return ret
Note: Usually it's not a good idea to change original files, so if you want a backup, you have to change the line BACKUP = False to BACKUP = True
Note: If you activate the BACKUP option and load Civ4 or Warlords scenarios, you will get Error-Messages, because the scenario list is not updated. These Error-Messages can be ignored.

Changelog:
15.Feb.2010
- fix: allow Civ4 and Warlords scenario files
06.Feb.2010
- fix: allow for missing team infos
- fix: allow for missing description in game info
- fix: allow for changes in player info sequence
- minimize default player info
02.Feb.2010
- fix: allow for multiple Barbarian factions (like 'Fall Further')
30.Jan.2010
- initial release
 

Attachments

  • CvWBInterface.zip
    3.9 KB · Views: 63
This is really awesome and solves a big hurdle lots of 50 civ mods have. Great work!

PS. This should really be in the "Mod Comp" forum.
 
I just tried this out, and I get an error when opening a scenario that says "Failed to Read Worldbuilder File"

The python exception logs show this traceback:

Traceback (most recent call last):

File "CvWBInterface", line 209, in readDesc

File "CvWBInterface", line 99, in adjustMap

ValueError: invalid literal for int(): NONE

ERR: Python function readDesc failed, module CvWBInterface

Any idea on why it wouldn't be working?
 
Looks to me that something strange was in the file, at the place were I expected to be the team number for the last player.

Could you post the scenario file, so that I could go bug hunting ? :sniper:
 
Looks to me that something strange was in the file, at the place were I expected to be the team number for the last player.

Could you post the scenario file, so that I could go bug hunting ? :sniper:

Yeah, sure. It was just the BTS Europe Scenario.

I appreciate you looking at this. Hand-Fixing scenario's is a huge pain, so this would be a huge boon for me.

View attachment Europe.rar
 
Thanks for bringing the problem to my attention. :)
Apparently the syntax of .CivBeyondSwordWBSave files is a lot more flexible than I thought. It's pretty rare for me to play a scenario, so I probably wouldn't have found out on my own anytime soon.

The new corrected version is now attached to the first post.
 
Tried to incorporate this into RevDCM; no dice, get a CTD and some python exceptions when trying to load any of the default BtS scenarios. RevDCM doesn't remove any items that would cause a crash, in fact I've made default BtS scenarios work by setting the player and team numbers correctly; so not sure what is wrong. I'm attaching my logs for you to take a look at. Would be awesome if you got this to work. The RevDCM SVN can be found here if it helps with testing:

https://revolutiondcm.svn.sourceforge.net/svnroot/revolutiondcm/Trunk/RevolutionDCM/
 
I've seen the logs and realize that something went wrong, but I wasn't able to reproduce the error.
After installing RevolutionDCM 2.61 (from the thread not via SVN) and adding the 'CvWBInterface.py' file to ..\Python\EntryPoints, I was able to start the original 'Earth IceAge.CivBeyondSwordWBSave' scenario without a problem.

The file itself is somewhat strangely formed without 'TeamID=' tags, which might have defeated my first versions. Maybe you used an older version of CvWBInterface.py? The version from 6.Feb.2010 is the actual one.
 
You only have one download in the OP, and that's the one I used (downloaded it today). The current SVN makes some changes, the major thing is that it moves most of the Revolutions stuff into a BUG modular format, I can't see why that would break this component, but it has. Please take a look at this; I'd really like to incorporate this component into the RevDCM core, it's used by alot of large mods so it will get plenty of use if you can figure out what's wrong and get it to work. I did try to redownload the file and reaply it, same crash and python exceptions. If you don't want to bother with setting up the SVN, I've packaged the current version of RevDCM here for you to just download. Remember to create a RevolutionDCM folder in the BtS folder in your My Documents path (where saves and logs and stuff are stored), and cut out the User Settings folder and paste it in the new RevDCM folder in your MyDocuments/BtS directory you created; this is so users don't need to launch as an admin (allows the BUG mod to write and load to it's UserSettings ini files without admin privileges), and the component should be tested with this in effect:
 
I've tried your zip and still saw no problems. :confused:

What I did step by step (using XP):
- deleted old RevolutionDCM
- downloaded your new zip
- put the 'RevolutionDCM' folder into my '..Civilization 4\Beyond the Sword\Mods' folder
- put my 'CvWBInterface.py' file into the '..\Mods\RevolutionDCM\Python\EntryPoints' folder
- FIRST TRY: started RevolutionDCM and loaded original 'Earth IceAge.CivBeyondSwordWBSave' scenario --> conversion worked and scenario loaded
- created folder '..\My Documents\My Games\Beyond the Sword\RevolutionDCM'
- cut folder '..\Mods\RevolutionDCM\UserSettings' and pasted it into '..\My Documents\My Games\Beyond the Sword\RevolutionDCM'
- SECOND TRY: started RevolutionDCM and loaded original 'Earth IceAge.CivBeyondSwordWBSave' scenario --> conversion worked and scenario loaded, played a few turns; seems ok

:goodjob: Actually what you seem to have done is trying to load a Civ4 scenario instead of a BtS scenario.
 
:think: Thinking about it, I guess compatibility for old civ4 '.Civ4WorldBuilderSave' files should be next. Is there some interest ? :dubious:
 
Ah, you're right, I was trying to load a vanilla civ4 scenario. That's weird, I didn't realize that vanilla scenarios populated the BtS scenario list. This modcomp worked fine loading BtS scenarios. Anyway since vanilla civ scenarios are put on the list of scenarios, it would be nice if you could get them to load without crashing. I mainly ask because otherwise I know users will try to load these scenarios, get a crash, and blame the mod (even though this behavior is from default BtS); so it would be nice to fix this.
 
Allowing Civ4 and Warlords scenarios turned out not to be a problem :), but if BACKUP is activated, the new file will not be integrated into the sessions scenario list and some annoying Error-Messages are popping up :(. This is not the case for proper BtS scenarios with a '.CivBeyondSwordWBSave' extention :cool:.
I don't think there is much to be done about that, as I believe the Error-Messages are probably generated by the .exe (If not, can someone point me the way where BtS gathers the scenario list?).

:goodjob: You find the new version linked to the first post.
 
Top Bottom