summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/editorui/paths.py117
-rw-r--r--src/exporter.py135
-rw-r--r--src/mapdata.py9
-rw-r--r--src/ui.py6
-rw-r--r--src/unlock.py224
5 files changed, 325 insertions, 166 deletions
diff --git a/src/editorui/paths.py b/src/editorui/paths.py
index 4110188..43a1465 100644
--- a/src/editorui/paths.py
+++ b/src/editorui/paths.py
@@ -538,93 +538,6 @@ class KPEditorPath(QtGui.QGraphicsLineItem):
self.setPalette(palette)
- class UnlockButton(QtGui.QPushButton):
- def __init__(self, pathRef):
- QtGui.QPushButton.__init__(self)
-
- self.setFixedSize(48,48)
-
- self.iconList = [QtGui.QIcon("Resources/Key.png"),
- QtGui.QIcon("Resources/SecretKey.png")]
-
- self.unlockIcon = QtGui.QIcon("Resources/Unlock.png")
- self.arrowIcon = [QtGui.QIcon("Resources/KeyArrow.png"),
- QtGui.QIcon("Resources/SecretKeyArrow.png")]
-
-
- self._pathRef = pathRef
-
- self.secret = 0
- self.path = 0
-
- if not hasattr(KPEditorPath.UnlockButton, 'PALETTE'):
- KPEditorPath.UnlockButton.PALETTE = QtGui.QPalette(Qt.transparent)
-
- self.setPalette(self.PALETTE)
-
- self.released.connect(self.toggle)
-
-
- def toggle(self):
-
- path = self._pathRef()
-
- if KP.app.keyboardModifiers() == Qt.ShiftModifier:
- if self.secret == 1:
- self.secret = 0
- else:
- self.secret = 1
-
- path.secret = self.secret
-
- else:
- self.path += 1
-
- if self.path > 2:
- self.path = 0
-
- path.unlocks = self.path
-
-
- def paintEvent(self, event):
- painter = QtGui.QPainter(self)
- contentsRect = self.contentsRect()
- smallRect = QtCore.QRect(12, 12, 24, 24)
- painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
-
- if self.path > 0:
- painter.save()
-
- displaceX, displaceY = contentsRect.width() / 2, contentsRect.height() / 2
-
- pathItem = self._pathRef().qtItem.line()
-
- if self.path == 1:
- angle = 90 - pathItem.angle()
- else:
- angle = 270 - pathItem.angle()
-
- painter.translate(displaceX, displaceY)
- painter.rotate(angle)
- painter.translate(-displaceX, -displaceY)
- self.arrowIcon[self.secret].paint(painter, contentsRect, Qt.AlignCenter)
-
- painter.restore()
-
- if self.isDown():
- self.iconList[self.secret].paint(painter, smallRect, Qt.AlignCenter, QtGui.QIcon.Disabled)
- else:
- self.iconList[self.secret].paint(painter, smallRect, Qt.AlignCenter)
-
- else:
- if self.isDown():
- self.unlockIcon.paint(painter, smallRect, Qt.AlignCenter, QtGui.QIcon.Disabled)
- else:
- self.unlockIcon.paint(painter, smallRect, Qt.AlignCenter)
-
- painter.end()
-
-
class HiddenProxy(QtGui.QGraphicsProxyWidget):
def __init__(self, button, parent, x, y):
QtGui.QGraphicsProxyWidget.__init__(self, parent)
@@ -663,12 +576,6 @@ class KPEditorPath(QtGui.QGraphicsLineItem):
if not hasattr(KPEditorPath, 'SELECTION_PEN'):
KPEditorPath.SELECTION_PEN = QtGui.QPen(Qt.blue, 1, Qt.DotLine)
- self.unlock = self.UnlockButton(self._pathRef)
- self.unlockProxy = self.HiddenProxy(self.unlock, self, -24, -24)
-
- self.unlock.secret = path.secret
- self.unlock.path = path.unlocks
-
self.options = self.PathOptionsMenuButton(self._pathRef)
self.optionsProxy = self.HiddenProxy(self.options, self, -54, +24)
@@ -677,6 +584,25 @@ class KPEditorPath(QtGui.QGraphicsLineItem):
self.updatePosition()
+ def mousePressEvent(self, event):
+ if event.button() != Qt.LeftButton:
+ return
+ if QtGui.QApplication.keyboardModifiers() != QtCore.Qt.ControlModifier:
+ return
+
+ # modify the unlock settings
+ from unlock import KPUnlockSpecDialog
+
+ dlg = KPUnlockSpecDialog('path', 'unlocked')
+
+ if hasattr(self._pathRef(), 'unlockSpec'):
+ dlg.setSpec(self._pathRef().unlockSpec)
+
+ result = dlg.exec_()
+ if result == QtGui.QDialog.Accepted:
+ print "New spec:", dlg.spec
+ self._pathRef().unlockSpec = dlg.spec
+
def updatePosition(self):
path = self._pathRef()
@@ -720,12 +646,9 @@ class KPEditorPath(QtGui.QGraphicsLineItem):
painter.setPen(self.SELECTION_PEN)
painter.setBrush(QtGui.QColor(0,0,0,0))
painter.drawPath(self.shape())
-
- self.unlockProxy.show()
self.optionsProxy.show()
else:
- self.unlockProxy.hide()
self.optionsProxy.hide()
@@ -753,4 +676,4 @@ class KPEditorPath(QtGui.QGraphicsLineItem):
self.scene().removeItem(self)
- \ No newline at end of file
+
diff --git a/src/exporter.py b/src/exporter.py
index 2396633..2f99a56 100644
--- a/src/exporter.py
+++ b/src/exporter.py
@@ -191,7 +191,8 @@ class KPMapExporter:
sectorData = self._packSectorData(sectors)
# now that we've got that, we can pack the first part of the file
- data = bytearray(struct.pack('>IIII', len(self.layers), 16 + len(sectorData), 0, 0))
+ data = bytearray(struct.pack('>IIIII', len(self.layers), 20 + len(sectorData), 0, 0, 0))
+ requiredFixUps.append((16, 'UnlockBytecode'))
# list of layer pointers goes here.. or will, later
data += sectorData
@@ -200,22 +201,6 @@ class KPMapExporter:
requiredFixUps.append((len(data), layer))
data += zero32
- # map all paths to unlock info
- unlockInfo = {}
-
- for node in self.map.pathLayer.nodes:
- if not node.level: continue
-
- checked = set()
- affected = []
- self._findUnlocksForNode(node, checked, affected)
-
- level1, level2 = node.level
-
- for item, secret in affected:
- unlockInfo[item] = (level1, level2, secret)
-
-
# now build the layers
for eLayer in self.layers:
layer = eLayer.layer
@@ -225,6 +210,7 @@ class KPMapExporter:
if isinstance(eLayer, self.TileLayerExporter):
data += u32.pack(0)
+ data += u32.pack(0xFF000000)
# tileset name
tileset = '/Maps/Texture/%s.bin' % layer.tileset
@@ -243,6 +229,7 @@ class KPMapExporter:
elif isinstance(eLayer, self.DoodadLayerExporter):
data += u32.pack(1)
+ data += u32.pack(0xFF000000)
# doodad list
try:
@@ -277,6 +264,15 @@ class KPMapExporter:
elif isinstance(eLayer, self.PathLayerExporter):
data += u32.pack(2)
+ data += zero32
+
+ # before we do anything, build the list of secret levels
+ # we'll need that
+ levelsWithSecrets = set()
+
+ for path in layer.paths:
+ if hasattr(path, 'unlockSpec') and path.unlockSpec is not None:
+ self._checkSpecForSecrets(path.unlockSpec, levelsWithSecrets)
# lists
current = len(data)
@@ -343,8 +339,9 @@ class KPMapExporter:
if node.isStop():
if node.level:
level1, level2 = node.level
- # i i b b b b: node type, Extra pointer, world, level, padding (hasSecret?), padding
- data += struct.pack('>iibbbb', 2, 0, level1, level2, 0, 0)
+ hasSecret = (1 if ((level1,level2) in levelsWithSecrets) else 0)
+ # i i i b b b b: node type, isNew, Extra pointer, world, level, hasSecret, padding
+ data += struct.pack('>iiibbbb', 2, 0, 0, level1, level2, hasSecret, 0)
elif node.mapChange:
data += u32.pack(3) # node type
@@ -353,17 +350,22 @@ class KPMapExporter:
requiredFixUps.append((len(data)+4, destMap))
stringsToAdd.add(destMap)
- # i i b b b b: Extra pointer, dest map, map ID, foreign ID, transition, padding
- data += struct.pack('>iibbbb', 0, 0, node.mapID, node.foreignID, node.transition, 0)
+ # i i i b b b b: isNew, Extra pointer, dest map, map ID, foreign ID, transition, padding
+ data += struct.pack('>iiibbbb', 0, 0, 0, node.mapID, node.foreignID, node.transition, 0)
else:
data += u32.pack(1) # node type
- data += u32.pack(0) # Extra pointer
+ data += zero32 # isNew
+ data += zero32 # Extra pointer
else:
- data += u32.pack(0) # node type
- data += u32.pack(0) # Extra pointer
+ data += zero32 # node type
+ data += zero32 # isNew
+ data += zero32 # Extra pointer
- for path in layer.paths:
+ pathIndices = {}
+
+ for i, path in enumerate(layer.paths):
+ pathIndices[path] = i
offsets[path] = len(data)
start = path._startNodeRef()
@@ -379,13 +381,11 @@ class KPMapExporter:
data += (zero32 * 4)
- try:
- unlockL1, unlockL2, isSecret = unlockInfo[path]
- unlockType = (2 if isSecret else 1)
- except KeyError:
- unlockL1, unlockL2, unlockType = 0, 0, 0
+ available = 0
+ if (not hasattr(path, 'unlockSpec')) or path.unlockSpec is None:
+ available = 3
- data += struct.pack('>bbbbfi', unlockType, unlockL1, unlockL2, 1, path.movementSpeed, path.animation)
+ data += struct.pack('>bbbbfi', available, 0, 0, 0, path.movementSpeed, path.animation)
# now that we're almost done... pack the strings
for string in stringsToAdd:
@@ -420,6 +420,41 @@ class KPMapExporter:
for piece in imageData:
data += piece
+ # at the end comes the unlock bytecode
+ offsets['UnlockBytecode'] = len(data)
+
+ # first off, build a map of unlocks
+ unlockLists = {}
+
+ from unlock import stringifyUnlockData
+
+ for path in self.map.pathLayer.paths:
+ if not hasattr(path, 'unlockSpec'):
+ continue
+ spec = path.unlockSpec
+ if spec is None:
+ continue
+
+ # we stringify it first because the specs become lists when
+ # imported from the kpmap (not tuples) and those can't be
+ # used as dict keys
+ spec = stringifyUnlockData(spec)
+ try:
+ lst = unlockLists[spec]
+ except KeyError:
+ lst = []
+ unlockLists[spec] = lst
+ lst.append(path)
+
+ # now produce the thing
+ from unlock import parseUnlockText, packUnlockSpec
+
+ for spec, lst in unlockLists.iteritems():
+ data += packUnlockSpec(parseUnlockText(spec))
+ data += chr(len(lst))
+ for p in lst:
+ data += u16.pack(pathIndices[p])
+
# to finish up, correct every offset
for offset, target in requiredFixUps:
u32.pack_into(data, offset, offsets[target])
@@ -430,36 +465,16 @@ class KPMapExporter:
ANIM_CURVES = ['Linear', 'Sinusoidial', 'Cosinoidial']
ANIM_TYPES = ['X Position', 'Y Position', 'Angle', 'X Scale', 'Y Scale', 'Opacity']
+ def _checkSpecForSecrets(self, spec, levelSet):
+ kind = spec[0]
- def _findUnlocksForNode(self, node, checked, affected, isFirstBranch=True, secret=None):
- if node in checked: return
-
- checked.add(node)
-
- for path in node.exits:
- if path not in checked:
- checked.add(path)
- self._findUnlocksForPath(path, node, checked, affected, isFirstBranch, secret)
-
-
- def _findUnlocksForPath(self, path, sourceNode, checked, affected, isFirstBranch, secret=None):
- start, end = path._startNodeRef(), path._endNodeRef()
- if start == sourceNode:
- destNode = end
- if isFirstBranch and path.unlocks != 1:
- return
- else:
- destNode = start
- if isFirstBranch and path.unlocks != 2:
- return
-
- if secret is None:
- secret = path.secret
- affected.append((path, secret))
-
- if not destNode.isStop():
- self._findUnlocksForNode(destNode, checked, affected, False, secret)
-
+ if kind == 'level':
+ k, one, two, secret = spec
+ if secret:
+ levelSet.add((one, two))
+ elif kind == 'and' or kind == 'or':
+ for term in spec[1]:
+ self._checkSpecForSecrets(term, levelSet)
def _buildGXTexObjRGB5A3(self, width, height, imgOffset):
# Format: RGB5A3 (5)
diff --git a/src/mapdata.py b/src/mapdata.py
index c32bd7b..d175046 100644
--- a/src/mapdata.py
+++ b/src/mapdata.py
@@ -451,7 +451,7 @@ class KPNode(object):
@mapfile.dumpable('path')
class KPPath(object):
- __dump_attribs__ = ('unlocks', 'secret', 'animation', 'movementSpeed')
+ __dump_attribs__ = ('unlockSpec', 'animation', 'movementSpeed')
def _dump(self, mapObj, dest):
dest['startNodeLink'] = mapObj.refNode(self._startNodeRef())
@@ -464,6 +464,10 @@ class KPPath(object):
# self.linkedLayer = mapObj.derefLayer(src['linkedLayer'])
def __init__(self, startNode=None, endNode=None, cloneFrom=None):
+ # this is placed before the null ctor in case we load an old
+ # kpmap that didn't have unlockSpec
+ self.unlockSpec = None # always unlocked, by default
+
if startNode is None and endNode is None:
# null ctor, ignore this
# we're probably loaded from a file, so trust
@@ -476,9 +480,6 @@ class KPPath(object):
startNode.exits.append(self)
endNode.exits.append(self)
- self.unlocks = 0 # 0 = always unlocked, 1 = unlocked from startNode, 2 = unlocked from endNode
- self.secret = 0 # 0 = unlocks from normal exit, 1 = unlocks from secret exit
-
if cloneFrom is None:
self.animation = 0
else:
diff --git a/src/ui.py b/src/ui.py
index 766a6f1..ba63320 100644
--- a/src/ui.py
+++ b/src/ui.py
@@ -61,12 +61,8 @@ class KPPathNodeList(QtGui.QWidget):
"Ladder", "LadderLeft", "LadderRight", "Fall",
"Swim", "Run", "Pipe", "Door"]
animation = AnimationList[self.associate.animation]
- if self.associate.secret == 0:
- unlock = 'Normal'
- else:
- unlock = 'Secret'
- return 'Path: {0} Exit, {1}'.format(unlock, animation)
+ return 'Path: {1}'.format(None, animation)
elif role == Qt.CheckStateRole:
return (Qt.Checked if self.layer.visible else Qt.Unchecked)
diff --git a/src/unlock.py b/src/unlock.py
new file mode 100644
index 0000000..cf3e6fd
--- /dev/null
+++ b/src/unlock.py
@@ -0,0 +1,224 @@
+import re
+
+LEVEL_RE = re.compile(r'^([0-9]{1,2})-([0-9]{1,2})( secret)?$')
+COMBINER_RE = re.compile(r'[ ]*(and|or)[ ]*')
+
+class UnlockParseError(ValueError):
+ # todo: is this the proper way to make an Error?
+ pass
+
+def parseUnlockText(text):
+ parsed = _parseUnlockBit(text.lower())
+ if parsed == ('always',):
+ return None
+ else:
+ return parsed
+
+def _parseUnlockBit(text):
+ # blank criterion
+ if text == '':
+ return ('always',)
+
+ # is this a simple one...?
+ m = LEVEL_RE.match(text)
+ if m:
+ one, two, secret = m.groups()
+ w = int(one)
+ l = int(two)
+ if w < 1 or w > 10:
+ raise UnlockParseError('world must be between 1 to 10 inclusive; not %s' % w)
+ return ('level', w, l, (secret != None))
+
+ # OK, let's parse parentheses
+ pLevel = 0
+ endAt = len(text) - 1
+
+ # this could be either AND or OR or nothing at all
+ # we won't know it until we finish parsing!
+ whatCombiner = None
+
+ subTerms = []
+ currentSubTermStart = None
+
+ skip = 0
+
+ for index, char in enumerate(text):
+ if skip > 0:
+ skip -= 1
+ continue
+
+ if char == '(':
+ if pLevel == 0:
+ currentSubTermStart = index
+ pLevel += 1
+ elif char == ')':
+ pLevel -= 1
+ if pLevel < 0:
+ raise UnlockParseError('close parenthesis without a matching open')
+ elif pLevel == 0:
+ subTerms.append((currentSubTermStart, index, text[currentSubTermStart+1:index]))
+ if len(subTerms) > 64:
+ raise UnlockParseError('no more than 64 subterms in one %s condition' % whatCombiner.upper())
+
+ # are we expecting to see something else?
+ if index == endAt: break
+
+ m = COMBINER_RE.match(text, index + 1)
+ if not m:
+ raise UnlockParseError('something unexpected at position %d' % (index+1))
+
+ # what is it?
+ nextCombiner = m.group(1)
+ if whatCombiner is not None and nextCombiner != whatCombiner:
+ raise UnlockParseError('mixed %s and %s in one term. use more parentheses!' % (whatCombiner,nextCombiner))
+ whatCombiner = nextCombiner
+
+ # go right past this, to the next subterm
+ skip = len(m.group(0))
+ if (index + skip) == endAt:
+ raise UnlockParseError('%s what?!' % (whatCombiner.upper()))
+ else:
+ if pLevel == 0:
+ if index == 0:
+ raise UnlockParseError('that\'s not right')
+ else:
+ raise UnlockParseError('something unexpected at position %d' % index)
+
+ if pLevel > 0:
+ raise UnlockParseError('unclosed parenthesis')
+
+ # now that we're here, we must have parsed these subterms
+ # do we have a combiner?
+ if whatCombiner is None:
+ if len(subTerms) != 1:
+ raise UnlockParseError('unclosed parenthesis')
+
+ return _parseUnlockBit(subTerms[0][2])
+ else:
+ return (whatCombiner, map(lambda x: _parseUnlockBit(x[2]), subTerms))
+
+
+def stringifyUnlockData(data):
+ if data == None:
+ return ''
+
+ kind = data[0]
+
+ if kind == 'always':
+ return ''
+ elif kind == 'level':
+ return '%02d-%02d%s' % (data[1], data[2], (' secret' if data[3] else ''))
+ elif kind == 'and' or kind == 'or':
+ return (' %s ' % kind).join(map(lambda x: '(%s)' % stringifyUnlockData(x), data[1]))
+
+
+def packUnlockSpec(data):
+ kind = data[0]
+
+ if kind == 'always':
+ return '\x0F'
+
+ elif kind == 'level':
+ k, world, level, secret = data
+
+ one = (1 << 6) | (0x10 if secret else 0) | (world - 1)
+
+ return chr(one) + chr(level - 1)
+
+ elif kind == 'and' or kind == 'or':
+ terms = data[1]
+ cond = 2 if (kind == 'and') else 3
+ one = (cond << 6) | (len(terms) - 1)
+
+ return chr(one) + ''.join(map(packUnlockSpec, terms))
+
+
+if __name__ == '__main__':
+ p1 = parseUnlockText('((01-01 secret) and (01-02)) or (02-99 secret) or (01-01)')
+ p2 = parseUnlockText('(1-1 secret) or ((1-2) and (1-3 secret)) or (2-1)')
+
+ print
+ print repr(p1)
+ print
+ print stringifyUnlockData(p1)
+ print
+ print repr(p2)
+ print
+ print stringifyUnlockData(p2)
+
+ from sys import exit
+ exit()
+
+
+
+
+
+from common import *
+
+
+
+
+class KPUnlockSpecDialog(QtGui.QDialog):
+ def __init__(self, forWhat, unlockAdjective):
+ QtGui.QDialog.__init__(self)
+
+ self.setWindowTitle('Set Unlock Criteria')
+
+ text = """You may enter various criteria that must be fulfilled for this {0} to be {1}.<br>
+ <br>
+ Here are some examples of what you can use:
+ <ul>
+ <li>01-01 - <i>a single criterion</i></li>
+ <li>01-01 secret - <i>secret exits</i></li>
+ <li>(01-01 secret) and (01-02) - <i>combine two criteria</i></li>
+ <li>((01-01 secret) or (01-02)) and (01-04) - <i>nested criteria</i></li>
+ </ul>
+ Each criterion used on the sides of AND and OR must be surrounded by parentheses.
+ You may use more than one, for example: <i>(01-01) or (02-02) or (03-03)</i><br>
+ <br>
+ To leave this {0} permanently unlocked, leave the box blank.
+ """.format(forWhat, unlockAdjective)
+
+ self.label = QtGui.QLabel(text)
+ self.label.setWordWrap(True)
+
+ self.textBox = QtGui.QLineEdit()
+ self.textBox.textChanged.connect(self.checkInputValidity)
+
+ self.statusLabel = QtGui.QLabel()
+ self.statusLabel.setWordWrap(True)
+
+ self.buttons = QtGui.QDialogButtonBox(
+ QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
+
+ self.buttons.accepted.connect(self.accept)
+ self.buttons.rejected.connect(self.reject)
+
+ self.layout = QtGui.QVBoxLayout()
+ self.layout.addWidget(self.label)
+ self.layout.addWidget(self.textBox)
+ self.layout.addWidget(self.statusLabel)
+ self.layout.addWidget(self.buttons)
+ self.setLayout(self.layout)
+
+ self.spec = None
+
+ def setSpec(self, spec):
+ self.textBox.setText(stringifyUnlockData(spec))
+
+ def checkInputValidity(self, text):
+ valid = True
+ try:
+ self.spec = parseUnlockText(str(text))
+ except UnlockParseError as e:
+ valid = False
+ error = str(e)
+ self.spec = None
+
+ self.buttons.button(QtGui.QDialogButtonBox.Ok).setEnabled(valid)
+
+ if valid:
+ self.statusLabel.setText('Your input is valid.')
+ else:
+ self.statusLabel.setText('[!] %s' % error)
+