Converting TTopRPG to the Mac
This page documents my thoughts and progress converting TTopRPG Windows to Mac.
Currently it only works on OS X 10.7 and up. I'll see about reducing the requirement to 10.6.
PaizoCon was great. Now that I've returned, work is hitting me hard with some things I need to get done for a product delivery coming up soon. So, I'm taking a small break from this conversion. I hope to get back to it soon.
Work has been getting hectic, and my PaizoCon trip is coming up. Unfortunately, I wasn't able to complete the client only version of TTop Mac before then. So my focus has shifted toward other things for the time being. I have been polishing up the right click menus and haven't moved on the the chat area or macros yet. I'll get to those after I return from PaizoCon on July 11th.
Wow, it's been this long...
I've been working on the player-side icon modification screen.
It is mostly working the way I want it to, so I'll show it off. This video also demonstrates resource downloading.
I've gotten some of the right click menu functions working, so I'll show them off. Here's a video.
There are a couple of bugs though.
First, the mouse cursor doesn't seem to want to let itself get set to a crosshair when an attack arrow is initiated from the right click menu.
Second, when I start up a right click menu, then move to the upper right and right click to start another, the interpretation gets wacky. So I'll have to chase those down.
Tonight I gave popup menus a heartbeat. I will be using the click to reveal menu, click again to select an option methodology (as opposed to the press to reveal, move over an option, and lift to activate).
I haven't gotten enough working on them yet to show anything off. I should have a video demonstrating it within a few days.
I've started experimenting with popup menus. And alas, they are also blocking the timers.
So I did a little more research. I was able to add the network and map timers to the NSEventTrackingRunLoopMode, so that the timers also fire when a menu is displayed, the window is being dragged, and the scroll bars and sliders are being slid. So that's a relief.
There is also a NSModalPanelRunLoopMode. So if I add my timers to that mode as well, the modal popups probably won't block the timers either. This may make the app easier to complete for me, not having to do everything in sheets. We'll see.
I've added rulers and tuned the mouse behavior a little.
I got some basic player controls working. Thankfully, capturing keypresses on an NSView control is fairly easy. I'm able to detect SHIFT and Command keys to help affect operations, as demonstrated in a new video. The video demonstrates placing and deleting markers, placing flares, using the attack arrow, and using the path plot to move icons during battle. Holding SHIFT, Command, or both while path plotting creates a x2 or x4 difficult terrain calculation for that segment. The more interesting functionality of plot path is that you can start a path, realize you made a mistake, and use the backspace. If you back it up all the way, you can then highlight an icon to tag to use for path plotting.
I also found that NSColor blackColor and whiteColor aren't necessarily guaranteed to have red, green and blue components. I had to chase down the source of an odd exception I was getting trying to retrieve the color components of an NSColor. Explicitly creating a color with the three components got around that problem, but I'll have to watch for that. I also had a problem with the debugger claiming a somewhat deeply nested variable in a switch statement was "out of scope". Giving it a unique name resolved that issue (but it is still annoying when problems like these delay my progress).
The mouse functions and timings of the captions, etc. aren't quite right, and I have to add rulers yet, but this is a good start. The haggling of all the code to handle the situations of mouseovers, caption rendering, etc. while changing states during path plotting could also use some cleaning up, especially to make way for rulers.
Original thoughts start here and go in order of posting.
When I heard that Apple offers their full featured development environment for Mac for free, I figured I would get an iMac and try it out. Of course, my first thought was converting TTopRPG to run on a Mac, since a few of the TTop fans were asking for it.
So, I brought home an iMac, downloaded XCode 4 (their development envrionment), and gave it a whirl.
My first thought was to stare at the screen in a detached sense of confusion. But, I plan to get past that, eventually. It's a different structure than Visual Studio Express 2008 uses, for certain. I won't bother to go into the details.
I will start with a sample form that offers some challenges, like detecting text changes, detecting mouse movement, etc. But before I can even get started on that, I need to learn more about Objective C. So I ordered a few books. They will arrive soon.
In the meantime, I figured I would poke around the Objective C code set and see what kinds of function calls it can offer for rendering, particularly lighting. I did find support for rendering complex paths, but I did not find any union or intersection routines, which TTopRPG's lighting system depends on heavily (its occlusion rendering also depends on graphic set addition and subtraction, which I will have to find an alternative for). So I'm going to have to code a different methodology. I will attempt to code the lighting system around preparing off-screen bitmaps and rendering them on top of the map as masks (one for darkness, the other for shadowy).
My goal is to have light rendering similar to TTopRPG. Like this (note that the left shadow goes straight to darkness even though the pillar is in the bright light):
To start, I'll prepare a rendering mask for darkness and shadowy. The darkness mask will have a color value of 1.0 and the shadowy will have a color value of 0.5 to use as an alpha value. This way, when I finally render them to the final scene with the color black, the darkness will be solid and the shadow will be transparent.
The darkness and shadow masks may not starts as full rectangles of a single color. If the current ambient color is full, but darkness and shadow areas are defined, then their masks will start black and I'll add gray areas to the shadowy mask for the darkness and shadow areas, and I'll add white areas to the darkness mask for darkness areas. For this example, though, I'll assume ambient darkness level, which always creates a full rectangle of shadowy gray and darkness white masks. I'll check for the defined area bounding rectangles to be within the initial masks. If they aren't, I won't render them. If none are rendered, then I'll make sure I keep the corresponding mask flagged as empty so that the appropriate steps are skipped to speed up rendering.
I won't bother showing the starting darkness mask, since it is all white. But the starting shadow mask looks like this:
The idea is to take light, cast shadows from it, and use what's leftover to black out the darkness and shadow masks by rendering black on them. This will prevent those areas of shadow and darkness from rendering on to the final surface.
Using the above example, the light mask to render as black on to the shadow mask looks like this:
To create the mask, I'll establish the rectangle containing the bright light, initialize it to black, then render the bright light source as a white circle, and render the casted normal shadows and casted darkness shadows as black (as long as their bounding rectangles fall within the rectangle for the bright light source).
And the light mask to render as black on to the darkness mask looks like this:
The hard part of calculating this mask will be the two inner arcs on the upper right and right side. Those edges are defined by where the bright portion of light ends and the shadow portion of light begins. I'll probably accomplish this by placing the shadow masks first (the upper, right, and lower shadows), then erasing the inner area with the circle of bright light, then placing the darkness shadows (the one on the left).
So the method for creating the light mask for darkness is:
Establish the bouncing rectangle for the light source out to its limit (bright
and shadowy) as it will actually be rendered
- Initialize the rectangle on the mask to black
- Render the light source out to its limits as white
- Render the casted normal shadows as black (checking for being within the bounding rectangle light)
- Render the bright light source area as white (as mentioned above)
- Render the casted darkness shadows as black (checking for being within the bouncing rectangle of the light)
For every light in my scene, I'll need to create these two masks to render on to the shadow and darkness masks. That way, lights will gradually consume the darkness and shadow masks, effectively lighting the darkness. For this example, I'll just use this single light. After I'm done rendering these lighting masks with black on to the darkness and shadow masks, they'll look like this:
Final shadow mask:
Final Darkness mask:
When I render the shadow mask and darkness mask with black using their mask value as the transparency alpha on to the original scene, I get my goal:
I'll be eager to try this out and see if it works.
Other problems to solve:
My books have yet to arrive. And I was unable to find one at the only remaining large books store in my area.
So, I'll set another goal for when the books arrive, and maybe I'll try to haggle it together with what I can figure out.
The task is:
Also, I have decided that I will start by converting TTopRPG to a client only version of TTopRPG for Mac (including co-GM). This will reduce the work necessary to get a working version out to users, but it won't allow any GM tasks, like hosting campaigns or modification of campaign files, etc.
I also notice that where Visual Studio 2008 Express places all references to controls and delegates within the .h file for a form, Objective C can potentially put its references to controls and delegates in the App delegate, which isn't necessarily associated with the main application form. I'm used to having the controls and delegates and any additional data necessary for a form in the class for the form itself, so organizing the code for TTopRPG Mac may be a challenge. I'm aware that the main goal is to follow the Model-View-Controller pattern, but I doubt I'll get it right the first time. At this point I just want to get a version working that is organized in such a fashion that I know exactly where to go to make changes and the organization of information is intuitive. We'll see.
Finally, my books arrived. Sadly, when I thumbed to the places I thought I would find answers to my primary questions, I was once again left with partial discussions.
So I start hammering away at some test apps today. I started by adding delegate methods to the AppDelegate, which all projects apparently start with (the applicationDidFinishLaunching() method is where you put your primary initializations for your app). I added methods for applicationShouldTerminate (in which I display an NSAlertPanel that asks if the app should terminate, which apparently is NOT the same as clicking the Close button!).
After some trouble I was also able to get a NSWindowDelegate working, and I implemented shouldWindowClose() and windowWillMiniaturize(), but I was having a heck of a time avoiding some odd exception. It turns out I was using Automatic Reference Counting (ARC), and when I allocated the delegate to assign as the delegate for the window, it would deallocate itself at the end of applicationDidFinishLaunching(). When I instead allocated it at the AppDelegate level (by defining a pointer to hold it outside the method), the exception went away. All the examples I saw put the initialization inside the method because they assumed ARC wasn't on. It was very frustrating and consumed most of my effort. But at least I know more now than I did.
I also found that NSRunAlertPanel is the usual call for message boxes, bu we are now encouraged to use NSAlert. Knowing that, I'm going to set up a Tools class that will create the message box with similar arguments as the deprecated macro. That way in the future if they change the recommended way yet again, I'll just change what happens in the Tools method. I also have to figure out how to get an alert sound to play when a message box appears.
Finally, if allocated obejcts automatically deallocate themselves within methods if they are assigned to a local variable or used directly in a message (I tried [_window setDelegate:[[MyWindowDelegate alloc] init]; to no avail), I may have to set up something special for holding on to in-memory bitmaps. Perhaps if I am assigning them to a structure that is not considered local, it will hold on to them. And I wonder if I have to dealloc them explicitly when the structure is deallocated... I guess I'll find out.
CFSocket (Core Foundation Socket) apparently has a lot of what I'm looking for when dealing with netowrk communication. If it doesn't allow me to pull in socket information and inspect it manually, then I may have to resort to the lower level routines on socket.h. I'll need to know how to make sockets linger and check themf or being ready, too.
Apparently the usual Mac behavior when closing a window is that the app itself is still running. In Windows, when you run TTopRPG, each instance is its own application. In Mac, they are all the same app. I should code it so that if you stop the last window of the app, the app quits. This also means that quitting TTopRPG from the main menu or Command Quit will close ALL windows. I hope Mac supports running TTopRPG Mac from different folders, because that is how it is meant to be run and installed.
Update: I created a Tools class with a static method to show an alert that calls with "encouraged" conventions. Also, I learned and Mac programs avoid the Yes/No/Cancel thing and show what will happen when you click each button - it keeps the user from having to read the buttons, which I tend to agree with.
Tonight I coded up the ever popular Random engine from TTopRPG. In the Mac program it is a Singleton that automatically seeds itself on first use. The example code I used for a singleton is simple, and I don't know how thread safe it is, nor does it guard against alloc or init (Mac programmers will know what I'm talking about here). But, it looks like it's working!
Not sure what I'll try next. I may try sending a connect request to a TTopRPG host session and see if it connects.
I got a modal window working, as well as a single non modal panel. Apparently, windows that are not modal but stay in front of the window they are a child of are NSPanels. But I want multiple panels of the same type to appear. I may need to instantiate multiple controller classes and load their .nib files for each instance of the window I want to open. It seems to be a very kludgy way to show multiple instances of a single window type. I'll see.
Also, the modal
window (and possibly modal alert panels) may prevent all events on other windows
and other parts of the app, including timers. This means while a modal window
or alert is up, the rest of the application might not be able to run its network
loop. I'll have to test this. I'm going to want the main window to update its
map and macro areas, player list, etc. as it processes network messages.
Update: After trying out a timer created at the app delegate level, with a modal up, the main form does not render at all, although at the time the modal is closed, the main form immediately refreshes. I'd like it to update live behind the modal - so maybe it can be a panel but not allow the user to access the main form through keyboard or mouse... not sure yet. TTop for Windows definitely updates the main form in all ways when an event comes in while a modal Yes/No dialog is up. Apparently Mac dev tries to avoid Are You Sure dialogs entirely by allowing users to Undo. But that isn't possible in a networked client app - at least, there is no server side providing of an undo to a client. TTop for Mac, therefore, will need those Are You Sures.
Update 2:It turns out, based on when the NSLog posts its text, that the main timer fires but waits for the modal to close before it actually performs any tasks. This means the timer will fire when the modal is closed (which is why the label updates immediately) and then continue with its regular schedule. However, this means that only no actual execution takes place while the modal is up, which could be problematic. I need a foreground modal that captures all keyboard, mouse and tab events, but allows background processes to execute.
Update 3: A modal Sheet still allows the timer to execute, but does not block any panels. TTop Windows blocks "panels" when a modal is up.
It looks like the sheet popup is what I'll need to do. This makes things difficult since I can't just run a routine call and get a response back from it. Instead I have to define a sheet window, populate its text and set up its buttons myself, and accept the button click and interpret its results (perhaps I can put a tag on a button that says something like "delete;macro;id=45" or something.
I noticed that the modal panel still allows acces to the main menu, however. That can't happen either. This will be a challenge. The only floating sheets that might be in conflict are View Visual Aid (read only) and the Info Box dialog (editable - should lock out changes to info boxes or saving campaign, using main menu, etc. while it is up). If I can move the menu to the top of the main form instead of having a man app menu, that would be better. I notice though that certain options in the main menu gray out when a modal sheet is up. This may be good.
Also, I realized today that the Visual Aid Text control saves the text as Rich Text, compliant with Microsoft's Rich Text Format. If this isn't compatible at all with Mac and vice versa, this will prevent full compatibility between the two versions. Mac players connecting to a Windows TTop session might not get pretty text if the app has to strip out all the control codes. Conversely, Visual Aid text created on the Mac might not appear pretty on the Windows clients. Additionally, users with both systems that move their campaign files between each OS to work on campaigns might lose the control codes as one version loads and converts (or strips codes from) one format and saves again in a possible less rich format.
The more I experiment with modal sheets, the more I like them. They act pretty much just like the modal popups I want (preventing access to the main form yet allowing main form events, rendering and timers to fire), except that I can't invoke them and wait for their cycle to end, expecting a value return telling me which button was pressed. If I can find a way to do that, I'll be very happy.
If I can't code them on a wait for return basis, then I'll have to go with an ugly method of having to store interpretive text somewhere that I can retrieve for a given button press, and act on that text (suchas as "deletemacroid=87" or "applyiconchanges:id=331;AC=xxxx\nSpeed=xxxxx\n" etc.)
I'll see what I can do.
But I realize that Action Sheets in iOS programming are very much like these modal sheets. They show buttons, you click one, and you act on the button clicked. They are pretty much the same as modal popups in Windows that let you answer Yes No Cancel with some more customizable features.
I'll need to watch how they affect access of the main app menu, however. I don't want the user selecting anything that interrupts a process that I don't allow to be interrupted in TTop Windows.
Another challenge will be edit dialogs. I don't want them blocking the main form's events either. I don't think I can make them sheets, and I don't want the user modifying the main form while these other dialogs are up. (for instance, deleting the same icon on the map that you care currently modifying in an edit form is not good). Plus this edit form will need its own sheet for confirmations.
I've been trying to capture keydown on an NSTextField. Apparently how an NSTextField works is split between NSTextField and NSTextCell or something. So setting up a subclass that handles firstResponder events doesn't quite work. In my opinion, it should, and should be easy. I saw an article, though, where someone said "you probably shouldn't do it that way." Why not? Because it's hard? Why isn't it easy?
Anyway, I'll try it with an NSTextView, and hopefully I don't have to code a lot to get it to act like a NSTextField.
Every step of this process has shown difficulties. I'm not encouraged about the results, or how Mac development shoehorns you into certain UI designs. I'm beginning to see why there is more development for Windows machines. There may be a fair number of games on the Mac, but game developers write their own UIs contained within the rendering engine, so they don't have to deal with this ridiculousness.
Anyway, maybe for now I should just not include the ability to recall previous text entered, and just make sure I can capture textchanged so I can handle the "I'm typing" indicator, prevent carriange returns and non UTF-8 characters from being pasted into chat entry, and handle the Enter key to clear the text and post it to the chat screen... I'll save the text recall for later. I have too many other problems to solve to let this little thing stop me. Hopefully there aren't too many more compromises as I go.
I tried out a simple connect form sheet and was able to ping a TTop Windows session today. I should try to get the chat text area working so I can spit out text messages for debugging. I'll also try to send login info so that the TTop Windows session thinks the client is a legitimate TTop user and attempts to send down all the map data.
I send a login packet and successfully got TTop Windows to recognize a Mac login. However, the structure of the packet I sent had to be changed slightly. I had to change the logns to ints because it was generating a packet size of 136 instead of 124. This means I'll have to change the sizes of all the structs in order to get them to communicate properly.
It was recommended to me that I shoot for a build target of 10.6, 32 bit, to reach the widest audience.
This means I can't use ARC, because that is 64 bit only. And I'm not going to maintain two binaries.
Apparently 32 bit apps also can't use synthesize without actual declared space for their variables. So, a little extra work there. AND I'll have to deallocate everything I allocate manually.
Better to find this out earlier than later.
If I can find a way to make this easier on me should I want to build it interchangeably between 32 bit and 64 bit and using ARC or not, all the better. But for now, I'm not too enthused about that possibility.
It may be that I can just depend on users to have 64 bit machines, since the last 32 bit Mac was manufactured in 2006. However, to use ARC, I need to build using the 10.7 SDK. I can still deploy for 10.6, but the code should check for the OS's capabilities before deciding to run commands. I don't like that at all. This is so different from Windows programming. Yeesh.
Today I got hte Mac client to absorb and sent confirmation packets for all information being sent down from the host. I even got the Mac client to send keepalive packets at 7 second intervals.
It isn't an entirely robust set of network calls, but at least I was able to prove the two platforms can communicate (still worried about how RTF visual aids will appear, but I'll get to that).
Also, apparently on a Mac, if you try to write to a socket that has been closed by the host, you can get a SIGPIPE error that crashes the app. There can be timing problems where the host might forcibly disconnect the client yet the client is trying to send something back, and this error occurs. For now I handled it with a small chunk of code that shuts off the SIGPIPE notification on the socket. I haven't run into this problem on Windows, possibly because in the final Windows code, I check for the socket to be ready before attempting to do so. But even that is no guarantee the socket is good between the check and the write.
In any case, good info there.
Next I might start to tackle mouse events and object dragging on the map area.
I've been converting my Network class over to Objective C. This isn't fun. I have to rearrange every method definition and member declaration. I might try looking into how easily I can intermix C++ with Objective C. The books I've read so far don't discuss doing so. But if I can do it, it might save me some time.
At least the profiler isn't telling me my allocated objects aren't being deallocated automatically by ARC.
Still busy converting the network classes to Objective C. The behavior of the XCode editor is not helping. When I page down or up I expect the cursor to move with the screen. It doesn't. Home and End also do not move you to the beginning and end of a line. It's very annoying. If this is Apple's way of getting me to love some proprietary device of theirs, it isn't working!
I'm also unclear when I need to define properties for an interface. If I can avoid doing so, I'd much prefer to. After I'm done setting up the network classes, hopefully I'll have better information along those lines.
Finally, some functions are different between Windows and Mac - namely, Windows uses closesocket() where Mac uses close(). ioctlsocket is used in Windows to set a socket to blocking or non-blocking. Can't remember right now what Mac uses, but what I did works...
The Network class is now converted. Now I'm integrating it into the main app delegate and timer loop. It took some time to figure out that the select() method, which is used to determine if a socket is ready for writing, actually uses the first parameter explicitly in Mac development. In Windows, I set the first parameter to zero and it worked just fine. My use of select() can probably be refined a bit, but for now I'm just interested in getting things working.
I hope to have the class integrated by the end of today so I can start coding the real guts of the application.
Also, although this hasn't happened yet in Windows TTop, I read that it's possible that even though a socket is ready for writing, not all of your packet may be shoved into it when you send(). TTop's max packet size is 8100 bytes - that's probably why it hasn't been a problem for me yet. Mac TTop will be the same way, but I'll have to keep an eye on it. If an incomplete packet is sent, that will garble things badly and also likely result in a bad packet validation.
I'd like to get a USB modem and try dial-up with the iMac so I can really put its connectivity through its paces - especially if I proxy host through the dialup. That will be a really nice test.
This weekend I started setting up the data-holding classes. I'm keeping the map and campaign data separate from the UI data. Map data includes icons, occlusions, etc. UI data holds map scroll position, zoom, view as PC, etc. This way I can keep all the data elements nicely separated and see how that helps me put everything together.
To that end, the iMac is now receiving all occlusion data events from the host. So after the iMac connects to a live session, it's holding occlusion data. This will be a good first thing to attempt to render. I may or may not try to get scrolling working. If I can just get something rendering, I'll be happy.
Also, important note: If you have a method that is prototyped to return a pointer, but it doesn't actually return a pointer, and ARC attempts to dealloc the "pointer" that was returned, you're ripe for SIGABRT errors. So return a meaningful pointer!
After some mild trials, I finally got occlusions rendering (left side is the Windows version, right side is the Mac version):
I had some rude awakenings getting this working, however. I used the same strategy for lighting as demonstrated in the upper portion of this page. But when it came to converting the greyscale image into an alpha map, I couldn't find ANY routines in XCode that would allow me to do that. So, I created an NSBitmapMapRep of the NSImage I rendered the grayscale to and moved the values around myself.
And THEN... when it came to compositing the alpha map with what was already in the NSView on the main form, I couldn't find ANY compositing method that gave me the result I needed (that is, R = S*Sa + D*(1 - Sa)). So, I grabbed an NSBitmapRep of what was already in the view, calculated the results myself, and rendered the final shot back into the view. What you see above is the completed image.
The loop to calculate the values is surprisingly fast. But the parts of the method that convert the image are slow. Currently, on my iMac, this scene renders at around 60 fps, a little slower than on the Windows machine - and I haven't even added the map graphics, icons, etc. I'll see about speeding it up. Next up, though, is lighting. I want to see just how badly this bitmap compositing slows things down and decide if I want to go with OpenGL.
Eureka! I made a few important discoveries.
It turns out NSImages in XCode are automatically stored premultiplied (that is, their color values are color * alpha already). This means the R = S*Sa + D*(1-Sa) really just needs to be R = S + D *(1-Sa). And that is the NSCompositeSourceOver composite method! Then I just needed to find a way to get NSBezierPath to render with NSCompositeCopy (R = S) to the masks I prepare (it uses NSComposoiteSourceOver by default, but I need to replace chunks of the in memory image, not blend portions, so I need NSCompositeCopy).
Then came the next eureka. This morning I found a chunk of code that lets you modify the current context and set the composite method to anything you like for rendering with NSBezierPath. Afterwards, you restore the old context to keep the stack clean.
Doing this, I am able to prepare the in-memory images and overlay them exactly as I need to the destination view without any converting of NSImage to bitmap data or parsing through arrays. And the render time for a scene went from 86ms to 15ms! Excellent! Looks like I don't need to go with OpenGL after all!
TTop Mac is now receiving light information from the host. With it, I rendered some default lights in their proper locations. I'll need to add things like determining if the light is even visible based on the scroll position of the map, and use the grid size to determine the proper light radius to render.
I also discovered that a lot of context switching can cause a lot of slowdown. So I'll have to be careful about how often I do that when rendering lights. It may be tricky, because I have to render a light mask, then render to the darkness and shadow masks, then prepare another light to render. I can't render multiple lights to the same light mask, because each light has to be prepared and subatracted from separately. There may be something about the context switching I don't under stand yet. I'll see if I can maxomize the rendering.
As an example, by context switching too much while rendering lights, the frame rate dropped to around 12 fps. But reducing the context switching raised it to 70 fps. Quite a difference.
The context switch and rendering mode only seems to last for as long as I have one image set as the focus. Once I switch to a different image, the rendering mode is lost. Perhaps I can nest a context grab, mode set, lock darkness mask, lock light mask, prepare light, unlock image, render to darkness mask and repeat, then unlock the darkness layer, restore the context, then render the darkness to the final view. And repeat all that again with the shadow mask. We'll see.
After inserting some timing calls, I found that the greatest slowdown of all comes from calling lockFocus. If I can minimize that, I'll get the speed I need, because compared to that routine, all the others pale in how much time they cost (the others reported 0ms cost consistently).
There are a couple things going my way, though. lockFocus works in stacks. So I can lockFocus on a large canvas, then lockFocus on a smaller canvas to prepare lights, then unlockFocus and immediately render the prepared lights to the larger canvas.
Another strategy is to prepare a larger canvas, prepare multiple lights on it in sections, then unlockFocus and render the lights prepared in batch on to the final view. Each light would need to be in its own clipped area, however. Each light has to be prepared in its own distinct universe for lighting to come out correctly.
Clearly, I have some work to do in this area. But at least I have somewhat of a plan. In the meantime, I may discover a better way to do all of this.
At long last. Lighting is functional. I still have to handle floating darkvision light sources and 4e square light sources, but I'm happy with the result. Compare! Again, left side is TTop Windows, right side is TTop Mac.
If you look closely, you may notice a little line of antialiasing on the Mac side just above the full light shining out of the cave. I also see this occur when a full light source exists inside a spiral. I'll have to handle that as well.
I also should put some timers in and see how long it takes to render the shadow casts. I'm expecting not long. Perhaps I should load up a larger map with more complex shadow arrangements and check the timing. Hopefully the only really expensive operations is calling lockFocus. I'm still trying to find a way to reduce its cost.
I finally managed to chase down that antialiasing bug. I needed to round the submask origin to an integer, render the submask to the darkness and shadow layers using a rounded upper left location, AND turn off antialiasing for all light-related rendering.
The antialiasing line you see above lies between point n-1 and point 0 of a shadow wall loop. The code builds each shadow cast separately at that boundary instead of combining them into one. Ultimately I think XCode tries a little too hard to anti alias and make things pretty, so two adjacent rendered filled bezier paths don't fill the border between them properly. So I made sure everything was lined up pixel wise and shut off antialiasing, and the problem is gone. For now.
I still have to handle square light sources for 4e and the floating darkvision light source, and support PFRFG combining two shadow light sources to full - that one will be a bit tricky.
I've been making more progress. I'm rendering the attack arrow, sketch lines and area effects now. Next I'll work on markers and the plotted path with square counting, etc. I'll post a comparison pic when I get those working. Then comes icons, graphic downloading, binary file archiving, etc.
I haven't gotten the plot path drawing yet, but I spent so much time getting markers and their captions working that I wanted to take a break and show off the results.
So here are area effects, sketch lines, the attack arrow and markers. TTop Windows on the left, Mac on the right.
Still plenty to do.
I managed to build a mostly non functional version of the application and asked a user to try it on his 10.7 OS Mac. It worked. However, he placed it in his Application folder, where it created all the usual TTop folders when it executed (portraits, campaigns, etc.). He didn't like this.
I explained that TTop for Mac will work much like it does for Windows. All its subfolders will be contained with the app location folder, meaning you can "install" the application in multiple places on your machine to perform testing (run one as a host, another as a client, etc.). I envision users should place TTop Mac within a folder on their desktop or on a specific area of their hard disk (requiring that Finder be configured to show hard disk devices to maek things easier to get to).
I'll be collecting more information about this and get a feel for what typical Mac users will want out of how these folders are created. I don't want to add complexity by having the app ask where you want to folders created, and I really want the folders to be contained at the app location in a way that doesn't obtrusively create folders in a place you don't want them.
On the Mac, the deployed app just looks like an executable to run. When I copied this file structure to Windows, it looked like a folder structure. So I could move the folders inside the main application folder, but how would you, the user, then get to them? I ask because the Mac interface treats the folder like an executable and doesn't let you open it up to explore the files within, from what I've seen.
So, two goals:
- Keep the folders with the executable somehow so that users can install the app in multiple places if they desire for testing multiple instances of it together
- Make the folders accessible from Mac explorer so users can maintain their own backups, place graphics directly if they wish, etc.
Update: It's easy enough for a user to dig into the deeper applciation folders by right clicking the app and choosing "Show Package Contents". I've created a "userdata" folder inside that will hold everything the app uses. Now I just need to make sure grabbing an application update manually and pasting it in doesn't destroy what's there (it would be a shame for someone to lose all their data because they didn't understand that Replace means the Mac will delete everything first).
I just tried a Paste, and the asked if I wanted to "Keep Both" or "Replace". "Replace" destroys what's there (users would lose their data), and "Keep Both" just renames the pasted application and places it separately (which gives users the update but doesn't include their data with it. Also not good).
Sigh. I hope users don't end up destroying all their TTop data. I better put up some serious warnings about this.
Path plot is working, and properly counting squares/calculating distances for all counting modes and grid types. I had some trouble getting the ints and floats to behave together in the same way that they do in MS Visual C++. That's what I get for not explicitly casting every value in an equation instead of letting the code decide how it rounds or doesn't round mixed types. All things considered, I'm probably lucky I got the Mac code to calculate things properly without haveing to go back and explicitly cast things in the Windows version.
Next up: Maps and Icon rendering! But I think I'll take a break for my Memorial Day Monday. Hope yours is worthwile.
I spent this evening adding support for arc lights - a small detail I missed.
Also, after speaking witha couple of regular Mac users and developers, I realized how horrible bad wrong it would be for me to place the user data pool instead the app folder. Users would inevitably paste a new app over the top and lose their user data and would be sorely irritated at me. So, I won't do that.
Instead, I'll ask the user on startup where they would like their user data pool to be, and give them the option to change it later. The downside of this is that all installations of TTop Mac on the same machine, I presume, will use the same data pool. This means you can run at most 1 instance for hosting and 1 instance for connecting as a client. That's it.
Placing the user data pool inside the app folder would allow you to install as many as you like and run them from wherever you like, but not if it is going to be an app setting.
So for now, expect TTop Mac to support 1 host and 1 client instance running on the same machine, max.
I'll have my own personal version that forcibly places the user data pool inside the app folder without using an app setting so that I can do some serious testing with many clients on the same machine.
Plot paths, rulers, and the map grid. I'm not rendering hex grids yet. You can, however, see an arc light source shining inside the cave.
Hex grids are now rendering. I think I can finally start focusing on rendering JPGs. I'll start with the blank map paper image for simplicity so I don't have to receive and assemble communicated files yet.
I also realized recently that selecting a dropdown menu from the main upper menu actually blocks the timers from firing. So if someone selects a menu and stares at it, they will be disconnected from the session because keepalives won't be sent. I hope the right click context popups don't do that. There has to be a way to tell the app not to block the timers when the menu is up. That's annoying. I want to avoid multithreading at this point.
I've also successfully rendered the paper-like blank map background image as a fill pattern. However, it is drawing as if the texture origin is at the upper left of the view, when I want it the origin to be the upper left of the map rectangle, so that the image doesn't look like it's swimming around as the map is scrolled or zoomed in and out. I think an affine transform will help, but I want to transform the image fill pattern, NOT the rectangle I want to draw with it. Not sure how to accomplish this just yet. Update: Got it. NSGraphicsContext lets me set the origin for the image fill pattern. Next!
It took me a while to get the blank map graphic properly lined up, because I'm using it as a fill pattern and have to offset its starting point at the origin of the map in the view. setPatternPhase gives me that ability, but the original fill point is based on the origin of the ENTIRE WINDOW, not the view! It took me a while to realize that, and I think that's silly. Why would anyone base their rendering origin on the Window and not the sub-view they are in?
Oh well. Finally, after some proper adjustments and tweaking, the blank map paper jpg is lined up.
I'm currently working on downlaoding file graphics into a binary archive.
In the meantime, I was able to test on a 10.6.8 platform and got a crash report about NSTableCellView, which is a 10.7 control. That's in the player list and easy enough to remove. However, all the control position constraints are also 10.7 or later, so my automatic anchoring will also not work. It was painful coding all of that on the Windows side, and I got that done before I realized VS 2008 had anchoring on controls. So I figured I would take advantage of that in XCode and save myself the bother. Now it looks like I have to bother anyway.
This is tempting me to just target 10.7.
Update: I'm finally rendering a jpeg map. It's starting to get very difficult to tell Windows and Mac apart.
I did discover another possible compatibility problem, however. Along with the RTF data for text-only visual aids, I'm also writing .NET binary data to the local archive client storage. It would be safest to just clear the archives if you move your client data between Mac and Windows. It would be nice if I could find an automatic way of telling the different between the two. I'll have to do compatibility testing and see if I can determine a way to tell them apart. Perhaps I can just write "Windows" or "Mac" at the front and let each version wipe them out if they see a version from another system.
Chances are the Windows clients will have their archives wiped, so you'll have to download everything again when I standardize. I also found an error in my data encryption for the archive that will need correction - which also requires that all Windows client archives be wiped. But that won't happen until I release the first Mac client. (To be clear - you'll only lose the graphic and binary data you downloaded from the host - you won't lose your character macros, and GMs won't lose any of their campaign files or data).
Update 2: FINALLY archiving is working (archiving is what happens when clients download from the host so that they don't need to request the files again on the next connect).
I realized the rendering is SO FAST on the Mac that I don't need to implement Map Chunking there! It's only necessary for Windows machines!
Time for a comparison pic.
I learned the difference between an NSImageRep and the NSImage it resides inside. Scaling an image to provide a colorFill pattern will be easy now - I can set any size on the NSImage I want and it doesn't destroy the encased image representation in any way.
Oh, and icons!
I've made some progress drawing MapBuilder shapes. But as you can see, I'm not filling them with textures yet. Also, you should notice a couple chair images on the left that are not yet rendered at all on the right. I'll handle that tomorrow. It's been a long day.
Well, I'm rendering shape textures now. But I got stuck on a couple of things:
1. I realized
my setPatternPhase coordinate calculations were off. I finally managed to figure
out the correct formula.
2. Even after the origin of the pattern was set properly, I found that the image pattern fill refuses to antialias with the origin on a floating point pixel boundary, so as the zoom changes, the shape textures swim around a little. Being the perfectionist that I am, I won't be happy until I resolve that.
Because of the time it took to investigate these two issues, I didn't get to rendering the chair images yet. Tomorrow.
Update: when I tried distinct zoom increments of a factor of 1.5 (which Windows TTop operates on), the texture swimming wasn't noticable at all. And there is no swimming at all when scrolling at a constant zoom setting. So I may just leave it as it is.
Oh, and now that I'm rendering shapes with borders, I can post this picture of the lighting test, which matches the lighting goal I posted at the beginning of this page:
I happened upon a small discussion that warned that NSImage::setSize considers points, not pixels. After digging further, I found that some displays may assume 144dpi or some other value instead of 72dpi which can supposedly affect the interpretation of pixel location or image rendering size. So, I changed a texture to 300dpi and checked how it rendered. It rendered the same as before. This may be because my display is a 72dpi display and NSImage points assumes a 72 dpi.
The short of it is, I need a situation that causes things to display differently than my intent, and I'm not currently seeing it. I hope that others that have display setups I'm not aware of don't have problems with how I'm rendering and interpreting things with TTop Mac. I'm trying to make it identical with how the app it renders and operates in Windows to keep things consistent.
And last but not least, rotated, scaled images are rendering properly. I call that pretty darn close to good enough. Time to start working on mouse controls.
Today I got the zoom slider, scroll bars, map drag, and mouse stroke zoom working.
TTopRPG Mac will NOT support scrolling the map by stroking the mouse. You'll have to grab the map and drag it around.
I'll also have to provide a mouse wheel sensitivity setting so that users with an actual wheel mouse don't get carpal tunnel trying to zoom the map in and out.
Here's a video to show off these functions. TTop users will be used to seeing what's happening. Those of you new to TTop might need to watch it a couple times to figure out what I'm demonstrating (scroll bars and zoom slider staying in sync with mouse dragging and zooming the map, etc.). The scroll bars don't have page increments or arrow increments yet. I also don't like that the scroll bar and zoom slider don't update the map live - the map only redraws when you release the control. I'll try to fix that.
I found yet another situation where I had to had some 10.7-only position constraints to controls. If I can't get around that, TTop Mac will be stuck supporting 10.7+ only.
Also, after trying out lighting on a zoomed-in map, I know I'll definitely have to speed up the lighting rendering routines. I have a few ideas, I just haven't implemented them yet.
Today I got mouseMove working, allowing mouse-over. The app also detects the mouse entering and exiting the map area - which is nice, because then I can code how the mouse interacts with the map much in the same way that it works in Windows. The less to redesign, the better. I can use the same methdology to determine entry of the chat area and the macros.
I also got mouse-over working, so markers and icons are being highlighted, along with showing the name, status and stats of highlighted icons.
I reduced the size of the map view font a little because I suspect the larger font would have been too large to support a compact sized icon portrait list that can also show (-998)/(999) under each portrait comfortably.
I noticed in the Windows version that even though mousing over an icon shows its name and status, all of that disappears when you grab it and drag it. I thought that was kind of snazzy - I forget the little polishes I put in the Windows version.
Icons are now draggable and report their new positions to the GM machine. I'm not rendering drag paths yet. I'll work on that tomorrow.
I encountered my first instance of having to move things around in classes as I find that the same method needs to be accessed from two different classes. Hopefully that doesn't occur too much - I'm just building this as I go. (In this instance, the map timer sent the dragged icon position to the host, and in the map view, the mouse up routine also sends the icon's final position to the host - same method. At first I put it where the timer was, and moved it inside the map view class, since the map view class doesn't have access to where the timer is - perhaps that method call should be in the icon class...).
Ok, I moved the update position request to the icon class.
I also got drag path working, and with icon position updates, I am moving attached lighting sources with them and recalculating their shadow casts. Video here!
I've been slowing down a little lately. Things are hectic at work and I need to rest a bit.
However, I did get condition graphics, the unconscious X decoration, and PC icon portraits rendered: