diff --git a/.gitignore b/.gitignore
index 13d7896..02401c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.vs/
packages/
/TODO
+mybuild.bat
diff --git a/BNA/BNA.csproj b/BNA/BNA.csproj
index f1935ed..6998a08 100644
--- a/BNA/BNA.csproj
+++ b/BNA/BNA.csproj
@@ -14,6 +14,7 @@
+
diff --git a/BNA/FNA.filter b/BNA/FNA.filter
index 35438da..0cf5226 100644
--- a/BNA/FNA.filter
+++ b/BNA/FNA.filter
@@ -102,3 +102,11 @@
*.Content.*
*.Storage.*
+
+*.Audio.AudioChannels
+*.Audio.SoundState
+*.Audio.*Exception
+
+*.Media.MediaQueue
+*.Media.MediaState
+*.Media.SongCollection
diff --git a/BNA/lzxdecoder.license b/BNA/lzxdecoder.license
new file mode 100644
index 0000000..c570d11
--- /dev/null
+++ b/BNA/lzxdecoder.license
@@ -0,0 +1,29 @@
+LzxDecoder.cs was derived from libmspack
+Copyright 2003-2004 Stuart Caie
+Copyright 2011 Ali Scissons
+
+The LZX method was created by Jonathan Forbes and Tomi Poutanen, adapted
+by Microsoft Corporation.
+
+This source file is Dual licensed; meaning the end-user of this source file
+may redistribute/modify it under the LGPL 2.1 or MS-PL licenses.
+
+About
+-----
+This derived work is recognized by Stuart Caie and is authorized to adapt
+any changes made to lzxd.c in his libmspack library and will still retain
+this dual licensing scheme. Big thanks to Stuart Caie!
+
+This file is a pure C# port of the lzxd.c file from libmspack, with minor
+changes towards the decompression of XNB files. The original decompression
+software of LZX encoded data was written by Suart Caie in his
+libmspack/cabextract projects, which can be located at
+http://http://www.cabextract.org.uk/
+
+GNU Lesser General Public License, Version 2.1
+----------------------------------------------
+https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+
+Microsoft Public License
+------------------------
+http://www.opensource.org/licenses/ms-pl.html
diff --git a/BNA/monoxna.license b/BNA/monoxna.license
new file mode 100644
index 0000000..185aa38
--- /dev/null
+++ b/BNA/monoxna.license
@@ -0,0 +1,22 @@
+MIT License
+Copyright 2006 The Mono.Xna Team
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/BNA/src/Effect.cs b/BNA/src/Effect.cs
index d661568..276abf0 100644
--- a/BNA/src/Effect.cs
+++ b/BNA/src/Effect.cs
@@ -22,7 +22,6 @@ namespace Microsoft.Xna.Framework.Graphics
}
-
//
// CreateProgram
//
diff --git a/BNA/src/FNA3D_Rt.cs b/BNA/src/FNA3D_Rt.cs
index 959cc58..dc07b0b 100644
--- a/BNA/src/FNA3D_Rt.cs
+++ b/BNA/src/FNA3D_Rt.cs
@@ -37,7 +37,8 @@ namespace Microsoft.Xna.Framework.Graphics
if ((state.TargetFramebuffer = id[0]) == 0)
return;
}
- GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.TargetFramebuffer);
+ GLES20.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER,
+ state.TargetFramebuffer);
int attachmentIndex = GLES20.GL_COLOR_ATTACHMENT0;
foreach (var renderTarget in renderTargetsCopy)
@@ -48,7 +49,7 @@ namespace Microsoft.Xna.Framework.Graphics
// from FNA3D_GetMaxMultiSampleCount, which we never do
throw new PlatformNotSupportedException();
/*GLES20.glFramebufferRenderbuffer(
- GLES20.GL_FRAMEBUFFER, attachmentIndex,
+ GLES20.GL_DRAW_FRAMEBUFFER, attachmentIndex,
GLES20.GL_RENDERBUFFER, (int) renderTarget.colorBuffer);*/
}
else
@@ -60,7 +61,7 @@ namespace Microsoft.Xna.Framework.Graphics
+ renderTarget.data2;
}
GLES20.glFramebufferTexture2D(
- GLES20.GL_FRAMEBUFFER, attachmentIndex,
+ GLES30.GL_DRAW_FRAMEBUFFER, attachmentIndex,
attachmentType, (int) renderTarget.texture, 0);
}
attachmentIndex++;
@@ -71,12 +72,12 @@ namespace Microsoft.Xna.Framework.Graphics
while (attachmentIndex < lastAttachmentPlusOne)
{
GLES20.glFramebufferRenderbuffer(
- GLES20.GL_FRAMEBUFFER, attachmentIndex++,
+ GLES30.GL_DRAW_FRAMEBUFFER, attachmentIndex++,
GLES20.GL_RENDERBUFFER, 0);
}
GLES20.glFramebufferRenderbuffer(
- GLES20.GL_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT,
+ GLES30.GL_DRAW_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT,
GLES20.GL_RENDERBUFFER, (int) depthStencilBuffer);
state.RenderToTexture = true;
@@ -100,7 +101,7 @@ namespace Microsoft.Xna.Framework.Graphics
var renderer = Renderer.Get(device);
renderer.Send( () =>
{
- GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+ GLES20.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER, 0);
var state = (State) renderer.UserData;
state.RenderToTexture = false;
@@ -138,7 +139,10 @@ namespace Microsoft.Xna.Framework.Graphics
GLES20.glBindTexture(attachmentType, texture);
GLES20.glGenerateMipmap(attachmentType);
- GLES20.glBindTexture(attachmentType, 0);
+
+ var state = (State) renderer.UserData;
+ if (state.TextureOnLastUnit != 0)
+ GLES20.glBindTexture(attachmentType, state.TextureOnLastUnit);
});
}
}
@@ -204,6 +208,75 @@ namespace Microsoft.Xna.Framework.Graphics
return ((State) Renderer.Get(graphicsDevice.GLDevice).UserData).RenderToTexture;
}
+ //
+ // Get Texture Data
+ //
+
+ private static void GetTextureData(Renderer renderer, int textureId,
+ int x, int y, int w, int h, int level,
+ object dataObject, int dataOffset, int dataLength)
+ {
+ java.nio.Buffer buffer = dataObject switch
+ {
+ sbyte[] byteArray =>
+ java.nio.ByteBuffer.wrap(byteArray, dataOffset, dataLength),
+
+ int[] intArray =>
+ java.nio.IntBuffer.wrap(intArray, dataOffset / 4, dataLength / 4),
+
+ _ => throw new ArgumentException(dataObject?.GetType().ToString()),
+ };
+
+ renderer.Send( () =>
+ {
+ var state = (State) renderer.UserData;
+ if (state.SourceFramebuffer == 0)
+ {
+ var id = new int[1];
+ GLES20.glGenFramebuffers(1, id, 0);
+ if ((state.SourceFramebuffer = id[0]) == 0)
+ return;
+ }
+
+ var config = state.TextureConfigs[textureId];
+ if (config[1] != (int) SurfaceFormat.Color)
+ {
+ throw new NotSupportedException(
+ ((SurfaceFormat) config[1]).ToString());
+ }
+
+ GLES20.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER,
+ state.SourceFramebuffer);
+
+ GLES20.glFramebufferTexture2D(
+ GLES30.GL_READ_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
+ GLES20.GL_TEXTURE_2D, textureId, level);
+
+ GLES20.glReadPixels(x, y, w, h,
+ GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer);
+
+ GLES20.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER, 0);
+ });
+ }
+
+ //
+ // FNA3D_GetTextureData2D
+ //
+
+ public static void FNA3D_GetTextureData2D(IntPtr device, IntPtr texture,
+ int x, int y, int w, int h, int level,
+ IntPtr data, int dataLength)
+ {
+ // FNA Texture2D uses GCHandle::Alloc and GCHandle::AddrOfPinnedObject.
+ // we use GCHandle::FromIntPtr to convert that address to an object reference.
+ // see also: system.runtime.interopservices.GCHandle struct in baselib.
+ int dataOffset = (int) data;
+ var dataObject = System.Runtime.InteropServices.GCHandle.FromIntPtr(data).Target;
+
+ GetTextureData(Renderer.Get(device), (int) texture,
+ x, y, w, h, level, dataObject, dataOffset, dataLength);
+ }
+
//
// DepthFormatToDepthStorage
//
@@ -238,6 +311,7 @@ namespace Microsoft.Xna.Framework.Graphics
private partial class State
{
public bool RenderToTexture;
+ public int SourceFramebuffer;
public int TargetFramebuffer;
public int ActiveAttachments;
}
diff --git a/BNA/src/FNA3D_Tex.cs b/BNA/src/FNA3D_Tex.cs
index 4db1ca1..c225483 100644
--- a/BNA/src/FNA3D_Tex.cs
+++ b/BNA/src/FNA3D_Tex.cs
@@ -257,7 +257,7 @@ namespace Microsoft.Xna.Framework.Graphics
{
string reason = (bitmap == null) ? "unspecified error"
: $"unsupported config '{bitmap.getConfig()}'";
- throw new System.BadImageFormatException(
+ throw new BadImageFormatException(
$"Load failed for bitmap image '{titleStream.Name}': {reason}");
}
@@ -341,15 +341,22 @@ namespace Microsoft.Xna.Framework.Graphics
ref FNA3D_SamplerState sampler)
{
var samplerCopy = sampler;
+ int textureId = (int) texture;
var renderer = Renderer.Get(device);
renderer.Send( () =>
{
var state = (State) renderer.UserData;
- var config = state.TextureConfigs[(int) texture];
+ var config = state.TextureConfigs[textureId];
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index);
- GLES20.glBindTexture(config[0], (int) texture);
+ GLES20.glBindTexture(config[0], textureId);
+
+ if (index == renderer.TextureUnits - 1)
+ state.TextureOnLastUnit = textureId;
+
+ if (textureId == 0)
+ return;
GLES20.glTexParameteri(config[0], GLES30.GL_TEXTURE_MAX_LEVEL,
config[2] - 1);
@@ -571,6 +578,8 @@ namespace Microsoft.Xna.Framework.Graphics
// #1 - SurfaceFormat
// #2 - levels count
public Dictionary TextureConfigs = new Dictionary();
+
+ public int TextureOnLastUnit;
}
}
diff --git a/BNA/src/Import.cs b/BNA/src/Import.cs
index 64aac7e..71db5ce 100644
--- a/BNA/src/Import.cs
+++ b/BNA/src/Import.cs
@@ -226,3 +226,24 @@ namespace Microsoft.Xna.Framework.Input.Touch
}
}
+
+
+
+namespace Microsoft.Xna.Framework.Media
+{
+
+ //
+ // MediaQueue
+ //
+
+ [java.attr.Discard] // discard in output
+ public class MediaQueue
+ {
+ public MediaQueue() { }
+ public Song ActiveSong { get; }
+ public int ActiveSongIndex { get; set; }
+ public void Add(Song song) { }
+ public void Clear() { }
+ }
+
+}
diff --git a/BNA/src/MediaPlayer.cs b/BNA/src/MediaPlayer.cs
new file mode 100644
index 0000000..24da1fe
--- /dev/null
+++ b/BNA/src/MediaPlayer.cs
@@ -0,0 +1,331 @@
+
+using System;
+using System.Diagnostics;
+#pragma warning disable 0436
+
+namespace Microsoft.Xna.Framework.Media
+{
+
+ public static class MediaPlayer
+ {
+
+ [java.attr.RetainType] private static android.media.MediaPlayer player;
+ [java.attr.RetainType] private static MediaQueue queue;
+ [java.attr.RetainType] private static java.util.concurrent.atomic.AtomicInteger state;
+ [java.attr.RetainType] private static float volume;
+ [java.attr.RetainType] private static bool muted;
+ [java.attr.RetainType] private static bool looping;
+
+ //
+ // static constructor
+ //
+
+ static MediaPlayer()
+ {
+ volume = 1f;
+ state = new java.util.concurrent.atomic.AtomicInteger(0);
+ queue = new MediaQueue();
+ var watcher = new Watcher();
+ player = new android.media.MediaPlayer();
+ player.setOnPreparedListener(watcher);
+ player.setOnCompletionListener(watcher);
+ }
+
+ //
+ // PrepareAndStart
+ //
+
+ private static void PrepareAndStart()
+ {
+ int oldState = state.getAndSet((int) MediaState.Playing);
+ player.prepareAsync();
+ if (oldState != (int) MediaState.Playing && MediaStateChanged != null)
+ {
+ MediaStateChanged(null, EventArgs.Empty);
+ }
+ }
+
+ //
+ // Play
+ //
+
+ public static void Play(Song song)
+ {
+ if (ActiveSongChanged != null)
+ throw new PlatformNotSupportedException("ActiveSongChanged");
+
+ if (song.isAsset)
+ {
+ var asset = GameRunner.Singleton.Activity.getAssets().openFd(song.path);
+ player.setDataSource(asset.getFileDescriptor(),
+ asset.getStartOffset(), asset.getLength());
+ }
+ else
+ player.setDataSource(song.path);
+
+ Queue.Clear();
+ Queue.Add(song);
+ Queue.ActiveSongIndex = 0;
+
+ PrepareAndStart();
+ }
+
+ //
+ // Play (SongCollection)
+ //
+
+ public static void Play(SongCollection songs, int index)
+ {
+ if (songs.Count == 0)
+ {
+ Queue.Clear();
+ Stop();
+ }
+ else if (songs.Count == 1)
+ {
+ Play((Song) (object) songs[0]);
+ }
+ else
+ throw new PlatformNotSupportedException();
+ }
+
+ public static void Play(SongCollection songs) => Play(songs, 0);
+
+ //
+ // Pause
+ //
+
+ public static void Pause()
+ {
+ if (State == MediaState.Playing)
+ {
+ if (player.isPlaying())
+ player.pause();
+ State = MediaState.Paused;
+ }
+ }
+
+ //
+ // Resume
+ //
+
+ public static void Resume()
+ {
+ if (State == MediaState.Paused)
+ {
+ player.start();
+ State = MediaState.Playing;
+ }
+ }
+
+ //
+ // Stop
+ //
+
+ public static void Stop()
+ {
+ if (State != MediaState.Stopped)
+ {
+ player.stop();
+ State = MediaState.Stopped;
+ }
+ }
+
+ //
+ // MoveNext, MovePrevious
+ //
+
+ public static void MoveNext()
+ {
+ Stop();
+ if (looping)
+ PrepareAndStart();
+ }
+
+ public static void MovePrevious() => MoveNext();
+
+ //
+ // IsMuted, Volume
+ //
+
+ public static bool IsMuted
+ {
+ get => muted;
+ set
+ {
+ muted = value;
+ Volume = volume;
+ }
+ }
+
+ public static float Volume
+ {
+ get => volume;
+ set
+ {
+ if (value < 0f)
+ value = 0f;
+ else if (value > 1f)
+ value = 1f;
+ volume = value;
+
+ if (muted)
+ value = 0f;
+ player.setVolume(value, value);
+ }
+ }
+
+ public static bool GameHasControl => true;
+ public static bool IsShuffled { get; set; }
+
+ //
+ // IsRepeating
+ //
+
+ public static bool IsRepeating
+ {
+ get => looping;
+ set
+ {
+ if (value != looping)
+ {
+ looping = value;
+ player.setLooping(value);
+ }
+ }
+ }
+
+ //
+ // State
+ //
+
+ public static MediaState State
+ {
+ get => (MediaState) state.get();
+ set
+ {
+ if ( state.getAndSet((int) value) != (int) value
+ && MediaStateChanged != null)
+ {
+ MediaStateChanged(null, EventArgs.Empty);
+ }
+ }
+ }
+
+ //
+ // PlayPosition
+ //
+
+ public static TimeSpan PlayPosition
+ => TimeSpan.FromMilliseconds(player.getCurrentPosition());
+
+ //
+ // Properties
+ //
+
+ public static event EventHandler ActiveSongChanged;
+ public static event EventHandler MediaStateChanged;
+
+ public static MediaQueue Queue => queue;
+
+ //
+ // IsVisualizationEnabled
+ //
+
+ public static bool IsVisualizationEnabled
+ {
+ get => false;
+ set => throw new PlatformNotSupportedException();
+ }
+
+ //
+ // Watcher
+ //
+
+ private class Watcher : android.media.MediaPlayer.OnPreparedListener,
+ android.media.MediaPlayer.OnCompletionListener
+ {
+
+ //
+ // onPrepared
+ //
+
+ [java.attr.RetainName]
+ public void onPrepared(android.media.MediaPlayer player)
+ {
+ if (MediaPlayer.State == MediaState.Playing)
+ {
+ try
+ {
+ var duration = player.getDuration();
+ if (duration != -1)
+ {
+ MediaPlayer.Queue.ActiveSong.Duration =
+ TimeSpan.FromMilliseconds(duration);
+ }
+ }
+ catch (Exception)
+ {
+ }
+
+ player.start();
+ }
+ }
+
+ //
+ // onCompletion
+ //
+
+ [java.attr.RetainName]
+ public void onCompletion(android.media.MediaPlayer player)
+ {
+ MediaPlayer.Stop();
+ }
+
+ }
+ }
+
+ //
+ // Song
+ //
+
+ public sealed class Song : IEquatable, IDisposable
+ {
+ [java.attr.RetainType] public string path;
+ [java.attr.RetainType] public bool isAsset;
+
+ public bool IsDisposed { get; private set; }
+ public string Name { get; private set; }
+ public TimeSpan Duration { get; set; }
+ public bool IsProtected => false;
+ public bool IsRated => false;
+ public int PlayCount => 0;
+ public int Rating => 0;
+ public int TrackNumber => 0;
+
+ public static Song FromUri(string name, Uri uri)
+ {
+ var song = new Song() { Name = name };
+ if (uri.IsAbsoluteUri && uri.IsFile)
+ song.path = uri.LocalPath;
+ else
+ {
+ song.path = uri.ToString();
+ song.isAsset = true;
+ }
+ return song;
+ }
+
+ ~Song() => Dispose();
+ public void Dispose() => IsDisposed = true;
+
+ public override int GetHashCode() => base.GetHashCode();
+ public bool Equals(Song other) => (((object) other) != null) && (path == other.path);
+ public override bool Equals(object other) => Equals(other as Song);
+ public static bool operator ==(Song song1, Song song2)
+ => (song1 == null) ? (song2 == null) : song1.Equals(song2);
+ public static bool operator !=(Song song1, Song song2) => ! (song1 == song2);
+
+ }
+
+}
diff --git a/BNA/src/SoundEffect.cs b/BNA/src/SoundEffect.cs
new file mode 100644
index 0000000..f5e4f38
--- /dev/null
+++ b/BNA/src/SoundEffect.cs
@@ -0,0 +1,631 @@
+
+using System;
+using System.IO;
+#pragma warning disable 0436
+
+namespace Microsoft.Xna.Framework.Audio
+{
+
+ public sealed class SoundEffect : IDisposable
+ {
+
+ [java.attr.RetainType] public object dataArray;
+ [java.attr.RetainType] public int dataCount;
+ [java.attr.RetainType] public int sampleRate;
+ [java.attr.RetainType] public int channelConfig;
+ [java.attr.RetainType] public int markerFrame;
+
+ [java.attr.RetainType] public static java.util.ArrayList instancesList = new java.util.ArrayList();
+ [java.attr.RetainType] public static java.util.concurrent.locks.ReentrantLock instancesLock = new java.util.concurrent.locks.ReentrantLock();
+
+ //
+ // Constructor (for ContentReader)
+ //
+
+ public SoundEffect(string name, byte[] buffer, int offset, int count,
+ ushort wFormatTag, ushort nChannels,
+ uint nSamplesPerSec, uint nAvgBytesPerSec,
+ ushort nBlockAlign, ushort wBitsPerSample,
+ int loopStart, int loopLength)
+ {
+ if (wFormatTag != 1 /* WAVE_FORMAT_PCM */)
+ throw new ArgumentException("bad wFormatTag");
+ if (offset != 0)
+ throw new ArgumentException("bad offset");
+ if (nBlockAlign != nChannels * wBitsPerSample / 8)
+ throw new ArgumentException("bad nBlockAlign");
+ if (nAvgBytesPerSec != nSamplesPerSec * nBlockAlign)
+ throw new ArgumentException("bad nAvgBytesPerSec");
+
+ sampleRate = (int) nSamplesPerSec;
+ channelConfig = (nChannels == 1) ? android.media.AudioFormat.CHANNEL_OUT_MONO
+ : (nChannels == 2) ? android.media.AudioFormat.CHANNEL_OUT_STEREO
+ : throw new ArgumentException("bad nChannels");
+
+ if (wBitsPerSample == 8)
+ {
+ dataArray = buffer;
+ dataCount = count;
+ }
+ else if (wBitsPerSample == 16)
+ {
+ int shortCount = count / 2;
+ var shortBuffer = new short[shortCount];
+ java.nio.ByteBuffer.wrap((sbyte[]) (object) buffer)
+ .order(java.nio.ByteOrder.LITTLE_ENDIAN).asShortBuffer()
+ .get(shortBuffer);
+ dataArray = shortBuffer;
+ dataCount = shortCount;
+ }
+ else
+ throw new ArgumentException("bad wBitsPerSample");
+
+ markerFrame = dataCount / nChannels;
+
+ Name = name;
+ Duration = TimeSpan.FromSeconds(count / (double) nAvgBytesPerSec);
+ }
+
+ //
+ // Constructor
+ //
+
+ public SoundEffect(byte[] buffer, int sampleRate, AudioChannels channels)
+ : this(null, buffer, 0, buffer.Length,
+ 1 /* WAVE_FORMAT_PCM */, (ushort) channels, (uint) sampleRate,
+ (uint) (sampleRate * ((ushort) channels * 2)),
+ (ushort) ((ushort) channels * 2), 16, 0, 0)
+ {
+ }
+
+ //
+ // Constructor
+ //
+
+ public SoundEffect(byte[] buffer, int offset, int count, int sampleRate,
+ AudioChannels channels, int loopStart, int loopLength)
+ : this(null, buffer, offset, count,
+ 1 /* WAVE_FORMAT_PCM */, (ushort) channels, (uint) sampleRate,
+ (uint) (sampleRate * ((ushort) channels * 2)),
+ (ushort) ((ushort) channels * 2), 16, loopStart, loopLength)
+ {
+ }
+
+ //
+ // FromStream
+ //
+
+ public static SoundEffect FromStream(Stream stream)
+ {
+ using (BinaryReader reader = new BinaryReader(stream))
+ {
+ for (;;)
+ {
+ if (new string(reader.ReadChars(4)) != "RIFF")
+ break;
+ reader.ReadUInt32(); // skip chunk size
+ if (new string(reader.ReadChars(4)) != "WAVE")
+ break;
+
+ if (new string(reader.ReadChars(4)) != "fmt ")
+ break;
+ if (reader.ReadInt32() != 16) // fmt chunk size always 16
+ break;
+
+ var wFormatTag = reader.ReadUInt16();
+ var nChannels = reader.ReadUInt16();
+ var nSamplesPerSec = reader.ReadUInt32();
+ var nAvgBytesPerSec = reader.ReadUInt32();
+ var nBlockAlign = reader.ReadUInt16();
+ var wBitsPerSample = reader.ReadUInt16();
+
+ if (new string(reader.ReadChars(4)) != "data")
+ break;
+
+ var count = reader.ReadInt32();
+ var buffer = reader.ReadBytes(count);
+
+ return new SoundEffect(null, buffer, 0, count, wFormatTag, nChannels,
+ nSamplesPerSec, nAvgBytesPerSec,
+ nBlockAlign, wBitsPerSample, 0, 0);
+ }
+ }
+
+ throw new BadImageFormatException("invalid wave data for sound effect");
+ }
+
+ //
+ // Destructor
+ //
+
+ ~SoundEffect() => Dispose();
+
+ //
+ // Dispose
+ //
+
+ public void Dispose()
+ {
+ if (! IsDisposed)
+ {
+ IsDisposed = true;
+ DiscardInstance(null, this);
+ }
+ }
+
+ //
+ // Properties
+ //
+
+ public string Name { get; set; }
+ public TimeSpan Duration { get; private set; }
+ public bool IsDisposed { get; private set; }
+
+ //
+ // Play
+ //
+
+ public bool Play() => Play(1f, 0f, 0f);
+
+ public bool Play(float volume, float pitch, float pan)
+ => CreateInstance().Play(volume, pitch, pan);
+
+ //
+ // CreateInstance
+ //
+
+ public SoundEffectInstance CreateInstance()
+ {
+ var inst = new SoundEffectInstance(this);
+ try
+ {
+ instancesLock.@lock();
+ instancesList.add(new java.lang.@ref.WeakReference(inst));
+ }
+ finally
+ {
+ instancesLock.unlock();
+ }
+ return inst;
+ }
+
+ //
+ // DiscardInstance
+ //
+
+ public static void DiscardInstance(SoundEffectInstance discardInstance, SoundEffect discardEffect)
+ {
+ if (instancesLock.isHeldByCurrentThread())
+ return;
+ try
+ {
+ instancesLock.@lock();
+ for (int idx = instancesList.size(); idx-- > 0;)
+ {
+ var instRef = (java.lang.@ref.WeakReference) instancesList.get(idx);
+ var inst = (SoundEffectInstance) instRef.get();
+ if (inst == discardInstance || inst == null || inst.ShouldDiscard(discardEffect))
+ instancesList.remove(idx);
+ }
+ }
+ finally
+ {
+ instancesLock.unlock();
+ }
+ }
+
+ //
+ // ReleaseInstance
+ //
+
+ public static bool ReleaseInstance()
+ {
+ bool found = false;
+ try
+ {
+ instancesLock.@lock();
+ int num = instancesList.size();
+ for (int idx = 0; idx < num; idx++)
+ {
+ var instRef = (java.lang.@ref.WeakReference) instancesList.get(idx);
+ var inst = (SoundEffectInstance) instRef.get();
+ if (inst != null && inst.ReleaseTrack(false))
+ {
+ found = true;
+ break;
+ }
+ }
+ }
+ finally
+ {
+ instancesLock.unlock();
+ }
+ return found;
+ }
+
+ //
+ // GetSampleDuration, GetSampleSizeInBytes
+ //
+
+ public static TimeSpan GetSampleDuration(int sizeInBytes, int sampleRate,
+ AudioChannels channels)
+ => TimeSpan.FromSeconds(
+ ((sizeInBytes / (2 * (int) channels)) / (float) sampleRate));
+
+ public static int GetSampleSizeInBytes(TimeSpan duration, int sampleRate,
+ AudioChannels channels)
+ => (int) (duration.TotalSeconds * sampleRate * 2 * (int) channels);
+
+ //
+ // MasterVolume, DistanceScale, DopplerScale, SpeedOfSound (no-op)
+ //
+
+ public static float MasterVolume { get; set; }
+ public static float DistanceScale { get; set; }
+ public static float DopplerScale { get; set; }
+ public static float SpeedOfSound { get; set; }
+ }
+
+
+
+ //
+ // SoundEffectInstance
+ //
+
+ public class SoundEffectInstance : IDisposable
+ {
+ [java.attr.RetainType] private SoundEffect effect;
+ [java.attr.RetainType] private SoundEffectInstanceWatcher watcher;
+ [java.attr.RetainType] private android.media.AudioTrack track;
+ [java.attr.RetainType] private float pitch, pan, volume;
+ [java.attr.RetainType] private bool isLooped;
+
+ //
+ // Constructor (for SoundEffect.CreateInstance)
+ //
+
+ public SoundEffectInstance(SoundEffect fromEffect)
+ {
+ effect = fromEffect;
+ volume = 1f;
+ }
+
+ //
+ // CreateTrack
+ //
+
+ private void CreateTrack(bool tryRelease)
+ {
+ int numToWrite, numWritten;
+
+ if (effect.dataArray is sbyte[] byteData)
+ {
+ numToWrite = byteData.Length;
+ track = new android.media.AudioTrack(
+ android.media.AudioManager.STREAM_MUSIC,
+ effect.sampleRate, effect.channelConfig,
+ android.media.AudioFormat.ENCODING_PCM_8BIT,
+ numToWrite, android.media.AudioTrack.MODE_STATIC);
+ numWritten = track.write(byteData, 0, numToWrite);
+ }
+
+ else if (effect.dataArray is short[] shortData)
+ {
+ numToWrite = shortData.Length;
+ track = new android.media.AudioTrack(
+ android.media.AudioManager.STREAM_MUSIC,
+ effect.sampleRate, effect.channelConfig,
+ android.media.AudioFormat.ENCODING_PCM_16BIT,
+ numToWrite * 2, android.media.AudioTrack.MODE_STATIC);
+ numWritten = track.write(shortData, 0, numToWrite);
+ }
+ else
+ {
+ numToWrite = 0;
+ numWritten = android.media.AudioTrack.ERROR_INVALID_OPERATION;
+ }
+
+ if (numWritten != numToWrite)
+ {
+ track = null;
+ if (numWritten < 0)
+ {
+ if (SoundEffect.ReleaseInstance())
+ CreateTrack(false);
+ }
+ if (track == null)
+ {
+ GameRunner.Log($"SoundEffectInstance '{effect.Name}' error {numWritten}/{numToWrite}");
+ }
+ return;
+ }
+
+ track.setNotificationMarkerPosition(effect.markerFrame);
+ track.setPlaybackPositionUpdateListener(watcher = new SoundEffectInstanceWatcher());
+ }
+
+ //
+ // ReleaseTrack
+ //
+
+ public bool ReleaseTrack(bool disposing)
+ {
+ var track = this.track;
+ if (track != null)
+ {
+ if (disposing || track.getPlayState() == 1 /* android.media.AudioTrack.PLAYSTATE_STOPPED */)
+ {
+ this.track = null;
+ track.setPlaybackPositionUpdateListener(null);
+ track.stop();
+ track.release();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ //
+ // Destructor
+ //
+
+ ~SoundEffectInstance() => Dispose(true);
+
+ //
+ // Dispose
+ //
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (! IsDisposed)
+ {
+ ReleaseTrack(true);
+ SoundEffect.DiscardInstance(this, null);
+ effect = null;
+ IsDisposed = true;
+ }
+ }
+
+ public void Dispose() => Dispose(true);
+
+ //
+ // ShouldDiscard
+ //
+
+ public bool ShouldDiscard(SoundEffect fromEffect)
+ {
+ if (effect == fromEffect)
+ Dispose(true);
+ return IsDisposed;
+ }
+
+ //
+ // Properties
+ //
+
+ public SoundState State
+ {
+ get
+ {
+ var track = this.track;
+ if (track == null)
+ return SoundState.Stopped;
+
+ return track.getPlayState() switch
+ {
+ 1 /* android.media.AudioTrack.PLAYSTATE_STOPPED */ => SoundState.Stopped,
+ 2 /* android.media.AudioTrack.PLAYSTATE_PAUSED */ => SoundState.Paused,
+ 3 /* android.media.AudioTrack.PLAYSTATE_PLAYING */ => SoundState.Playing,
+ _ => throw new InvalidOperationException()
+ };
+ }
+ }
+
+ public bool IsDisposed { get; protected set; }
+ public float Pitch
+ {
+ get => pitch;
+ set => SetPlaybackRate(value, State == SoundState.Playing);
+ }
+ public float Pan
+ {
+ get => pan;
+ set => SetStereoVolume(volume, value, State == SoundState.Playing);
+ }
+ public float Volume
+ {
+ get => volume;
+ set => SetStereoVolume(value, pan, State == SoundState.Playing);
+ }
+
+ //
+ // IsLooped
+ //
+ // note that looping does not respect any custom loop points,
+ // and always occurs on the entire effect
+ //
+
+ public virtual bool IsLooped
+ {
+ get => isLooped;
+ set
+ {
+ if (State == SoundState.Playing)
+ throw new InvalidOperationException();
+ isLooped = value;
+ }
+ }
+
+ //
+ // SetPlaybackRate
+ //
+
+ private void SetPlaybackRate(float pitch, bool playing)
+ {
+ if (pitch < -1f)
+ pitch = -1f;
+ else if (pitch > 1f)
+ pitch = 1f;
+ this.pitch = pitch;
+
+ if (playing)
+ {
+ // convert pitch from range 0 .. 1 to range 0.5 .. 2
+ // (which represents half to twice the sample rate)
+ if (pitch < 0f)
+ pitch = 1f + pitch * 0.5f;
+ else if (pitch > 0f)
+ pitch = 1f + pitch;
+ int r = (int) (effect.sampleRate * pitch);
+ var track = this.track;
+ if (track != null)
+ track.setPlaybackRate(r);
+ }
+ }
+
+ //
+ // SetStereoVolume
+ //
+
+ private void SetStereoVolume(float volume, float pan, bool playing)
+ {
+ if (volume < 0f)
+ volume = 0f;
+ else if (volume > 1f)
+ volume = 1f;
+ this.volume = volume;
+
+ if (pan < -1f)
+ pan = -1f;
+ else if (pan > 1f)
+ pan = 1f;
+ this.pan = pan;
+
+ if (playing)
+ {
+ float leftGain = 1f;
+ float rightGain = 1f;
+ if (pan < 0f)
+ {
+ leftGain *= 0f - pan;
+ rightGain *= pan + 1f;
+ }
+ else if (pan > 0f)
+ {
+ rightGain *= pan;
+ leftGain *= 1f - pan;
+ }
+ var track = this.track;
+ if (track != null)
+ track.setStereoVolume(leftGain * volume, rightGain * volume);
+ }
+ }
+
+ //
+ // Play, Pause, Resume, Stop
+ //
+
+ public virtual void Play()
+ {
+ if (State != SoundState.Playing)
+ {
+ var track = this.track;
+ if (track == null)
+ {
+ CreateTrack(true);
+ track = this.track;
+ }
+
+ if (track != null)
+ {
+ SetPlaybackRate(pitch, true);
+ SetStereoVolume(volume, pan, true);
+
+ watcher.instance = this;
+ track.play();
+ }
+ }
+ }
+
+ public void Pause()
+ {
+ var track = this.track;
+ if (track != null && State == SoundState.Playing)
+ track.pause();
+ }
+
+ public void Resume() => Play();
+
+ public void Stop() => Stop(true);
+
+ public void Stop(bool immediate)
+ {
+ if (immediate)
+ {
+ var track = this.track;
+ if (track != null && State != SoundState.Stopped)
+ track.stop();
+ }
+ if (watcher != null)
+ watcher.instance = null;
+ }
+
+ //
+ // Play (for SoundEffect.Play)
+ //
+
+ public bool Play(float volume, float pitch, float pan)
+ {
+ this.volume = volume;
+ this.pitch = pitch;
+ this.pan = pan;
+ Play();
+ if (track == null)
+ {
+ Dispose(true);
+ return false;
+ }
+ return true;
+ }
+
+ //
+ // Apply3D (no-op)
+ //
+
+ public void Apply3D(AudioListener listener, AudioEmitter emitter) { }
+ public void Apply3D(AudioListener[] listeners, AudioEmitter emitter) { }
+
+ }
+
+
+
+ //
+ // SoundEffectInstanceWatcher
+ //
+
+ public class SoundEffectInstanceWatcher :
+ android.media.AudioTrack.OnPlaybackPositionUpdateListener
+ {
+ [java.attr.RetainType] public SoundEffectInstance instance;
+
+ [java.attr.RetainName]
+ public void onMarkerReached(android.media.AudioTrack track)
+ {
+ // release the strong reference to the SoundEffectInstance,
+ // so it can be garbage collected if not otherwise referenced
+ track.stop();
+ var instance = this.instance;
+ if (instance != null)
+ {
+ if (instance.IsLooped && (! instance.IsDisposed))
+ track.play();
+ else
+ this.instance = null;
+ }
+ }
+
+ [java.attr.RetainName]
+ public void onPeriodicNotification(android.media.AudioTrack track) { }
+ }
+
+}
diff --git a/BNA/src/TitleContainer.cs b/BNA/src/TitleContainer.cs
index 20e438e..0b67268 100644
--- a/BNA/src/TitleContainer.cs
+++ b/BNA/src/TitleContainer.cs
@@ -59,4 +59,10 @@ namespace Microsoft.Xna.Framework
}
+ /*internal static class TitleLocation
+ {
+ // there is no file path for asset files which are part of the APK
+ public static string Path => throw new System.PlatformNotSupportedException();
+ }*/
+
}
diff --git a/Demo1/Demo1/Demo1.csproj b/Demo1/Demo1/Demo1.csproj
index 2c947b0..c7b0104 100644
--- a/Demo1/Demo1/Demo1.csproj
+++ b/Demo1/Demo1/Demo1.csproj
@@ -60,6 +60,7 @@
+
diff --git a/Demo1/Demo1/Game1.cs b/Demo1/Demo1/Game1.cs
index 260bf0e..080e089 100644
--- a/Demo1/Demo1/Game1.cs
+++ b/Demo1/Demo1/Game1.cs
@@ -3,6 +3,7 @@ using System;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Audio;
namespace Demo1
{
@@ -14,6 +15,8 @@ namespace Demo1
public Texture2D white;
private SpriteBatch spriteBatch;
private Font myFont;
+ private SoundEffectInstance effect;
+ private bool playEffect;
private int pageNumber, pageNumberOld;
private bool paused;
private bool anyDrawText;
@@ -58,8 +61,33 @@ namespace Demo1
// Texture2D.FromStream(GraphicsDevice, stream);
//
// to disable processing, open Properties on the image in the
- // Content project, set "Build Action: None", and
- // "Copy to Output Directory: Copy if newer".
+ // Content project, and change the following in the Advanced tab:
+ // "Build Action: None" and "Copy to Output Directory: Copy if newer".
+
+ effect = Content.Load("effect").CreateInstance();
+ effect.Pitch = 0.2f;
+
+ // the XNA content processor for Song converts music files to WMA
+ // format, which Android does not support playing. use MP3 instead.
+ //
+ // to disable processing, open Properties on the image in the
+ // Content project, and change the following in the Advanced tab:
+ // "Build Action: None" and "Copy to Output Directory: Copy if newer".
+ //
+ // note that XNA on Windows may not play MP3 files which contain ID3
+ // tags. use one of the many free utilities to remove such tags.
+ //
+ // unsupported BNA MediaPlayer: queueing more than one song at a time;
+ // the ActiveSongChanged event; visualization data.
+
+ Microsoft.Xna.Framework.Media.MediaPlayer.Volume = 0.05f;
+ Microsoft.Xna.Framework.Media.MediaPlayer.Play(
+ Microsoft.Xna.Framework.Media.Song.FromUri("Song1",
+ new System.Uri(Content.RootDirectory + "/music.mp3", UriKind.Relative)));
+ Microsoft.Xna.Framework.Media.MediaPlayer.IsRepeating = true;
+ // song duration is populated during the call to Play()
+ // Console.WriteLine(Microsoft.Xna.Framework.Media.MediaPlayer.Queue.ActiveSong.Duration);
+
}
@@ -80,12 +108,26 @@ namespace Demo1
if (paused)
return;
+ if (playEffect)
+ {
+ effect.Play();
+ playEffect = false;
+ }
+
// basic scene management for the purpose of this demo:
// after either arrow at the top of the screen is clicked, and
// the current page number has changed, create the new 'page'.
- if (pageNumber != pageNumberOld)
+ if (pageNumber != pageNumberOld)
{
+ if (pageComponent != null)
+ {
+ if (effect.State == SoundState.Playing)
+ effect.Stop();
+ effect.Pan = (pageNumber > pageNumberOld) ? 1f : -1f;
+ playEffect = true;
+ }
+
const int LAST_PAGE = 4;
if (pageNumber <= 0)
pageNumber = LAST_PAGE;
diff --git a/Demo1/Demo1/RenderDemo.cs b/Demo1/Demo1/RenderDemo.cs
index eac63e1..9fe6120 100644
--- a/Demo1/Demo1/RenderDemo.cs
+++ b/Demo1/Demo1/RenderDemo.cs
@@ -42,7 +42,7 @@ namespace Demo1
{
if (renderToTexture)
{
- ((Game1)Game).DrawFlushBatch();
+ ((Game1) Game).DrawFlushBatch();
renderTargetWidth = Config.ClientWidth;
renderTargetHeight = Config.ClientHeight;
if (renderTarget != null)
diff --git a/Demo1/Demo1/Storage.cs b/Demo1/Demo1/Storage.cs
index 0f74a5b..b381138 100644
--- a/Demo1/Demo1/Storage.cs
+++ b/Demo1/Demo1/Storage.cs
@@ -48,6 +48,10 @@ namespace Demo1
file = container.OpenFile("state-v1.bin", FileMode.OpenOrCreate);
if (file.Length > 4)
Read(file);
+
+ // reset file in case we crash
+ file.SetLength(0);
+ file.Flush();
}
catch (Exception e)
{
diff --git a/Demo1/Demo1Content/Demo1Content.contentproj b/Demo1/Demo1Content/Demo1Content.contentproj
index cee89a8..2a9f94d 100644
--- a/Demo1/Demo1Content/Demo1Content.contentproj
+++ b/Demo1/Demo1Content/Demo1Content.contentproj
@@ -1,4 +1,11 @@
+
{E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}
{96E2B04D-8817-42c6-938A-82C39BA4D311};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
@@ -64,6 +71,21 @@
TextureProcessor
+
+
+ effect
+ Mp3Importer
+ SoundEffectProcessor
+
+
+
+
+ music
+ Mp3Importer
+ SongProcessor
+ PreserveNewest
+
+
+
Library
net461
@@ -14,6 +20,9 @@
+
+
+
C:\Program Files (x86)\Microsoft XNA\XNA Game Studio\v4.0\References\Windows\x86\Microsoft.Xna.Framework.dll
diff --git a/Demo1/Demo1FSharp/Library1.fs b/Demo1/Demo1FSharp/Library1.fs
index 30f1236..9ee1a16 100644
--- a/Demo1/Demo1FSharp/Library1.fs
+++ b/Demo1/Demo1FSharp/Library1.fs
@@ -7,8 +7,10 @@ open Microsoft.Xna.Framework.Graphics
type StencilDemo (game : Game) =
inherit DrawableGameComponent(game)
- let mutable spriteBatch = null
- let mutable texture = null
+ let mutable spriteBatch = new SpriteBatch (game.GraphicsDevice)
+ let mutable logoTexture = game.Content.Load("fsharp256")
+ let mutable backTexture = new Texture2D(game.GraphicsDevice, 16, 16)
+ let mutable counter = 0
// using option here solely to force a dependency on FSharp.Core.dll
let mutable rectFunc : (Game -> Rectangle) option = None
@@ -26,12 +28,36 @@ type StencilDemo (game : Game) =
(float32) (Math.Cos(gameTime.TotalGameTime.TotalMilliseconds * 0.001)))
override Game.Initialize() =
- spriteBatch <- new SpriteBatch (game.GraphicsDevice)
- texture <- game.Content.Load("fsharp256")
rectFunc <- Some logoRect
colorFunc <- Some logoColor
override Game.Draw gameTime =
+
+ let backArray = Array.zeroCreate (16 * 16)
+ backTexture.GetData backArray
+
+ if counter = 0
+ then
+ backArray.[12 * 16] <- 0xFF0000FF
+ counter <- 1
+ else
+ for y = 4 to 11 do
+ for x = 0 to 15 do
+ let idx = y * 16 + x
+ backArray.[idx] <- backArray.[idx + 1]
+ backArray.[idx + 1] <- 0
+ counter <- match counter with
+ | 60 -> 0
+ | n -> n + 1
+ backTexture.SetData backArray
+
+ spriteBatch.Begin (SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, new RasterizerState())
+ spriteBatch.Draw (backTexture,
+ Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height),
+ Nullable (Rectangle(0, 0, 16, 16)),
+ Color.White)
+ spriteBatch.End ()
+
spriteBatch.Begin ()
- spriteBatch.Draw (texture, rectFunc.Value game, colorFunc.Value gameTime)
+ spriteBatch.Draw (logoTexture, rectFunc.Value game, colorFunc.Value gameTime)
spriteBatch.End ()
\ No newline at end of file
diff --git a/MakeAPK.project b/MakeAPK.project
index df42009..06411e4 100644
--- a/MakeAPK.project
+++ b/MakeAPK.project
@@ -17,6 +17,8 @@
CONTENT_DIR = path to Content directory
e.g. $(OutputDir)/MyGame/Debug/Content
+ ICON_PNG = path to a PNG file to use as the app icon
+
APK_OUTPUT = path to copy the final APK
e.g. $(OutputDir)/MyGame.apk
diff --git a/build_demo.bat b/build_demo.bat
index cc555c4..6eeec36 100644
--- a/build_demo.bat
+++ b/build_demo.bat
@@ -30,6 +30,7 @@ echo Building BNA. Command:
echo MSBuild BNA -p:Configuration=Release
echo ========================================
MSBuild BNA -p:Configuration=Release
+if errorlevel 1 goto :EOF
pause
echo ========================================
@@ -38,14 +39,17 @@ echo nuget restore Demo1
echo msbuild Demo1 -p:Configuration=Release -p:Platform="x86"
echo ========================================
nuget restore Demo1
+if errorlevel 1 goto :EOF
msbuild Demo1 -p:Configuration=Release -p:Platform="x86"
+if errorlevel 1 goto :EOF
pause
echo ========================================
echo Converting Demo1 to APK. Command:
echo MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB%
echo ========================================
-MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB%
+ MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB%
+if errorlevel 1 goto :EOF
echo ========================================
echo All done