Sunday, February 27, 2011

Replacing LIVE functionality in XNA 4.0

Although the XNA framework supports multiple platforms, including Xbox and Windows on PC, there are parts that are not available on Windows PC.

One such part is dedicated to interacting with Xbox LIVE, such as gamer profile information, permissions, avatars. (Actually, these parts are available to developers, but not to users). That can be a problem if you want to release your Xbox game to PC. Even if you don't intend a commercial release and only want to distribute your game to your friends and family, they typically don't want to install the full SDK just so that they can run your game.

Before XNA Game Studio 4.0, if you wanted to support the PC platform, you had to write an abstraction layer over these restricted features, or sprinkle your code with #ifdef. There was no way you could replace the implementation of restricted features with your own as the default implementation was embedded into the same DLLs as non-restricted parts of XNA (and you don't want to reimplement these).

The release of XNA Game Studio 4.0 saw a clean-up of the organization of all assemblies. All restricted features are now confined to specific DLLs, which you can replace with your own.

I have started work on my own implementation. My level of ambition is not pretty low, I only intend to achieve the kind of coverage I need for my own code to compile and run. In any case, it's available on bitbucket, so feel free to grab it, modify and extend it to your liking.

I have started with Microsoft.Xna.Framework.Storage, as this is what I use in the code I mentioned in a previous post about safe IO on Xbox. Here are some random observations and small things I learned in the process.

References to record types can't be null. The XNA framework is primarily meant for C#, and it uses null (to the delight of generations of evil peer-reviewers). I initially used records for StorageDevice and StorageContainer, and I had to change to classes when I tried my replacement with existing game code. Even F# classes aren't null-friendly by default, you have to mark classes with a special attribute to change that:

[<AllowNullLiteral>]
type StorageDevice =
    class
        val drive : DriveInfo
        val path : string
        new (d, p) = { drive = d; path = p }
    end 

Separating methods from data in class declarations help avoid circular dependencies. StorageDevice depends on StorageContainer because a method of StorageDevice is used to create instances of StorageContainer. StorageContainer depends on StorageDevice because each StorageContainer is owned by a StorageDevice.
It would seem there is a circular dependency, which in F# must be dealt with explicitly. A simple solution consists of declaring the types as mutually dependent (using "and" instead of "type" for the second type). Another one is to move the definition of StorageDevice.BeginOpenContainer and StorageDevice.EndOpenContainer after StorageContainer.

[<AllowNullLiteral>]
type StorageDevice =
    class
        val drive : DriveInfo
        val path : string
        new (d, p) = { drive = d; path = p }
    end


[<AllowNullLiteral>]
type StorageContainer =
    class
        val name : string
        val device : StorageDevice
        val mutable is_disposed : bool
    
        new(name, dev) = { name = name; device = dev; is_disposed = false }
    end
with interface IDisposable with
        member this.Dispose() = this.is_disposed <- true

type StorageDevice with
    member this.BeginOpenContainer(displayName : string, cb : AsyncCallback, state : Object) =
        let f = new Task<_>(StorageInternals.openContainer(this, displayName), state)

        StorageInternals.doThenMaybeCallback(f, cb)

        f :> IAsyncResult

    member this.EndOpenContainer(result : IAsyncResult) =
        let task = result :?> Task<StorageContainer>
        task.Wait()
        task.Result


Not everything needs to be in a module. By default, each new F# source file starts with
module Module1.fs

You can use "namespace" instead:
namespace Microsoft.Xna.Framework.Storage

If you need F# functions at the root-level (i.e. outside a class or a method), you can start a module right in the middle of your source file:

type StorageContainer =
    class
        val name : string
        val device : StorageDevice
        val mutable is_disposed : bool
    
        new(name, dev) = { name = name; device = dev; is_disposed = false }
    end
with interface IDisposable with
        member this.Dispose() = this.is_disposed <- true


module StorageInternals =
    let checkConnected(dev : StorageDevice) =
        if not dev.drive.IsReady then raise(StorageDeviceNotConnectedException())

type StorageContainer with

    member this.CreateDirectory(dir) =
        StorageInternals.checkConnected(this.device)
        let path = Path.Combine(this.device.path, this.name, dir)
        Directory.CreateDirectory(path) |> ignore 

Windows.Forms.FolderBrowserDialog isn't very useful. I was initially using it to let the user select the StorageDevice and StorageContainer, but it turned out I can't use it in the main thread because it's modal, and I can't use in a separate thread because I can't. Actually, that's not true, I can use a separate thread if I create it myself using some special options. It the end, that's not what I did. I just rolled up my own browser using a TreeView.



How do I create an IAsyncResult? There may be many different solutions, but an easy one that work for me was to use a Task (they implement IAsyncResult).

let f = new Task<_>(StorageInternals.openContainer(this, displayName), state)

F# Asyncs are nice for GUI. The following code shows the browser on the right thread (assuming you can get hold of the Threading.SynchronizationContext of the GUI thread), and disposes of the dialog nicely.

let openContainer(dev : StorageDevice, name) _ =
        let dialog = new FolderBrowser(sprintf "Select container directory [%s]" name, Some dev.path)
        async {
            try
                do! Async.SwitchToContext(gui_context)
                dialog.Show()
                let! _ = Async.AwaitEvent(dialog.Closed)
                return
                    match dialog.Selected with
                    | Some path ->
                        if not(Directory.Exists(path)) then
                            Directory.CreateDirectory(path) |> ignore
                        new StorageContainer(name, dev)
                    | None ->
                        null
            finally
                dialog.Dispose()
        }
        |> Async.RunSynchronously

Playing and testing with your library code is easy, just use an F# script. No need for an extra project building an executable.



That's all for this article. Lots of small interesting details packed in little code.

No comments: