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.