Difference between revisions of "UI Tutorial (draft)"

From Dragon Age Toolset Wiki
Jump to: navigation, search
(GFX to SWF)
m (Intrinsics clarification)
Line 138: Line 138:
 
You can first get the open source FlashDevelop IDE which comes with MTASC and the standard Flash library. If you have the SWFs and the ActionScript *.as files, then what you can do is create an empty ActionScript 2 project, in the project settings turn off "use main entry point" and under the project "injection" tab, point it to the SWF you want to update with your new ActionScript.
 
You can first get the open source FlashDevelop IDE which comes with MTASC and the standard Flash library. If you have the SWFs and the ActionScript *.as files, then what you can do is create an empty ActionScript 2 project, in the project settings turn off "use main entry point" and under the project "injection" tab, point it to the SWF you want to update with your new ActionScript.
  
You'll also want to update the project classpaths to point to your game's ActionScript library. Just in my experience, there seem to be some properties on some of the standard classes that would normally prevent the script from compiling - but in that case what you can do is modify the so-called intrinsic files to let the compiler know about them. E.g., you can add needed function and var declarations to the intrinsic class interfaces under C:\Program Files\FlashDevelop\Tools\mtasc.
+
You'll also want to update the project classpaths to point to your game's ActionScript library. Just in my experience, there seem to be some properties on some of the standard classes that would normally prevent the script from compiling - but in that case what you can do is modify the so-called intrinsic files to let the compiler know about them. E.g., you can add needed function and var declarations to the intrinsic class interfaces under C:\Program Files\FlashDevelop\Tools\mtasc. The nice thing about ActionScript and intrinsics is that you don't even need to know the values of these "missing" properties or functions if they're basically defined in the runtime environment. E.g., Mouse.LEFT isn't standard Flash, but if you edit Mouse.as and add '''static var LEFT;''' to the intrinsic class definition, then you're good to go, as the value gets looked up during execution like a dictionary value (in fact, Mouse["left"] should yield the same result).
  
 
Also if you press Ctrl+J you'll get the type explorer where you can right click on a folder (that you've added to your classpath) and select "convert to intrinsic". So this way you only need to compile the actual *.as file you're making changes to and keep the others around as intrinsics. Using the IDE's SWF injection I mentioned above will make it so you don't have to worry about recompiling everything.
 
Also if you press Ctrl+J you'll get the type explorer where you can right click on a folder (that you've added to your classpath) and select "convert to intrinsic". So this way you only need to compile the actual *.as file you're making changes to and keep the others around as intrinsics. Using the IDE's SWF injection I mentioned above will make it so you don't have to worry about recompiling everything.

Revision as of 02:10, 13 November 2010

Author's note: STILL A WORK IN PROGRESS - FIRST DUMPING INFORMATION OUT, THEN I'LL FORMAT AND SECTION IT OFF BETTER. This isn't a very good tutorial, but as I said, I'm just trying to get the information out there and I'm slow to update this. Also, it may be simpler now that the UDK has support for GFX files, but I don't know the cross-licensing or technical situation on that right now.

Introduction

The UI files are basically just version 8 Adobe Flash files, but extended with some extra properties and image handling (they even use nonstandard tags in the SWF for that). GFX files supposedly have more optimized fonts and who really knows what else, but you shouldn't have to worry about font embedding in Dragon Age.

This means they use the older ActionScript 2.0, which you can find some learning resources on here fl8_learning_as2.pdf and here flash_as2_learning.pdf, among other places scattered on the web. You can download the API reference, but a decent online location is here (faster than Adobe's site I find).

Other information sources

There's a woefully incomplete GUI category on the wiki here Category:GUI. It was an early effort at detailing some of the class specifics in the UI files. There are a lot of stubs, but some classes are more detailed.

There's also a long-standing draft article on the profile page here User:FollowTheGourd about the conversation UI. It could also be ammended to explain things like 800x600 is treated as if it were 1024x768, and that the cutscene bars actually have a negative hight if the screen width is much longer than the screen height, as seen with Eyefinity display setups.

Another thing to realize is the game's aspect ratio correction and how one resolution in window mode will get stretched to fill your screen space in fullscreen mode. So then there's a difference between the Stage (Flash term) width and height and the video settings which you can access using the ExternalCommands ActionScript class.

Toolchain

FlashDevelop IDE. etc

Organizing Sources

Obtain an ActionScript 2.0 decompiler such as Sothink or Trillix. Each has their shortcomings and will require further post-processing. Sothink's ActionScript output will probably require more work to make compile with MTASC, but Trillix's output seems to have some stranger bugs I'll mention later, which makes me more hesitant to recommend it. Sothink's tool still seems to have an issue formatting double-precision values as single-precision, as detailed in the section below. Flare is also a nice free tool to compare output against, but it unfortunately isn't in a format that you can simply recompile.

Flasm is also a useful tool if you wish to examine the p-code directly (as a debug or learning aid perhaps), or even modify it that way.

GFX to SWF

If you want your changes to be compatible with a certain or the latest game patch, then you'll also have to first merge the appropriate GFX files. For example, to be compatible with game v1.03, you'll first need to extract the GFX files from guiexport.erf, and then from the patch000.erf, patch001.erf, and patch002.erf files found under C:\Program Files\Dragon Age\packages\core\patch. If you want to be compatible with v1.02a, then it should suffice to stop merging after patch001.erf.

Then you want to change the first three bytes for the GFX to CWS and change the file suffix to SWF. Example Python script:

from __future__ import with_statement
# The first line is required if you get the following error when running the python script under 2.5.4/2.5.5
# SyntaxError: invalid syntax - with open(gfxPath, "rb+") as gfxFile:
import glob
import os
import struct
 
for gfxPath in glob.glob('*.gfx'):
	with open(gfxPath, "rb+") as gfxFile:
		print(gfxPath)
		gfxFile.write(struct.pack('3s', "CWS"))
	os.rename(gfxPath, os.path.splitext(gfxPath)[0] + ".swf") 
 
line = input("Press enter to exit.")

Obtain ActionScript Sources

If you're using Sothink's tool, then here's an example of how you would quickly retrieve the ActionScript, if you've converted the GFX into SWFs as mentioned above. Do a multi-file export with all of the SWF files (merged to whichever patch version you wish). Screen shot

Merge ActionScript Classes

It's a good idea to create a single ActionScript library of all the game sources; that way you don't have to repeatedly make the same modifications for every SWF file that uses the same modified classes. As you'll see later when using the SWF injection method, that this doesn't make available all of the classes to every SWF file, but it's a way to avoid redundant editing.

For instance, here's a embarrassingly quick and dirty way of doing it with a Python script:

from __future__ import with_statement
# The first line is required if you get the following error when running the python script under 2.5.4/2.5.5
# SyntaxError: invalid syntax - with open(gfxPath, "rb+") as gfxFile:
# [Yes, this code probably sucks]
 
import shutil
import os
import re
import zlib
 
# Usage: have the batch exported ActionScript from the decompiler (say Sothink) in a dir called export. Then run this 
# script at the same level as the export dir. It will create a C:\commonlib folder with the ActionScript merged into it.
# Left over in export is what you can import into a FlashDevelop project. You'll also want to make
# intrinsics out of export and commonlib, but commlib goes in the global classpath, while
# the appropriate export subfolder goes in the appropriate project classpath.
 
mergedir = r'C:\commonlib'
scriptdict = {}
for dirpath, dirnames, filenames in os.walk('export'):
	print(dirpath)
	for junk in filter (lambda x: not x.endswith(".as"), filenames):
		os.remove(os.path.join(dirpath, junk))
	for script in filter( lambda x : x.endswith(".as"), filenames):
		scriptfile = open(os.path.join(dirpath, script))
		relativedir = re.sub(r'export\\[^\\]+\\?', '', dirpath)
		relativepath = os.path.join(relativedir, script)
		crc32 = zlib.crc32(scriptfile.read())
		scriptfile.close()
		sourcepath = os.path.join(dirpath, script)
		if relativepath in scriptdict:
			lastval = scriptdict[relativepath]
			if lastval['crc32'] == crc32:
				destdir = os.path.join(mergedir, relativedir)
				if os.path.exists(lastval['sourcepath']):
					os.remove(lastval['sourcepath'])
				if not os.path.exists(destdir):
					os.makedirs(destdir)
				#print("move " + sourcepath + " to " + destdir)
				shutil.move(sourcepath, destdir)
			else:
				# There any many *Scene.as files that are the same
				# BUT there's an "emptyish" Scene.as used an as interface as well.
				# So you probably want Scene.as as common, but the other scenes as unique
				# even if they have the same name. Still throw if script is Scene.as but not if it ends with it.
				if not re.search(r'.Scene\.as$', script):
					raise Exception("Same name, different CRC32: " + sourcepath)
		else:
			scriptdict[relativepath] = { 'crc32': crc32, 'sourcepath': sourcepath }
 
# Find unique entries in SharedLibary, GUI, etc. Copy them over to commonlib but print out what we found.
# [Bit of a copy-pasta job here...]
for dirpath, dirnames, filenames in os.walk('export'):
	for script in filter (lambda x: x.endswith(".as"), filenames):
		relativedir = re.sub(r'export\\[^\\]+\\?', '', dirpath)
		if relativedir == '':
			continue
		relativepath = os.path.join(relativedir, script)
		sourcepath = os.path.join(dirpath, script)
		destdir = os.path.join(mergedir, relativedir)
		if not os.path.exists(destdir):
			os.makedirs(destdir)
		print("Unique commonlib: " + sourcepath)
		shutil.move(sourcepath, destdir)
 
# Clean up empty dirs
for dirpath, dirnames, filenames in os.walk('export', topdown=False):
	for d in dirnames:
		try:
			os.rmdir(os.path.join(dirpath, d))
		except OSError:
			pass

Making Modifications

Bugs and getting sources to compile

[ Note: this is mostly just copy and pasted from my post in another forum at the moment. It still needs some reworking to better explain things ]


You can first get the open source FlashDevelop IDE which comes with MTASC and the standard Flash library. If you have the SWFs and the ActionScript *.as files, then what you can do is create an empty ActionScript 2 project, in the project settings turn off "use main entry point" and under the project "injection" tab, point it to the SWF you want to update with your new ActionScript.

You'll also want to update the project classpaths to point to your game's ActionScript library. Just in my experience, there seem to be some properties on some of the standard classes that would normally prevent the script from compiling - but in that case what you can do is modify the so-called intrinsic files to let the compiler know about them. E.g., you can add needed function and var declarations to the intrinsic class interfaces under C:\Program Files\FlashDevelop\Tools\mtasc. The nice thing about ActionScript and intrinsics is that you don't even need to know the values of these "missing" properties or functions if they're basically defined in the runtime environment. E.g., Mouse.LEFT isn't standard Flash, but if you edit Mouse.as and add static var LEFT; to the intrinsic class definition, then you're good to go, as the value gets looked up during execution like a dictionary value (in fact, Mouse["left"] should yield the same result).

Also if you press Ctrl+J you'll get the type explorer where you can right click on a folder (that you've added to your classpath) and select "convert to intrinsic". So this way you only need to compile the actual *.as file you're making changes to and keep the others around as intrinsics. Using the IDE's SWF injection I mentioned above will make it so you don't have to worry about recompiling everything.

You'll probably get a bunch of warnings like "... needs the class <Whatever> which was not compiled" but that's just MTASC not liking how some class names are used outside of what you're compiling like in Flash registerObject calls.

Now the bigger issue is that MTASC is pickier about things than Adobe's compiler. You'll often have to write this.varname or ClassName.varname instead of just varname, and MTASC doesn't like named *nested* functions. So you'll sometimes have to convert function FooBar() { ... } into something like "this.FooBar = function() { };" or "_root.FooBar = function() { };". But also make sure that the variable for the function is a class member and not function-local or it'll just be a register or randomly named variable when compiled, or just not be available in the scope you need it to be.

MTASC is also pickier about the scope of local variables. It may sound weird, but in ActionScript 2.0 a function-local variable is defined for the scope of the function - it doesn't matter if declared in an if-statement or in braces. But the MTASC compiler will enforce that you define the variable in a "proper" scope, so you may need to move where it's declared if you want things to compile. E.g., "if (foo) { var tmp = 1; } else { tmp = 2; }" won't fly with MTASC.

Also it seems like MTASC doesn't like the global function "Array", but doesn't mind if you use the constructor instead. E.g., I've had issues with var ar = Array(...), but var ar = new Array(...) works or you could use the square-bracket notation instead.

Some other stuff... if you want multiple projects, just create the projects all in the same dir (but create folders if you want to separate stuff and adjust the project classpath accordingly). Also, don't necessarily set "Always compile" for the *.as file or you might run into duplicate definition errors - things should get drawn in as needed, but I think you need to set it for classes in the original SWF you wish to update. That might be a little hit and miss at first, but you can always check to make sure your changes got in.

Also, looking at the output in FLASM, MTASC might not generate the most blazing bytecode ever - but neither does Adobe's compiler. Just some things I noticed is that Adobe's will use function argument registers more than MTASC seems to... e.g., "push '_root'; getVariable" vs just a r:_root register.

Another apparent annoyance with MTASC is that it doesn't seem to recognize some global variables (which you can confirm with flasm's output (push 'somevar'; getVariable). Workarounds include _global.someVariable, or sometimes _root.someVariable as appropriate.

One last thing... you may want to organize your *.as files so you combine the ones that are commonly used across different SWF files so you're not always having to edit multiple copies of SomeClass.as for each SWF that uses it.

Also, I noticed a bug in sothink that I emailed them about: doubles are formatted as single-precision floating point. But some other tools, e.g., flare, get the value correctly. You might want to search your *.as files for "E-" and "E+" to look for these misformatted values if you're using it.

EDIT: Here's a bug with Trillix I noticed, but they said it'll be fixed soon: if you have "var foo = this" and if a register is used for 'foo', then Trillix doesn't properly initialize the register... but instead just uses 'this' everywhere else. This causes problems if you needed that variable to choose between using 'this' and possibly another object. E.g., "var obj = this; if (something) { obj = someOtherObject; } someFunc.doIt(obj);" only works as long as "obj" is stored as a variable and not a register, which is up to the original compiler. In reply, they mentioned that you could work around that issue by turning off the "ActionScript 'Recover arguments' names" setting, which makes it a bit more difficult to keep track of function arguments, so it's less than ideal.

Issues displaying custom MovieClip subclasses

Note: this tries to explain some of the issues, and isn't suggesting best practices. This is a bit rambling and should be presented with more concrete examples. Ideally it wouldn't be needed at all and we could use the Scaleform SDK to convert our SWFs into GFX, or we could splice in our own movieclip symbol tags into the GFX file.

The simple issue is this: if it wasn't originally created in the Flash designer (also having a symbol in the Flash library), then you'll have a harder time of showing custom MovieClip objects through ActionScript alone. This would appear to be a bug, but speculatively, it could be for performance reasons.

Using swfmill

This hasn't been thoroughly tested, but swfmill may be a simple way to add you own symbols. Use swfmill to convert the SWF into an XML file, edit it, and then use swfmill to convert it back. It appears to work, and even properly retains the non-standard SWF tags. For custom class behavior, you'll still need to make an Object.registerClass call to associate the library symbol with the class. If you do use this method, one thing to make sure of is that the embedded textures display properly, since they're being referred to using a non-standard SWF tag.

createEmptyMovieClip method

So until splicing in tags becomes more streamlined, or some other method is found, then it may be easier to just create an empty movieclip and then populate the member fields using a mixin from another class or manually, although that's probably still less than ideal for classes that use the onLoad event. Another thing as mentioned below is the possibility of abusing prototype mechanics (which can be somewhat messy) or subclass an existing class to specialize the beha

It would appear that the MovieClip method createEmptyMovieClip is of limited use unless you then load another movieclip (such as an icon image) into it or or do a beginFill ... endFill (fill-code) for a few frames to define its dimensions, filling it for as many frames until, say, its _height property becomes non-zero. It's ugly, but works. Also, if you do a fill on an object in the library, then you actually affect its _xscale and _yscale by making the object stretch to fit into your fill. Ultimately, it'd probably be less of a hassle to alter the SWF file to insert your object. Because of having to rerun the code, you'll probably want to delay having to reload images or other intensive operations. You'll have to rerun the fill-code at least twice for this to work, timing alone doesn't matter as has been determined through trial and error.

attachMovie method

Perhaps a simpler way is to use attachMovie, although you'll have to find a pre-existing movieclip symbol that suites your needs, and if you want to change its default dimensions then you'll have to delay the fill-code for a few (let's say 150 or so since the scene began) frames after the game loads until it actually works. NOTE: Reconfirm this is actually necessary instead of just being able to set the _height/_width/_xscale/_yscale, although you may still need to delay setting those for the change to take effect. This way you shouldn't have to keep rerunning the fill-code as with the above createEmptyMovieClip method. This whole delay business is probably more of an issue in UI elements that are on the HUD instead of selected later from the navbar. You can probably check for when you've waited enough frames by running some simple fill-code on an empty movieclip and checking whether the _height property changes. E.g., the PieCooldown symbol exists in the quickbar.swf file and isn't the worst choice for an empty MovieClip. You can use programs like swix, swfmill, and probably others to find simple objects without much embedded graphics.

private static var EMPTYMC_LINKID:String = "PieCooldown";	
var mc:MovieClip = someClip_mc.attachMovie(EMPTYMC_LINKID, "SomeName_mc", someClip_mc.getNextHighestDepth(), {_x:10, _y:10});
 
// Changing the default dimensions of "PieCooldown"
// Although you can probably just change the _height, _width or scales after the
// needed delay instead, unlike with the createEmptyMovieClip method apparently.
// Also not really necessary if you just want to use it as an invisible container,
// since other items can be positioned outside of it, relative to its local 
// coordinates.
with(mc)
{
   beginFill(0, 0); // alpha is zero.
   moveTo(0, 0);
   lineTo(21, 0);
   lineTo(21, 21);
   lineTo(0, 21);
   endFill();	
}

The game's script sources also come with a MovieUtils class, which has a wrapper around attachMovie. This can be useful to create an instance of a library symbol in the SWF file and use it instead for another purpose.

Other useful workaround would be abusing prototype mechanics of ActionScript to give an already existing class other behavior if desired, and can be switched using a custom argument for the initObject during the attachMovie call. This way you don't have to edit the actual class file, if making it recompile is problematic or time consuming. Although this is a kludge method of doing it.

Also of note is that Object.registerClass or the so-called __Packages trick won't really get you anywhere either. You'll be able to create the MovieClip subclass, but it won't display properly (or just differently) without the above-mentioned tricks.

Creating a subclass of an existing class that's registered with Object.registerClass may also be a convenient way to customize class behavior without having to recompile the base class if you then change the registerClass call to use the subclass.