1# -*-python-*-
2# GemRB - Infinity Engine Emulator
3# Copyright (C) 2003 The GemRB Project
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18#
19#
20# GUICommon.py - common functions for GUIScripts of all game types
21
22import GemRB
23import GameCheck
24import GUIClasses
25import collections
26import CommonTables
27from ie_restype import RES_CHU, RES_2DA, RES_BAM, RES_WAV
28from ie_spells import LS_MEMO
29from GUIDefines import *
30from ie_stats import *
31
32CommonTables.Load ()
33
34def GetWindowPack():
35	width = GemRB.GetSystemVariable (SV_WIDTH)
36	height = GemRB.GetSystemVariable (SV_HEIGHT)
37
38	if GemRB.GameType == "pst":
39		default = "GUIWORLD"
40	else:
41		default = "GUIW"
42
43	# use a custom gui if there is one
44	gui = "CGUI" + str(width)[:2] + str(height)[:2]
45	if GemRB.HasResource (gui, RES_CHU, 1):
46		return gui
47
48	# select this based on height
49	# we do this because:
50	# 1. windows are never the entire width,
51	#    but the can be the entire height
52	# 2. the originals were for 4:3 screens,
53	#    but modern screens are usually a wider aspect
54	# 3. not all games have all the window packs
55	if height >= 960 and GemRB.HasResource ("GUIW12", RES_CHU, 1):
56		return "GUIW12"
57	elif height >= 768 and GemRB.HasResource ("GUIW10", RES_CHU, 1):
58		return "GUIW10"
59	elif height >= 600 and GemRB.HasResource ("GUIW08", RES_CHU, 1):
60		return "GUIW08"
61
62	# fallback to the smallest resolution
63	return default
64
65def LocationPressed ():
66	AreaInfo = GemRB.GetAreaInfo()
67	TMessageTA = GemRB.GetView("MsgSys", 0)
68	if TMessageTA:
69		message = "[color=ff0000]Mouse:[/color] x={0}, y={1}\n[color=ff0000]Area:[/color] {2}\n"
70		message = message.format(AreaInfo["PositionX"], AreaInfo["PositionY"], AreaInfo["CurrentArea"])
71		TMessageTA.Append(message)
72	else:
73		print("%s [%d.%d]\n" % (AreaInfo["CurrentArea"], AreaInfo["PositionX"], AreaInfo["PositionY"]))
74
75	return
76
77def SelectFormation (btn, val):
78	GemRB.GameSetFormation (val)
79	return
80
81def OpenFloatMenuWindow (x, y):
82	if GameCheck.IsPST():
83		import FloatMenuWindow
84		FloatMenuWindow.OpenFloatMenuWindow(x, y)
85	else:
86		GemRB.GameControlSetTargetMode (TARGET_MODE_NONE)
87
88def GetActorPaperDoll (actor):
89	anim_id = GemRB.GetPlayerStat (actor, IE_ANIMATION_ID)
90	level = GemRB.GetPlayerStat (actor, IE_ARMOR_TYPE)
91	row = "0x%04X" %anim_id
92	which = "LEVEL%d" %(level+1)
93	doll = CommonTables.Pdolls.GetValue (row, which)
94	if doll == "*":
95		# guess a name
96		import GUICommonWindows
97		doll = GUICommonWindows.GetActorPaperDoll (actor) + "INV"
98		if not GemRB.HasResource (doll, RES_BAM):
99			print("GetActorPaperDoll: Missing paper doll for animation", row, which, doll)
100	return doll
101
102def SelectAllOnPress ():
103	GemRB.GameSelectPC (0, 1)
104
105def GearsClicked ():
106	#GemRB.SetPlayerStat(GemRB.GameGetFirstSelectedPC (),44,249990)
107	GemRB.GamePause (2, 0)
108
109def GetAbilityBonus (Actor, Stat):
110	Ability = GemRB.GetPlayerStat (Actor, Stat)
111	return Ability//2-5
112
113def SetColorStat (Actor, Stat, Value):
114	t = Value & 0xFF
115	t |= t << 8
116	t |= t << 16
117	GemRB.SetPlayerStat (Actor, Stat, t)
118	return
119
120def CheckStat100 (Actor, Stat, Diff):
121	mystat = GemRB.GetPlayerStat (Actor, Stat)
122	goal = GemRB.Roll (1,100, Diff)
123	if mystat>=goal:
124		return True
125	return False
126
127def CheckStat20 (Actor, Stat, Diff):
128	mystat = GemRB.GetPlayerStat (Actor, Stat)
129	goal = GemRB.Roll (1,20, Diff)
130	if mystat>=goal:
131		return True
132	return False
133
134def GetGUISpellButtonCount ():
135	if GameCheck.HasHOW() or GameCheck.IsBG2():
136		return 24
137	else:
138		return 20
139
140def SetGamedaysAndHourToken ():
141	currentTime = GemRB.GetGameTime()
142	days = currentTime // 7200
143	hours = (currentTime % 7200) // 300
144	GemRB.SetToken ('GAMEDAY', str (days))
145	GemRB.SetToken ('GAMEDAYS', str (days))
146	GemRB.SetToken ('HOUR', str (hours))
147
148def Gain(infostr, ability):
149	GemRB.SetToken ('SPECIALABILITYNAME', GemRB.GetString(int(ability) ) )
150	GemRB.DisplayString (infostr, ColorWhite) # FIXME: what color should this really be
151
152# chargen version of AddClassAbilities
153def ResolveClassAbilities (pc, ClassName):
154	# apply class/kit abilities
155	IsMulti = IsMultiClassed (pc, 1)
156	Levels = [GemRB.GetPlayerStat (pc, IE_LEVEL), GemRB.GetPlayerStat (pc, IE_LEVEL2), \
157			GemRB.GetPlayerStat (pc, IE_LEVEL3)]
158	KitIndex = GetKitIndex (pc)
159	if IsMulti[0]>1:
160		#get the class abilites for each class
161		for i in range (IsMulti[0]):
162			TmpClassName = GetClassRowName (IsMulti[i+1], "class")
163			ABTable = CommonTables.ClassSkills.GetValue (TmpClassName, "ABILITIES")
164			if ABTable != "*" and GemRB.HasResource (ABTable, RES_2DA, 1):
165				AddClassAbilities (pc, ABTable, Levels[i], Levels[i])
166	else:
167		if KitIndex:
168			ABTable = CommonTables.KitList.GetValue (str(KitIndex), "ABILITIES")
169		else:
170			ABTable = CommonTables.ClassSkills.GetValue (ClassName, "ABILITIES")
171		if ABTable != "*" and GemRB.HasResource (ABTable, RES_2DA, 1):
172			AddClassAbilities (pc, ABTable, Levels[0], Levels[0])
173
174# Adds class/kit abilities
175def AddClassAbilities (pc, table, Level=1, LevelDiff=1, align=-1):
176	TmpTable = GemRB.LoadTable (table)
177	import Spellbook
178
179	# gotta stay positive
180	if Level-LevelDiff < 0:
181		return
182
183	# we're doing alignment additions
184	if align == -1:
185		iMin = 0
186		iMax = TmpTable.GetRowCount ()
187	else:
188		# alignment is expected to be the row required
189		iMin = align
190		iMax = align+1
191
192	# make sure we don't go out too far
193	jMin = Level-LevelDiff
194	jMax = Level
195	if jMax > TmpTable.GetColumnCount ():
196		jMax = TmpTable.GetColumnCount ()
197
198	for i in range(iMin, iMax):
199		# apply each spell from each new class
200		for j in range (jMin, jMax):
201			ab = TmpTable.GetValue (i, j, GTV_STR)
202			if ab and ab != "****":
203				# seems all SPINs act like GA_*
204				if ab[:4] == "SPIN":
205					ab = "GA_" + ab
206
207				# apply spell (AP_) or gain spell (GA_)
208				if ab[:3] == "AP_":
209					GemRB.ApplySpell (pc, ab[3:])
210				elif ab[:3] == "GA_":
211					Spellbook.LearnSpell (pc, ab[3:], IE_SPELL_TYPE_INNATE, 0, 1, LS_MEMO)
212				elif ab[:3] == "FS_":
213					Gain(26320, ab[3:])
214				elif ab[:3] == "FA_":
215					Gain(10514, ab[3:])
216				else:
217					GemRB.Log (LOG_ERROR, "AddClassAbilities", "Unknown class ability (type): " + ab)
218
219def MakeSpellCount (pc, spell, count):
220	have = GemRB.CountSpells (pc, spell, 1)
221	if count<=have:
222		return
223	# only used for innates, which are all level 1
224	import Spellbook
225	Spellbook.LearnSpell (pc, spell, IE_IWD2_SPELL_INNATE, 0, count-have, LS_MEMO)
226	return
227
228# remove all class abilities up to the given level
229# for dual-classing mainly
230def RemoveClassAbilities (pc, table, Level):
231	TmpTable = GemRB.LoadTable (table)
232	import Spellbook
233
234	# gotta stay positive
235	if Level < 0:
236		return
237
238	# make sure we don't go out too far
239	jMax = Level
240	if jMax > TmpTable.GetColumnCount ():
241		jMax = TmpTable.GetColumnCount ()
242
243	for i in range(TmpTable.GetRowCount ()):
244		for j in range (jMax):
245			ab = TmpTable.GetValue (i, j, GTV_STR)
246			if ab and ab != "****":
247				# get the index
248				SpellIndex = Spellbook.HasSpell (pc, IE_SPELL_TYPE_INNATE, 0, ab[3:])
249
250				# seems all SPINs act like GA_*
251				if ab[:4] == "SPIN":
252					ab = "GA_" + ab
253
254				# apply spell (AP_) or gain spell (GA_)?
255				if ab[:3] == "AP_":
256					GemRB.RemoveEffects (pc, ab[3:])
257				elif ab[:3] == "GA_":
258					if SpellIndex >= 0:
259						# TODO: get the correct counts to avoid removing an innate ability
260						# given by more than one thing?
261						# RemoveSpell will unmemorize them all too
262						GemRB.RemoveSpell (pc, IE_SPELL_TYPE_INNATE, 0, SpellIndex)
263				elif ab[:3] != "FA_" and ab[:3] != "FS_":
264					GemRB.Log (LOG_ERROR, "RemoveClassAbilities", "Unknown class ability (type): " + ab)
265
266# PST uses a button, IWD2 two types, the rest are the same with two labels
267def SetEncumbranceLabels (Window, ControlID, Control2ID, pc):
268	"""Displays the encumbrance as a ratio of current to maximum."""
269
270	# encumbrance
271	encumbrance = GemRB.GetPlayerStat (pc, IE_ENCUMBRANCE)
272	max_encumb = GemRB.GetMaxEncumbrance (pc)
273
274	Control = Window.GetControl (ControlID)
275	if GameCheck.IsPST():
276		# FIXME: there should be a space before LB symbol (':') - but there is no frame for it and our doesn't cut it
277		Control.SetText (str (encumbrance) + ":\n\n" + str (max_encumb) + ":")
278	elif GameCheck.IsIWD2() and not Control2ID:
279		Control.SetText (str (encumbrance) + "/" + str(max_encumb) + GemRB.GetString(39537))
280	else:
281		Control.SetText (str (encumbrance) + ":")
282		if not Control2ID: # shouldn't happen
283			print("Missing second control parameter to SetEncumbranceLabels!")
284			return
285		Control2 = Window.GetControl (Control2ID)
286		Control2.SetText (str (max_encumb) + ":")
287
288	ratio = encumbrance / max_encumb
289	if GameCheck.IsIWD2 () or GameCheck.IsPST ():
290		if ratio > 1.0:
291			Control.SetTextColor ({'r' : 255, 'g' : 0, 'b' : 0})
292		elif ratio > 0.8:
293			Control.SetTextColor ({'r' : 255, 'g' : 255, 'b' : 0})
294		else:
295			Control.SetTextColor ({'r' : 255, 'g' : 255, 'b' : 255})
296
297		if Control2ID:
298			Control2.SetTextColor ({'r' : 255, 'g' : 0, 'b' : 0})
299
300	else:
301		if ratio > 1.0:
302			Control.SetFont ("NUMBER3");
303		elif ratio > 0.8:
304			Control.SetFont ("NUMBER2");
305		else:
306			Control.SetFont ("NUMBER");
307
308	return
309
310def GetActorClassTitle (actor):
311	"""Returns the string representation of the actors class."""
312
313	ClassTitle = GemRB.GetPlayerStat (actor, IE_TITLE1)
314
315	if ClassTitle == 0:
316		ClassName = GetClassRowName (actor)
317		KitIndex = GetKitIndex (actor)
318		Multi = HasMultiClassBits (actor)
319		Dual = IsDualClassed (actor, 1)
320		MCFlags = GemRB.GetPlayerStat (actor, IE_MC_FLAGS)
321
322		if Multi and Dual[0] == 0: # true multi class
323			ClassTitle = CommonTables.Classes.GetValue (ClassName, "CAP_REF", GTV_REF)
324		else:
325			if Dual[0]: # dual class
326				# first (previous) kit or class of the dual class
327				if Dual[0] == 1:
328					ClassTitle = CommonTables.KitList.GetValue (Dual[1], 2)
329				else:
330					ClassTitle = CommonTables.Classes.GetValue (GetClassRowName(Dual[1], "index"), "CAP_REF")
331				if ClassTitle != "*":
332					ClassTitle = GemRB.GetString (ClassTitle)
333				ClassTitle += " / "
334				if Dual[0] == 3:
335					ClassTitle += CommonTables.KitList.GetValue (Dual[2], 2, GTV_REF)
336				else:
337					ClassTitle += CommonTables.Classes.GetValue (GetClassRowName(Dual[2], "index"), "CAP_REF", GTV_REF)
338			elif MCFlags & (MC_FALLEN_PALADIN|MC_FALLEN_RANGER): # fallen
339				ClassTitle = 10369
340				if MCFlags & MC_FALLEN_PALADIN:
341					ClassTitle = 10371
342				ClassTitle = GemRB.GetString (ClassTitle)
343			else: # ordinary class or kit
344				if KitIndex:
345					ClassTitle = CommonTables.KitList.GetValue (KitIndex, 2)
346				else:
347					ClassTitle = CommonTables.Classes.GetValue (ClassName, "CAP_REF")
348				if ClassTitle != "*":
349					ClassTitle = GemRB.GetString (ClassTitle)
350	else:
351		ClassTitle = GemRB.GetString (ClassTitle)
352
353	#GetActorClassTitle returns string now...
354	#if ClassTitle == "*":
355	#	return 0
356
357	return ClassTitle
358
359
360def GetKitIndex (actor):
361	"""Return the index of the actors kit from KITLIST.2da.
362
363	Returns 0 if the class is not kitted."""
364
365	Kit = GemRB.GetPlayerStat (actor, IE_KIT)
366	KitIndex = 0
367
368	if Kit & 0xc000 == 0x4000:
369		KitIndex = Kit & 0xfff
370
371	# carefully looking for kit by the usability flag
372	# since the barbarian kit id clashes with the no-kit value
373	if KitIndex == 0 and Kit != 0x4000:
374		KitIndex = CommonTables.KitList.FindValue (6, Kit)
375		if KitIndex == -1:
376			KitIndex = 0
377
378	return KitIndex
379
380# fetches the rowname of the passed actor's (base) class from classes.2da
381# NOTE: only the "index" method is iwd2-ready, since you can have multiple classes and kits
382def GetClassRowName(value, which=-1):
383	if which == "index":
384		ClassIndex = value
385		# if barbarians cause problems, repeat the lookup again here
386	else:
387		if which == -1:
388			Class = GemRB.GetPlayerStat (value, IE_CLASS)
389		elif which == "class":
390			Class = value
391		else:
392			raise RuntimeError("Bad type parameter for GetClassRowName: " + str(which))
393		ClassIndex = CommonTables.Classes.FindValue ("ID", Class)
394	ClassRowName = CommonTables.Classes.GetRowName (ClassIndex)
395	return ClassRowName
396
397# checks the classes.2da table if the class is multiclass/dualclass capable (bits define the class combination)
398def HasMultiClassBits(actor):
399	MultiBits = CommonTables.Classes.GetValue (GetClassRowName(actor), "MULTI")
400
401	# we have no entries for npc creature classes, so treat them as single-classed
402	if MultiBits == "*":
403		MultiBits = 0
404
405	return MultiBits
406
407def IsDualClassed(actor, verbose):
408	"""Returns an array containing the dual class information.
409
410	Return[0] is 0 if not dualclassed, 1 if the old class is a kit, 3 if the new class is a kit, 2 otherwise.
411	Return[1] contains either the kit or class index of the old class.
412	Return[2] contains the class index of the new class.
413	If verbose is false, only Return[0] contains useable data."""
414
415	if GameCheck.IsIWD2():
416		return (0,-1,-1)
417
418	Multi = HasMultiClassBits (actor)
419
420	if Multi == 0:
421		return (0, -1, -1)
422
423	DualedFrom = GemRB.GetPlayerStat (actor, IE_MC_FLAGS) & MC_WAS_ANY_CLASS
424
425	if verbose:
426		KitIndex = GetKitIndex (actor)
427		if KitIndex:
428			KittedClass = CommonTables.KitList.GetValue (KitIndex, 7)
429			KittedClassIndex = CommonTables.Classes.FindValue ("ID", KittedClass)
430		else:
431			KittedClassIndex = 0
432
433		if DualedFrom > 0: # first (previous) class of the dual class
434			FirstClassIndex = CommonTables.Classes.FindValue ("MC_WAS_ID", DualedFrom)
435
436			# use the first class of the multiclass bunch that isn't the same as the first class
437			for i in range (1,16):
438				Mask = 1 << (i - 1)
439				if Multi & Mask:
440					ClassIndex = CommonTables.Classes.FindValue ("ID", i)
441					if ClassIndex == FirstClassIndex:
442						continue
443					SecondClassIndex = ClassIndex
444					break
445			else:
446				GemRB.Log (LOG_WARNING, "IsDualClassed", "Invalid dualclass combination, treating as a single class!")
447				print(DualedFrom, Multi, KitIndex, FirstClassIndex)
448				return (0,-1,-1)
449
450			if KittedClassIndex == FirstClassIndex and KitIndex:
451				return (1, KitIndex, SecondClassIndex)
452			elif KittedClassIndex == SecondClassIndex:
453				return (3, FirstClassIndex, KitIndex)
454			else:
455				return (2, FirstClassIndex, SecondClassIndex)
456		else:
457			return (0,-1,-1)
458	else:
459		if DualedFrom > 0:
460			return (1,-1,-1)
461		else:
462			return (0,-1,-1)
463
464def IsDualSwap (actor, override=None):
465	"""Returns true if the dualed classes are reverse of expection.
466
467	This can happen, because the engine gives dualclass characters the same ID as
468	their multiclass counterpart (eg. FIGHTER_MAGE = 3). Logic would dictate that
469	the new and old class levels would be stored in IE_LEVEL and IE_LEVEL2,
470	respectively; however, if one duals from a fighter to a mage in the above
471	example, the levels would actually be in reverse of expectation."""
472
473	Dual = IsDualClassed (actor, 1)
474	if override:
475		CI1 = CommonTables.Classes.FindValue ("ID", override["old"])
476		CI2 = CommonTables.Classes.FindValue ("ID", override["new"])
477		Dual = (2, CI1, CI2) # TODO: support IsDualClassed mode 3 once a gui for it is added
478
479	# not dual classed
480	if Dual[0] == 0:
481		return 0
482
483	# split the full class name into its individual parts
484	# i.e FIGHTER_MAGE becomes [FIGHTER, MAGE]
485	Class = GetClassRowName(actor).split("_")
486	if override:
487		Class = GetClassRowName(override["mc"], "class").split("_")
488
489	# get our old class name
490	if Dual[0] > 1:
491		BaseClass = GetClassRowName(Dual[1], "index")
492	else:
493		BaseClass = CommonTables.KitList.GetValue (Dual[1], 7)
494		if BaseClass == "*":
495			# mod boilerplate
496			return 0
497		BaseClass = GetClassRowName(BaseClass, "class")
498
499	# if our old class is the first class, we need to swap
500	if Class[0] == BaseClass:
501		return 1
502
503	return 0
504
505def IsMultiClassed (actor, verbose):
506	"""Returns a tuple containing the multiclass information.
507
508	Return[0] contains the total number of classes.
509	Return[1-3] contain the ID of their respective classes.
510	If verbose is false, only Return[0] has useable data."""
511
512	# change this if it will ever be needed
513	if GameCheck.IsIWD2():
514		return (0,-1,-1,-1)
515
516	# get our base class
517	IsMulti = HasMultiClassBits (actor)
518	IsDual = IsDualClassed (actor, 0)
519
520	# dual-class char's look like multi-class chars
521	if (IsMulti == 0) or (IsDual[0] > 0):
522		return (0,-1,-1,-1)
523	elif verbose == 0:
524		return (IsMulti,-1,-1,-1)
525
526	# get all our classes (leave space for our number of classes in the return array)
527	Classes = [0]*3
528	NumClasses = 0
529	Mask = 1 # we're looking at multiples of 2
530	ClassNames = GetClassRowName(actor).split("_")
531
532	# loop through each class and test it as a mask
533	ClassCount = CommonTables.Classes.GetRowCount()
534	for i in range (1, ClassCount):
535		if IsMulti&Mask: # it's part of this class
536			#we need to place the classes in the array based on their order in the name,
537			#NOT the order they are detected in
538			CurrentName = GetClassRowName (i, "class")
539			if CurrentName == "*":
540				# we read too far, as the upper range limit is greater than the number of "single" classes
541				break
542			for j in range(len(ClassNames)):
543				if ClassNames[j] == CurrentName:
544					Classes[j] = i # mask is (i-1)^2 where i is class id
545			NumClasses = NumClasses+1
546		Mask = 1 << i # shift to the next multiple of 2 for testing
547
548	# in case we couldn't figure out to which classes the multi belonged
549	if NumClasses < 2:
550		GemRB.Log (LOG_ERROR, "IsMultiClassed", "Couldn't figure out the individual classes of multiclass!")
551		print(ClassNames)
552		return (0,-1,-1,-1)
553
554	# return the tuple
555	return (NumClasses, Classes[0], Classes[1], Classes[2])
556
557def IsNamelessOne (actor):
558	# A very simple test to identify the actor as TNO
559	if not GameCheck.IsPST():
560		return False
561
562	Specific = GemRB.GetPlayerStat (actor, IE_SPECIFIC)
563	if Specific == 2:
564		return True
565
566	return False
567
568def NamelessOneClass (actor):
569	# A shortcut function to determine the identity of the nameless one
570	# and get his class if that is the case
571	if IsNamelessOne(actor):
572		return GetClassRowName(actor)
573
574	return None
575
576def CanDualClass(actor):
577	# race restriction (human)
578	RaceName = CommonTables.Races.FindValue ("ID", GemRB.GetPlayerStat (actor, IE_RACE, 1))
579	RaceName = CommonTables.Races.GetRowName (RaceName)
580	RaceDual = CommonTables.Races.GetValue (RaceName, "CANDUAL")
581	if int(RaceDual) != 1:
582		return 1
583
584	# already dualclassed
585	Dual = IsDualClassed (actor,0)
586	if Dual[0] > 0:
587		return 1
588
589	DualClassTable = GemRB.LoadTable ("dualclas")
590	CurrentStatTable = GemRB.LoadTable ("abdcscrq")
591	ClassName = GetClassRowName(actor)
592	KitIndex = GetKitIndex (actor)
593	if KitIndex == 0:
594		ClassTitle = ClassName
595	else:
596		ClassTitle = CommonTables.KitList.GetValue (KitIndex, 0)
597	Row = DualClassTable.GetRowIndex (ClassTitle)
598
599	# create a lookup table for the DualClassTable columns
600	classes = []
601	for col in range(DualClassTable.GetColumnCount()):
602		classes.append(DualClassTable.GetColumnName(col))
603
604	matches = []
605	Sum = 0
606	for col in range (0, DualClassTable.GetColumnCount ()):
607		value = DualClassTable.GetValue (Row, col)
608		Sum += value
609		if value == 1:
610			matches.append (classes[col])
611
612	# cannot dc if all the columns of the DualClassTable are 0
613	if Sum == 0:
614		print("CannotDualClass: all the columns of the DualClassTable are 0")
615		return 1
616
617	# if the only choice for dc is already the same as the actors base class
618	if Sum == 1 and ClassName in matches and KitIndex == 0:
619		print("CannotDualClass: the only choice for dc is already the same as the actors base class")
620		return 1
621
622	AlignmentTable = GemRB.LoadTable ("alignmnt")
623	Alignment = GemRB.GetPlayerStat (actor, IE_ALIGNMENT)
624	AlignmentColName = CommonTables.Aligns.FindValue (3, Alignment)
625	AlignmentColName = CommonTables.Aligns.GetValue (AlignmentColName, 4)
626	if AlignmentColName == -1:
627		print("CannotDualClass: extraordinary character alignment")
628		return 1
629	Sum = 0
630	for classy in matches:
631		Sum += AlignmentTable.GetValue (classy, AlignmentColName)
632
633	# cannot dc if all the available classes forbid the chars alignment
634	if Sum == 0:
635		print("CannotDualClass: all the available classes forbid the chars alignment")
636		return 1
637
638	# check current class' stat limitations
639	ClassStatIndex = CurrentStatTable.GetRowIndex (ClassTitle)
640	for stat in range (6):
641		minimum = CurrentStatTable.GetValue (ClassStatIndex, stat)
642		name = CurrentStatTable.GetColumnName (stat)
643		if GemRB.GetPlayerStat (actor, SafeStatEval ("IE_" + name[4:]), 1) < minimum:
644			print("CannotDualClass: current class' stat limitations are too big")
645			return 1
646
647	# check new class' stat limitations - make sure there are any good class choices
648	TargetStatTable = GemRB.LoadTable ("abdcdsrq")
649	bad = 0
650	for match in matches:
651		ClassStatIndex = TargetStatTable.GetRowIndex (match)
652		for stat in range (6):
653			minimum = TargetStatTable.GetValue (ClassStatIndex, stat)
654			name = TargetStatTable.GetColumnName (stat)
655			if GemRB.GetPlayerStat (actor, SafeStatEval ("IE_" + name[4:]), 1) < minimum:
656				bad += 1
657				break
658	if len(matches) == bad:
659		print("CannotDualClass: no good new class choices")
660		return 1
661
662	# must be at least level 2
663	if GemRB.GetPlayerStat (actor, IE_LEVEL) == 1:
664		print("CannotDualClass: level 1")
665		return 1
666	return 0
667
668def IsWarrior (actor):
669	IsWarrior = CommonTables.ClassSkills.GetValue (GetClassRowName(actor), "NO_PROF")
670
671	# warriors get only a -2 penalty for wielding weapons they are not proficient with
672	# FIXME: make the check more robust, someone may change the value!
673	IsWarrior = (IsWarrior == -2)
674
675	Dual = IsDualClassed (actor, 0)
676	if Dual[0] > 0:
677		DualedFrom = GemRB.GetPlayerStat (actor, IE_MC_FLAGS) & MC_WAS_ANY_CLASS
678		FirstClassIndex = CommonTables.Classes.FindValue ("MC_WAS_ID", DualedFrom)
679		FirstClassName = CommonTables.Classes.GetRowName (FirstClassIndex)
680		OldIsWarrior = CommonTables.ClassSkills.GetValue (FirstClassName, "NO_PROF")
681		# there are no warrior to warrior dualclasses, so if the previous class was one, the current one certainly isn't
682		if OldIsWarrior == -2:
683			return 0
684		# but there are also non-warrior to non-warrior dualclasses, so just use the new class check
685
686	return IsWarrior
687
688def SetupDamageInfo (pc, Button, Window):
689	hp = GemRB.GetPlayerStat (pc, IE_HITPOINTS)
690	hp_max = GemRB.GetPlayerStat (pc, IE_MAXHITPOINTS)
691	state = GemRB.GetPlayerStat (pc, IE_STATE_ID)
692
693	if hp_max < 1 or hp == "?":
694		ratio = 0.0
695	else:
696		ratio = hp / float(hp_max)
697
698	if hp < 1 or (state & STATE_DEAD):
699		c = {'r' : 64, 'g' : 64, 'b' : 64, 'a' : 255}
700		Button.SetOverlay (0, c, c)
701
702	if ratio == 1.0:
703		band = 0
704		color = {'r' : 255, 'g' : 255, 'b' : 255}  # white
705	elif ratio >= 0.75:
706		band = 1
707		color = {'r' : 0, 'g' : 255, 'b' : 0}  # green
708	elif ratio >= 0.50:
709		band = 2
710		color = {'r' : 255, 'g' : 255, 'b' : 0}  # yellow
711	elif ratio >= 0.25:
712		band = 3
713		color = {'r' : 255, 'g' : 128, 'b' : 0}  # orange
714	else:
715		band = 4
716		color = {'r' : 255, 'g' : 0, 'b' : 0}  # red
717
718	if GemRB.GetVar("Old Portrait Health") or not GameCheck.IsIWD2():
719		# draw the blood overlay
720		if hp >= 1 and not (state & STATE_DEAD):
721			c1 = {'r' : 0x70, 'g' : 0, 'b' : 0, 'a' : 0xff}
722			c2 = {'r' : 0xf7, 'g' : 0, 'b' : 0, 'a' : 0xff}
723			Button.SetOverlay (ratio, c1, c2)
724	else:
725		# scale the hp bar under the portraits and recolor it
726		# GUIHITPT has 5 frames with different severity colors
727		# luckily their ids follow a nice pattern
728		hpBar = Window.GetControl (pc-1 + 50)
729		hpBar.SetBAM ("GUIHITPT", band, 0)
730		hpBar.SetPictureClipping (ratio)
731		hpBar.SetFlags (IE_GUI_BUTTON_NO_IMAGE, OP_OR)
732
733	ratio_str = ""
734	if hp != "?":
735		ratio_str = "\n%d/%d" %(hp, hp_max)
736	Button.SetTooltip (GemRB.GetPlayerName (pc, 1) + ratio_str)
737
738	return ratio_str, color
739
740def SetCurrentDateTokens (stat, plural=False):
741	# NOTE: currentTime is in seconds, joinTime is in seconds * 15
742	#   (script updates). In each case, there are 60 seconds
743	#   in a minute, 24 hours in a day, but ONLY 5 minutes in an hour!!
744	# Hence currentTime (and joinTime after div by 15) has
745	#   7200 secs a day (60 * 5 * 24)
746	currentTime = GemRB.GetGameTime ()
747	joinTime = stat['JoinDate'] - stat['AwayTime']
748
749	party_time = currentTime - (joinTime // 15)
750	days = party_time // 7200
751	hours = (party_time % 7200) // 300
752
753	GemRB.SetToken ('GAMEDAYS', str (days))
754	GemRB.SetToken ('HOUR', str (hours))
755	if plural:
756		return
757
758	# construct <GAMEDAYS> days ~and~ ~<HOUR> hours~
759	if days == 1:
760		time = GemRB.GetString (10698)
761	else:
762		time = GemRB.GetString (10697)
763	time += " " + GemRB.GetString (10699) + " "
764	if days == 0:
765		# only display hours
766		time = ""
767
768	if hours == 1:
769		time += GemRB.GetString (10701)
770	else:
771		time += GemRB.GetString (10700)
772
773	return time
774
775def SetSaveDir():
776	if GameCheck.IsIWD1() or GameCheck.IsIWD2():
777		GemRB.SetToken ("SaveDir", "mpsave")
778	elif GameCheck.IsBG1() and GemRB.GetVar ("PlayMode") == 1:
779		GemRB.SetToken ("SaveDir", "mpsave")
780	else:
781		GemRB.SetToken ("SaveDir", "save")
782
783# gray out window or mark it as visible depending on the actor's state
784# Always greys it out for actors that are: dead, berserking
785# The third parameter is another check which must be 0 to maintain window visibility
786def AdjustWindowVisibility (Window, pc, additionalCheck):
787	if not additionalCheck and GemRB.ValidTarget (pc, GA_SELECT|GA_NO_DEAD):
788		Window.SetDisabled (False)
789	else:
790		Window.SetDisabled (True)
791	return
792
793def UsingTouchInput ():
794	return GemRB.GetSystemVariable (SV_TOUCH)
795
796# return ceil(n/d)
797#
798def ceildiv (n, d):
799	if d == 0:
800		raise ZeroDivisionError("ceildiv by zero")
801	elif d < 0:
802		return (n + d + 1) // d
803	else:
804		return (n + d - 1) // d
805
806# a placeholder for unimplemented and hardcoded key actions
807def ResolveKey():
808	return
809
810# eval that only accepts alphanumerics and "_"
811# used for converting constructed stat names to their values, eg. IE_STR to 36
812def SafeStatEval (expression):
813	# if we ever import string: string.ascii_letters + "_" + string.digits
814	alnum = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
815	for chr in expression:
816		if chr not in alnum:
817			raise ValueError("Invalid input! Bad data encountered, check the GemRB install's integrity!")
818
819	return eval(expression)
820
821GameWindow = GUIClasses.GWindow(ID=0, SCRIPT_GROUP="GAMEWIN")
822GameControl = GUIClasses.GView(ID=0, SCRIPT_GROUP="GC")
823
824def DisplayAC (pc, window, labelID):
825	AC = GemRB.GetPlayerStat (pc, IE_ARMORCLASS) + GetACStyleBonus (pc)
826	Label = window.GetControl (labelID)
827	Label.SetText (str (AC))
828	Label.SetTooltip (17183)
829
830def GetACStyleBonus (pc):
831	stars = GemRB.GetPlayerStat(pc, IE_PROFICIENCYSINGLEWEAPON) & 0x7
832	if not stars:
833		return 0
834
835	WStyleTable = GemRB.LoadTable ("wssingle", 1)
836	if not WStyleTable:
837		return 0
838	# are we actually single-wielding?
839	cdet = GemRB.GetCombatDetails (pc, 0)
840	if cdet["Style"] % 1000 != IE_PROFICIENCYSINGLEWEAPON:
841		return 0
842	return WStyleTable.GetValue (str(stars), "AC")
843
844def AddDefaultVoiceSet (VoiceList, Voices):
845	if GameCheck.IsBG1 () or GameCheck.IsBG2 ():
846		Options = collections.OrderedDict(enumerate(Voices))
847		Options[-1] = "default"
848		Options = collections.OrderedDict(sorted(Options.items()))
849		VoiceList.SetOptions (list(Options.values()))
850		return True
851	return False
852
853def OverrideDefaultVoiceSet (Gender, CharSound):
854	# handle "default" gendered voice
855	if CharSound == "default" and not GemRB.HasResource ("defaulta", RES_WAV):
856		if GameCheck.IsBG1 ():
857			Gender2Sound = [ "", "mainm", "mainf" ]
858			CharSound = Gender2Sound[Gender]
859		elif GameCheck.IsBG2 ():
860			Gender2Sound = [ "", "male005", "female4" ]
861			CharSound = Gender2Sound[Gender]
862	return CharSound
863
864class _stdioWrapper(object):
865	def __init__(self, log_level):
866		self.log_level = log_level
867		self.buffer = ""
868	def write(self, message):
869		self.buffer += str(message)
870		if self.buffer.endswith("\n"):
871			out = self.buffer.rstrip("\n")
872			if out:
873				GemRB.Log(self.log_level, "Python", out)
874			self.buffer = ""
875	def flush(self):
876		pass
877
878def _wrapStdio():
879	import sys
880	sys.stdout = _stdioWrapper(LOG_MESSAGE)
881	sys.stderr = _stdioWrapper(LOG_ERROR)
882
883_wrapStdio()
884