Improve scrolling to only clamp scroll when player-initiated
        Implement SPLIT, JOIN, ANIME2 ops
        Fix TURA op rotation direction to be more resiliant
        Improve ANIMW to hopefully work better
Add support for looping sfx, allow field SOUND op to stop them
This commit is contained in:
ficedula 2023-02-09 19:11:02 +00:00
parent 72bcdf57f4
commit eea0e11c22
6 changed files with 232 additions and 122 deletions

View File

@ -91,6 +91,20 @@ namespace Braver {
}
}
public void StopLoopingSfx() {
foreach (var loop in _activeLoops) {
loop.Stop();
loop.Dispose();
}
_activeLoops.Clear();
}
public void Update() {
var needsRestart = _activeLoops.Where(instance => instance.State != SoundState.Playing);
foreach (var loop in needsRestart)
loop.Play();
}
private async Task RunMusic() {
var contexts = new Stack<MusicContext>();
contexts.Push(new MusicContext());
@ -182,37 +196,59 @@ namespace Braver {
_game.Net.Send(new Net.MusicMessage { Track = string.Empty, IsPop = popContext });
}
private Dictionary<int, WeakReference<SoundEffect>> _sfx = new();
private HashSet<SoundEffect> _recent0 = new(), _pinned = new(), _recent1;
private class LoadedEffect {
public SoundEffect Effect { get; set; }
public bool ShouldLoop { get; set; }
}
private Dictionary<int, WeakReference<LoadedEffect>> _sfx = new();
private HashSet<LoadedEffect> _recent0 = new(), _pinned = new(), _recent1;
private HashSet<SoundEffectInstance> _activeLoops = new();
private DateTime _lastPromote = DateTime.MinValue;
public void Precache(Sfx which, bool pin) {
byte[] raw = _sfxSource.ExportPCM((int)which, out int freq, out int channels);
private LoadedEffect GetEffect(int which) {
byte[] raw = _sfxSource.ExportPCM(which, out int freq, out int channels);
var fx = new SoundEffect(raw, freq, channels == 1 ? AudioChannels.Mono : AudioChannels.Stereo);
_sfx[(int)which] = new WeakReference<SoundEffect>(fx);
return new LoadedEffect {
Effect = fx,
ShouldLoop = _sfxSource.GetExtraData(which)[0] != 0, //TODO - seems like it *might* be right?!
};
}
public void Precache(Sfx which, bool pin) {
var effect = GetEffect((int)which);
_sfx[(int)which] = new WeakReference<LoadedEffect>(effect);
if (pin)
_pinned.Add(fx);
_pinned.Add(effect);
}
public void PlaySfx(Sfx which, float volume, float pan) => PlaySfx((int)which, volume, pan);
public void PlaySfx(int which, float volume, float pan) {
SoundEffect fx;
LoadedEffect effect;
if (_sfx.TryGetValue(which, out var wr) && wr.TryGetTarget(out fx)) {
if (_sfx.TryGetValue(which, out var wr) && wr.TryGetTarget(out effect)) {
//
} else {
byte[] raw = _sfxSource.ExportPCM(which, out int freq, out int channels);
fx = new SoundEffect(raw, freq, channels == 1 ? AudioChannels.Mono : AudioChannels.Stereo);
_sfx[which] = new WeakReference<SoundEffect>(fx);
effect = GetEffect(which);
_sfx[which] = new WeakReference<LoadedEffect>(effect);
}
if (effect.ShouldLoop) {
var instance = effect.Effect.CreateInstance();
instance.Pan = pan;
instance.Volume = volume;
instance.Play();
_activeLoops.Add(instance);
} else {
effect.Effect.Play(volume, 0, pan);
}
fx.Play(volume, 0, pan);
if (_lastPromote < DateTime.Now.AddMinutes(-1)) {
_recent1 = _recent0;
_recent0 = new();
}
_recent0.Add(fx);
_recent0.Add(effect);
_game.Net.Send(new Net.SfxMessage { Which = which, Volume = volume, Pan = pan });
}

View File

@ -191,6 +191,7 @@ namespace Braver {
}
Net.Update();
Audio.Update();
}
}

View File

@ -142,6 +142,8 @@ namespace Braver.Field {
g.Net.Listen<Net.FieldEntityModelMessage>(this);
g.Net.Listen<Net.FieldBGScrollMessage>(this);
g.Audio.StopLoopingSfx();
Overlay = new Overlay(g, graphics);
g.Net.Send(new Net.FieldScreenMessage { Destination = _destination });
@ -438,21 +440,23 @@ namespace Braver.Field {
(int)(_view2D.CenterY / 3)
);
}
public void BGScroll(float x, float y) {
BGScrollOffset(x - (-_view2D.CenterX / 3), y - (_view2D.CenterY / 3));
public void BGScroll(float x, float y, bool clampToViewport) {
BGScrollOffset(x - (-_view2D.CenterX / 3), y - (_view2D.CenterY / 3), clampToViewport);
}
public void BGScrollOffset(float ox, float oy) {
int minYScroll = Background.MinY >= -120 ? 0 : -Background.MinY - 120,
maxYScroll = Background.MaxY <= 120 ? 0 : Background.MaxY - 120,
minXScroll = Background.MinX >= -213 ? 0 : -Background.MinX - 213,
maxXScroll = Background.MaxX <= 213 ? 0 : Background.MaxX - 213;
public void BGScrollOffset(float ox, float oy, bool clampToViewport) {
_view2D.CenterX -= 3 * ox;
_view2D.CenterY += 3 * oy;
_view2D.CenterX = Math.Min(Math.Max(-3 * maxXScroll, _view2D.CenterX), 3 * minXScroll);
_view2D.CenterY = Math.Min(Math.Max(-3 * minYScroll, _view2D.CenterY), 3 * maxYScroll);
if (clampToViewport) {
int minYScroll = Background.MinY >= -120 ? 0 : -Background.MinY - 120,
maxYScroll = Background.MaxY <= 120 ? 0 : Background.MaxY - 120,
minXScroll = Background.MinX >= -213 ? 0 : -Background.MinX - 213,
maxXScroll = Background.MaxX <= 213 ? 0 : Background.MaxX - 213;
_view2D.CenterX = Math.Min(Math.Max(-3 * maxXScroll, _view2D.CenterX), 3 * minXScroll);
_view2D.CenterY = Math.Min(Math.Max(-3 * minYScroll, _view2D.CenterY), 3 * maxYScroll);
}
var newScroll = GetBGScroll();
_view3D.ScreenOffset = new Vector2(newScroll.x * 3f * 2 / 1280, newScroll.y * -3f * 2 / 720);
@ -509,7 +513,7 @@ namespace Braver.Field {
if (newScroll != scroll) {
System.Diagnostics.Debug.WriteLine($"BringPlayerIntoView: Player at BG pos {posOnBG}, BG scroll is {scroll}, needs to be {newScroll}");
BGScroll(newScroll.x, newScroll.y);
BGScroll(newScroll.x, newScroll.y, true);
}
}
}
@ -594,88 +598,10 @@ namespace Braver.Field {
if (_debugMode) {
if (input.IsDown(InputKey.PanLeft))
BGScrollOffset(0, -1);
BGScrollOffset(0, -1, false);
else if (input.IsDown(InputKey.PanRight))
BGScrollOffset(0, +1);
BGScrollOffset(0, +1, false);
if (input.IsAnyDirectionDown() || input.IsJustDown(InputKey.Select)) {
if (input.IsDown(InputKey.Up))
_view3D.CameraPosition += _view3D.CameraUp;
else if (input.IsDown(InputKey.Down))
_view3D.CameraPosition -= _view3D.CameraUp;
System.Diagnostics.Debug.WriteLine($"Player at {ModelToBGPosition(Player.Model.Translation, null, false)} WM0 {ModelToBGPosition(_walkmesh[0].V0.ToX(), null, false)}");
/*
//Now calculate 3d scroll amount
var _3dScrollAmount = new Vector2(_view3D.Width / 427f, _view3D.Height / 240f);
System.Diagnostics.Debug.WriteLine($"To scroll 3d view by one BG pixel, it will move {_3dScrollAmount}");
if (input.IsJustDown(InputKey.Select)) {
_view3D.CenterX += _3dScrollAmount.X * _view2D.CenterX / -3;
_view3D.CenterY += _3dScrollAmount.Y * _view2D.CenterY / -3;
}
if (input.IsDown(InputKey.OK)) {
if (input.IsDown(InputKey.Up)) {
_view2D.CenterY += 3;
_view3D.CenterY += _3dScrollAmount.Y;
}
if (input.IsDown(InputKey.Down)) {
_view2D.CenterY -= 3;
_view3D.CenterY -= _3dScrollAmount.Y;
}
if (input.IsDown(InputKey.Left)) {
_view2D.CenterX -= 3;
_view3D.CenterX -= _3dScrollAmount.X;
}
if (input.IsDown(InputKey.Right)) {
_view2D.CenterX += 3;
_view3D.CenterX += _3dScrollAmount.X;
}
} else if (input.IsDown(InputKey.Cancel)) {
if (input.IsDown(InputKey.Up))
_view2D.CenterY++;
if (input.IsDown(InputKey.Down))
_view2D.CenterY--;
if (input.IsDown(InputKey.Left))
_view2D.CenterX--;
if (input.IsDown(InputKey.Right))
_view2D.CenterX++;
} else {
if (input.IsDown(InputKey.Menu)) {
if (input.IsDown(InputKey.Up))
_view3D.Height++;
if (input.IsDown(InputKey.Down))
_view3D.Height--;
if (input.IsDown(InputKey.Left))
_view3D.Width--;
if (input.IsDown(InputKey.Right))
_view3D.Width++;
} else {
if (input.IsDown(InputKey.Up)) {
_view3D.CenterY++;
}
if (input.IsDown(InputKey.Down)) {
_view3D.CenterY--;
}
if (input.IsDown(InputKey.Left)) {
_view3D.CenterX--;
}
if (input.IsDown(InputKey.Right)) {
_view3D.CenterX++;
}
}
}
System.Diagnostics.Debug.WriteLine($"View2D Center: {_view2D.CenterX}/{_view2D.CenterY}");
System.Diagnostics.Debug.WriteLine($"View3D: {_view3D}");
*/
}
} else {
if (Dialog.IsActive) {
@ -1200,7 +1126,7 @@ namespace Braver.Field {
Background.SetParameter(message.Parm, message.Value);
}
public void Received(Net.FieldBGScrollMessage message) {
BGScroll(message.X, message.Y);
BGScroll(message.X, message.Y, false);
}
public void Received(Net.FieldEntityModelMessage message) {

View File

@ -622,7 +622,7 @@ namespace Braver.Field {
internal static class FieldModels {
public static OpResult IDLCK(Fiber f, Entity e, FieldScreen s) {
ushort triID = f.ReadU16();
byte enabled = f.ReadU8();
@ -672,6 +672,118 @@ namespace Braver.Field {
return OpResult.Continue;
}
public static OpResult SPLIT(Fiber f, Entity e, FieldScreen s) {
byte bankAXY = f.ReadU8(), bankADBX = f.ReadU8(), bankBYD = f.ReadU8();
short xa = f.ReadS16(), ya = f.ReadS16();
byte da = f.ReadU8();
short xb = f.ReadS16(), yb = f.ReadS16();
byte db = f.ReadU8(), speed = f.ReadU8();
int frame = (int?)f.ResumeState ?? 0;
var entities = s.Entities
.Where(e => e.Character != null)
.Where(e => e.Character != s.Player.Character);
Entity entA = entities.ElementAtOrDefault(0),
entB = entities.ElementAtOrDefault(1);
if ((entA == null) && (entB == null))
throw new InvalidOperationException();
void Process(Entity ent, int x, int y, int d) {
if (frame == 0) {
ent.Model.Translation = s.Player.Model.Translation;
ent.WalkmeshTri = s.Player.WalkmeshTri;
ent.Model.Visible = true;
float rotation = (float)(Math.Atan2(x - s.Player.Model.Translation.X, -(y - s.Player.Model.Translation.Y)) * 180 / Math.PI);
ent.Model.Rotation = new Vector3(0, 0, rotation);
ent.Model.PlayAnimation(1, true, 1f, null); //TODO - should be run, depending on speed
} else if (frame == speed) {
s.DropToWalkmesh(ent, new Vector2(x, y), ent.WalkmeshTri); //TODO - if it's blocked, this won't be right. But that probably shouldn't happen?
float rotation = 360f * d / 255f;
ent.Model.Rotation = new Vector3(0, 0, rotation);
ent.Model.PlayAnimation(0, true, 1f, null);
} else {
var target = Vector2.Lerp(s.Player.Model.Translation.XY(), new Vector2(x, y), 1f * frame / speed);
s.TryWalk(ent, new Vector3(target.X, target.Y, 0), false);
}
}
if (entA != null)
Process(entA,
s.Game.Memory.Read(bankAXY >> 4, xa), s.Game.Memory.Read(bankAXY & 0xf, ya),
s.Game.Memory.Read(bankADBX >> 4, da));
if (entB != null)
Process(entB,
s.Game.Memory.Read(bankADBX & 0xf, ya), s.Game.Memory.Read(bankBYD >> 4, yb),
s.Game.Memory.Read(bankBYD & 0xf, db));
if (frame < speed) {
f.ResumeState = ++frame;
return OpResult.Restart;
} else
return OpResult.Continue;
}
private class JoinState {
public int Frame;
public Vector2 EntAStart, EntBStart;
}
public static OpResult JOIN(Fiber f, Entity e, FieldScreen s) {
byte speed = f.ReadU8();
var entities = s.Entities
.Where(e => e.Character != null)
.Where(e => e.Character != s.Player.Character);
Entity entA = entities.ElementAtOrDefault(0),
entB = entities.ElementAtOrDefault(1);
if ((entA == null) && (entB == null))
throw new InvalidOperationException();
JoinState state;
if (f.ResumeState == null) {
f.ResumeState = state = new JoinState {
Frame = 0,
EntAStart = entA == null ? Vector2.Zero : entA.Model.Translation.XY(),
EntBStart = entB == null ? Vector2.Zero : entB.Model.Translation.XY(),
};
} else
state = (JoinState)f.ResumeState;
void Process(Entity ent, Vector2 start) {
if (state.Frame == 0) {
float rotation = (float)(Math.Atan2(s.Player.Model.Translation.X - ent.Model.Translation.X, -(s.Player.Model.Translation.Y - ent.Model.Translation.Y)) * 180 / Math.PI);
ent.Model.Rotation = new Vector3(0, 0, rotation);
ent.Model.PlayAnimation(1, true, 1f, null); //TODO - should be run, depending on speed
} else if (state.Frame == speed) {
ent.Model.Translation = s.Player.Model.Translation;
ent.Model.Visible = false;
} else {
var target = Vector2.Lerp(start, s.Player.Model.Translation.XY(), 1f * state.Frame / speed);
s.TryWalk(ent, new Vector3(target.X, target.Y, 0), false);
}
}
if (entA != null)
Process(entA, state.EntAStart);
if (entB != null)
Process(entB, state.EntBStart);
if (state.Frame < speed) {
state.Frame++;
return OpResult.Restart;
} else
return OpResult.Continue;
}
public static OpResult PDIRA(Fiber f, Entity e, FieldScreen s) {
byte chr = f.ReadU8();
var target = s.Entities
@ -783,9 +895,11 @@ namespace Braver.Field {
byte anim = f.ReadU8(), first = f.ReadU8(), last = f.ReadU8(), speed = f.ReadU8();
f.Pause();
var state = e.Model.AnimationState;
f.OtherState["AnimPlaying"] = true;
Action onComplete = () => {
f.Resume();
e.Model.AnimationState = state;
f.OtherState["AnimPlaying"] = false;
};
e.Model.PlayAnimation(anim, false, 1f / speed, onComplete, first, last); //TODO is this speed even vaguely correct?
return OpResult.Continue;
@ -794,28 +908,50 @@ namespace Braver.Field {
byte anim = f.ReadU8(), speed = f.ReadU8();
f.Pause();
var state = e.Model.AnimationState;
f.OtherState["AnimPlaying"] = true;
Action onComplete = () => {
f.Resume();
e.Model.AnimationState = state;
f.OtherState["AnimPlaying"] = false;
};
e.Model.PlayAnimation(anim, false, 1f / speed, onComplete); //TODO is this speed even vaguely correct?
return OpResult.Continue;
}
public static OpResult ANIME2(Fiber f, Entity e, FieldScreen s) {
byte anim = f.ReadU8(), speed = f.ReadU8();
var state = e.Model.AnimationState;
f.OtherState["AnimPlaying"] = true;
Action onComplete = () => {
e.Model.AnimationState = state;
f.OtherState["AnimPlaying"] = false;
};
e.Model.PlayAnimation(anim, false, 1f / speed, onComplete);
return OpResult.Continue;
}
public static OpResult ANIM_2(Fiber f, Entity e, FieldScreen s) {
byte anim = f.ReadU8(), speed = f.ReadU8();
f.Pause();
e.Model.PlayAnimation(anim, false, 1f / speed, f.Resume); //TODO is this speed even vaguely correct?
f.OtherState["AnimPlaying"] = true;
e.Model.PlayAnimation(anim, false, 1f / speed, () => {
f.Resume();
f.OtherState["AnimPlaying"] = false;
}); //TODO is this speed even vaguely correct?
return OpResult.Continue;
}
public static OpResult CANM_2(Fiber f, Entity e, FieldScreen s) {
byte anim = f.ReadU8(), fstart = f.ReadU8(), fend = f.ReadU8(), speed = f.ReadU8();
f.Pause();
e.Model.PlayAnimation(anim, false, 1f / speed, f.Resume, fstart, fend);
f.OtherState["AnimPlaying"] = true;
e.Model.PlayAnimation(anim, false, 1f / speed, () => {
f.Resume();
f.OtherState["AnimPlaying"] = false;
}, fstart, fend);
return OpResult.Continue;
}
public static OpResult DFANM(Fiber f, Entity e, FieldScreen s) {
byte anim = f.ReadU8(), speed = f.ReadU8();
e.Model.PlayAnimation(anim, true, 1f / speed, null); //TODO is this speed even vaguely correct?
//TODO - not setting AnimPlaying, is that reasonable?
return OpResult.Continue;
}
public static OpResult ASPED(Fiber f, Entity e, FieldScreen s) {
@ -835,6 +971,9 @@ namespace Braver.Field {
private static OpResult DoTurn(Entity e, float rotation, float rotationSteps, byte rotateDir, byte rotateType) {
float rotationAmount = rotationSteps == 0 ? 360f : 360f * rotationSteps / 255f;
if (rotateDir > 2)
rotateDir = 2; //TODO - it's 10 in elevtr1, so we can expect to see values other than 0/1/2, but how to treat them?
float ccwAmount = rotation > e.Model.Rotation.Z ? (e.Model.Rotation.Z + 360 - rotation) : e.Model.Rotation.Z - rotation,
cwAmount = rotation < e.Model.Rotation.Z ? (rotation + 360 - e.Model.Rotation.Z) : rotation - e.Model.Rotation.Z;
if (rotateDir == 2) {
@ -898,9 +1037,10 @@ namespace Braver.Field {
return OpResult.Continue;
}
public static OpResult ANIMW(Fiber f, Entity e, FieldScreen s) {
f.Pause();
e.Model.AnimationState.AnimationComplete += f.Resume;
return OpResult.Continue;
if ((bool)f.OtherState["AnimPlaying"])
return OpResult.Restart;
else
return OpResult.Continue;
}
public static OpResult SLIDR(Fiber f, Entity e, FieldScreen s) {
@ -1236,7 +1376,10 @@ if (y + h + MIN_WINDOW_DISTANCE > GAME_HEIGHT) { y = GAME_HEIGHT - h - MIN_WINDO
byte span = f.ReadU8();
int sound = s.Game.Memory.Read(banks & 0xf, ssound) - 1;
int pan = s.Game.Memory.Read(banks >> 4, span);
s.Game.Audio.PlaySfx(sound, 1f, (pan - 64) / 64);
if (sound < 0)
s.Game.Audio.StopLoopingSfx();
else
s.Game.Audio.PlaySfx(sound, 1f, (pan - 64) / 64);
return OpResult.Continue;
}
@ -1270,7 +1413,7 @@ if (y + h + MIN_WINDOW_DISTANCE > GAME_HEIGHT) { y = GAME_HEIGHT - h - MIN_WINDO
int x = s.Game.Memory.Read(banks & 0xf, sx),
y = s.Game.Memory.Read(banks >> 4, sy);
s.BGScroll(x, y);
s.BGScroll(x, y, false);
return OpResult.Continue;
}
@ -1278,11 +1421,11 @@ if (y + h + MIN_WINDOW_DISTANCE > GAME_HEIGHT) { y = GAME_HEIGHT - h - MIN_WINDO
if (s.Player == null) {
s.WhenPlayerSet += () => {
var pos = s.ModelToBGPosition(s.Player.Model.Translation);
s.BGScroll(pos.X, pos.Y);
s.BGScroll(pos.X, pos.Y, true);
};
} else {
var pos = s.ModelToBGPosition(s.Player.Model.Translation);
s.BGScroll(pos.X, pos.Y);
s.BGScroll(pos.X, pos.Y, true);
}
return OpResult.Continue;
}
@ -1312,11 +1455,11 @@ if (y + h + MIN_WINDOW_DISTANCE > GAME_HEIGHT) { y = GAME_HEIGHT - h - MIN_WINDO
var progress = Easings.QuadraticInOut(1f * state.Frame / speed); //TODO - is interpreting speed as framecount vaguely correct?
if (progress >= 1f) {
s.BGScroll(x, y);
s.BGScroll(x, y, false);
return OpResult.Continue;
} else {
var pos = state.Start + (end - state.Start) * progress;
s.BGScroll(pos.X, pos.Y);
s.BGScroll(pos.X, pos.Y, false);
state.Frame++;
return OpResult.Restart;
}
@ -1350,13 +1493,14 @@ if (y + h + MIN_WINDOW_DISTANCE > GAME_HEIGHT) { y = GAME_HEIGHT - h - MIN_WINDO
var target = s.ClampBGScrollToViewport(s.ModelToBGPosition(s.Player.Model.Translation));
if (progress >= 1f) {
s.BGScroll(target.X, target.Y);
s.BGScroll(target.X, target.Y, true);
s.Options &= ~FieldOptions.CameraIsAsyncScrolling;
return true;
} else {
s.BGScroll(
curX + (target.X - curX) * progress,
curY + (target.Y - curY) * progress
curY + (target.Y - curY) * progress,
true
);
return false;
}
@ -1381,7 +1525,7 @@ if (y + h + MIN_WINDOW_DISTANCE > GAME_HEIGHT) { y = GAME_HEIGHT - h - MIN_WINDO
f.Pause();
s.StartProcess(frame => {
float progress = 1f * frame / numFrames;
s.BGScroll(curX + (x - curX) * progress, curY + (y - curY) * progress);
s.BGScroll(curX + (x - curX) * progress, curY + (y - curY) * progress, false);
if (progress >= 1) {
f.Resume();

View File

@ -19,6 +19,8 @@ Field - model lighting looks a bit too bright
Music looping works but still a noticeable break at the loop point
Same with audio sfx looping!
Item menu: Icons, actually do arrange
Field Fade network msg, movie msg

View File

@ -224,7 +224,7 @@ namespace Ficedula.FF7 {
private struct SoundEntry {
public int Size;
public int Offset;
public byte[] UNK; //16
public byte[] ExtraData; //16
public byte[] WAVFORMATEX; //18
public ushort SamplesPerBlock;
public ushort NumCoef;
@ -261,7 +261,7 @@ namespace Ficedula.FF7 {
var entry = new SoundEntry {
Size = size,
Offset = s.ReadI32(),
UNK = s.ReadBytes(16),
ExtraData = s.ReadBytes(16),
WAVFORMATEX = s.ReadBytes(18),
SamplesPerBlock = s.ReadU16(),
NumCoef = s.ReadU16(),
@ -292,6 +292,7 @@ namespace Ficedula.FF7 {
dest.Write(buffer, 0, buffer.Length);
}
public byte[] GetExtraData(int soundID) => _entries[soundID].ExtraData;
public byte[] ExportPCM(int soundID, out int frequency, out int channels) {
var entry = _entries[soundID];
byte[] buffer = new byte[entry.Size];