finalburnneo / FBNeo

FinalBurn Neo - We are Team FBNeo.
http://neo-source.com
Other
881 stars 355 forks source link

Dual-stick controls for Forgotten Worlds #1707

Closed pjft closed 3 months ago

pjft commented 4 months ago

Hi all.

A few years ago we implemented dual-stick controls for a few arcade games, but one notable omission was Forgotten Worlds, as the CPS driver was partially off-limits, and not up to the same controls structure as most others - particularly having a "Aim" button, that all the other logic was based on.

At the time, @dinkc64 and myself chatted and suggested that maybe further down the line we could look into this together. I recalled that this weekend and thought I'd check, but it seems that the Forgotten Worlds controls are still the same.

There's no urgency whatsoever, I'm just adding this here as a placeholder, both to show my availability in exploring adding them there at some point, and asking if there'd be interest or availability in updating the CPS drivers to at least conform with the Rotation/Aim logic from the other drivers.

Hope this is not taken as rude, as that's not my intention, and I hope everyone is doing great!

dinkc64 commented 4 months ago

Hi pjft, nice to run into you again! I think this is totally doable, but we're dealing with an insane amount of resolution on the spinner thing compared to the other games.. I think its 64 (its a guess), so, we might have to do about this by using the analog thumbstick or something like that. It's been a while since I worked on this sorta thing, what do you think?

best regards,

pjft commented 4 months ago

Hi! Thanks for the reply - nice hearing from you as well :)

Indeed, the game seems to have 32 directions from my immediate test, so yes, it would require analog. The closest we did back then was Calibre 50, which had 16 directions.

https://github.com/finalburnneo/FBNeo/pull/950/files

And you're right, it's been 2 years since we last looked into this, so it'd require a bit of a warmup before things start to actually work, but happy to help or even follow your guidance as you know the code and logic better than I do. :)

I was thinking something similar to that logic, though double the complexity, after all.

Now, the logic back then was based on converting the analog inputs to the existing Joy2Rotate logic that already existed, where the "Aim" button logic was already implemented.

Since there is no such logic for Forgotten Worlds, we have two options:

Unsure what would be best here, to be honest, though I would need help with figuring out the actual values and addresses in memory for the actual rotation direction. That was already abstracted in Joy2Rotate for the other games, so my job was easier.

It's hard when coding on a Raspberry Pi and using printfs and GDB - I should probably set up a proper development environment at some point!

Let me know your thoughts. I'm happy to take a first look at the CPS driver and see what little I understand of it, and ask questions if it helps?

You let me know - I don't want to be a nuisance here, but I'd be happy to help make this work.

Thanks, and have a great week!

dinkc64 commented 4 months ago

Hi there, I'll try to do my best here, did some hunting and found the memory locations:

Memory locations of the player are: p1: ffcf20, 0-1f, 0 is pointing to the right (3-oclock), and increments clockwise p2: ffcf70, ""

I'm thinking we should do this a little smarter than calibr50's Joy2rotate16, maybe convert the x,y to radians then degrees, then divide it down + offset it to get the value we want (0-1f)? :) I think atan2 might help us here. I'll be able to get the exact math/code or this later today

p.s. don't worry about nuisance thing, that doesn't apply when doing great things :)

best regards,

pjft commented 4 months ago

Oh, wow - you're already ahead of me! All I got up to was set up a spare Pi to compile and test the code, go through the CPS driver code and identified where the dial controls were being registered, reviewed the Calibre 50 code and also booted it up to play it for a brief moment to see how it went!

I had never used atan2, but from reading about it it does seem to do the trick - much more elegantly than the mess of a code I put together for caliber 50!

The analog axis goes from -1 to 1, so using atan2(x,y) will return the angle (or, if it performs as in a code sample I tried out, a fraction of Pi). It seems that:

// up
ATAN2(0, 1) = 0
// right
ATAN2(1, 0) = Pi/2
// down
ATAN2(0, -1) = -Pi/2
// left
ATAN2(-1, 1) = Pi (or -Pi)

so, if we start pointing to the right, we need to transpose the angles here to make for a clean "divide it down + offset" logic. I'm thinking that maybe (don't quote me on that) simply swapping the x and y axis on the ATAN2 function will do the trick to transpose them 90 degrees, so

ATAN2(analog-y-value, analog-x-value) so that when you're aiming right it returns

ATAN2(0,1) = 0

Either way, it seems you're way ahead of me here and probably already were thinking along these lines, so I think that that is a good way to go. How can I help?

Thanks!

Best regards.

barbudreadmon commented 4 months ago

Hmmmm thinking out loud, won't deadzones be a major problem if you need 32 directions ? (thinking about directions next to cardinals)

pjft commented 4 months ago

Hm. Good point.

I am assuming we could just register the input if sqrt(x^2+y^2) > dead_zone_threshold, or are you referring to something different?

Thanks!

barbudreadmon commented 4 months ago

Not sure myself, i'm just wondering if there might not be some bad interactions due to faulty analog sticks and the quirks used to get around it. I might just be overthinking this.

pjft commented 4 months ago

You're probably right on the risks there - I suppose we'll have to see when we get there.

dinkc64 commented 4 months ago

good idea, so, I've thought about this for a few hours (before doing anything), and decided that we should modify pst90s/d_seta's calibr50 to the new degrees method before hooking up forgotten worlds - that way we'll have a known working system that just needs to be shoehorned into the cps driver :)

barbudreadmon, deadzone is not a problem and will be accounted for :)

dinkc64 commented 4 months ago

yay! I got it to return the proper 0-f value calibr 50 wants w/maths :) going to integrate it into the rotation code next, after a little break.

dinkc64 commented 4 months ago

Here's a modified d_seta with the new analog handling code, it seems to work ok :) the -9 offset when scaling from degrees to 0-f is accurate for one of my gamepads, but another crappier one prefers -8. hmm, I wonder how it fares on your side?

seta.zip

best regards,

pjft commented 4 months ago

Wow, this is a great update to wake up to!

Definitely, I'll test it out and get back to you. If the previous version worked well on both controllers, though, then we probably would be able to replicate it somehow. I'll check the code as well and report back.

Thank you for looking into this so promptly! Get some rest please:)

pjft commented 4 months ago

Thanks for putting this together. I tested it out, it played well, but one thing wasn't working well for me which was the lack of a deadzone. Also, your -8 and -9 comment got me thinking that the reason for that to happen is because, in the scale of 0-f, "0" isn't really the range between 0-22.5 degrees, but rather -11.25 to 11.25.

I made a small adjustment to reflect that, see how it works in your controllers. Also, I added a tentative deadzone approach, but ideally this would be handled by ProcessAnalog and only after that would we do the rotation logic, to have it be standardized across all controls. Thoughts?

I also tweaked the atan2 logic so that we don't need to subtract it from 90, and then do the -8/-9 :)

I added PJFT to the comments there where I changed things.

See how it fares on our end, and great job! I hope this helps.

d_seta.cpp.zip

dinkc64 commented 4 months ago

Hi, It works nicely here :) Thanks!

Feel free to change anything if you think it would improve things, when you think it's all good I'll go ahead and shove it into the cps driver, unless you'd like to do that part.

I had the deadzone setup while it was still in the integer domain, (the d_min(40, port)), the 40 was probably just way too small, though! (40 was just a guess) Anything under 40 would just return 0, which explains the test for both floats being 0.0. That's 40 out of +-1023 for most thumbsticks. Changing that to 225 (0.22 * 1023..?) would end up with the same effect, I think :)

best regards,

pjft commented 4 months ago

Thanks!

I missed that the "40" value was supposed to be the deadzone, haha. Makes sense, now that I read it, though!

I think I'll try to see if I can get it to work with ProcessAnalog for the analog input handling, just for consistency's sake.

If you have a clear idea of how to do it in the CPS code, I'd very much appreciate it, as that's the code I'm very much not familiar with! Of course, if you don't have the time, I'm happy to stumble through it with your guidance - let me get this sorted first though!

Thanks, and have a great day.

pjft commented 4 months ago

Ok, here we go. Please double check that I didn't break anything or that I didn't leave any debug code behind, just in case :)

I tested it and it worked as intended for 2 players, with the deadzone from ProcessAnalog! I think I'm comfortable with this version of the code being used to implement the 32-rotation logic for Forgotten Worlds, though as the driver isn't structured the same way as this one it might need a few more changes.

From what I remember from 2 years ago, per your suggestion we made sure to add an "analog auto-fire" option on the Dip Switches section as well, at least, so that it could be toggled by users.

d_seta_v2.cpp.zip

Let me know your thoughts!

Regards.

dinkc64 commented 4 months ago

deeee-cent!! : ) The rotation works great, and I especially like how you improved code clarity, too :)

best regards,

pjft commented 4 months ago

By all means - happy to help! :) Thanks for being willing to look into this!

dinkc64 commented 4 months ago

A little update: got it in, but its a huge mess. :) A neat thing about this one, compared to the other games - well, at first it could never lock onto where it wanted to go, it kept under/overshooting, it turns out - the game could take 0-4 frames to move the rotation, depending on certain things. Since the accumulator for the dial is "absolutely massive", it turns out you can make all of the steps at once, and wait for it to get there - yay!

I'll try to clean it up and post the code tomorrow morning (detroit time)

best regards,

dinkc64 commented 4 months ago

Here's a messy test, it only responds to the analogs right now (not impl.'d yet: button+dpad, dip-autofire, autofire) Maybe you can experiment and try to get it to respond to analogs better? I'm out of ideas for tonight! test.zip

pjft commented 4 months ago

Hi! Good morning :) Thank you so much for this - let me test it out and get back to you. Had an early day at work, and still haven't had a moment, but I'll see what I can get to.

dinkc64 commented 4 months ago

thanks :) Actually, now that I try it with a fresh mind, things seem OK, what do you think? Maybe I'll try to hook up the autofire and button+dpad rotation

best regards,

pjft commented 4 months ago

From reading the code (only that), it seems ok but I'm compiling it and will try it out now.

The only comment I have is that I didn't expect that we'd get rid of the "analog +/-" button inputs that I imagine players are used to, especially in the absence of analog controllers. Maybe this was changed because we adapted the previous code from calibr50, but thought I'd bring it up in case it wasn't a deliberate change.

I'll be honest - it rotates wonderfully, this is fantastic work! Well done :)

One thing that might be on me - it seems to be using the left analog stick to rotate, rather than the right one, but I imagine it's probably on my settings as I'm using the libretro core. Just confirming on your end that it's all working as intended - I know Calibre 50 used the right stick.

Thank you so much! Let me know if there's anything else to test, and when I have the chance I'll also spend some more quality time with this.

Best.

pjft commented 4 months ago

Yeah, after I manually configured the controls to be mapped to the place I expected them to, and played a credit.

Let me tell you that this control scheme makes the game so much more enjoyable - it does beg the question of what would the original developers think about the ideal control mapping if they had dual analog stick controllers back then :) It fits right in!

Thank you so much!

dinkc64 commented 4 months ago

Here's something worth a watch: Todd Tuckey from TNT Amusements did a video about a Forgotten Worlds arcade cabinet; going through the electronics and control panel and things :)

https://youtu.be/0V9eNfil_dw?si=_4Y-UQKThNTZKQee&t=173

(the link skips past all the non-relevant intro stuff)

best regards,

pjft commented 3 months ago

I'll watch it later for sure - thanks for the recommendation! :)

dinkc64 commented 3 months ago

Thanks for testing it out :) Feel free to hook up or work on anything, I won't be able to work on it until tonight around 8pm Detroit time (EST)

I really just did a quick throw-it-in job, so I could get to the point and work on the rotation code. We can put back (or remove) any features you think should be there, anything I did wasn't set in stone, of course :)

best regards,

pjft commented 3 months ago

Ok, so I tried a few things but I'm kind of stuck now.

I fixed a few leftover warnings from the DIP switch changes, and added back the rotate left/right with buttons.

I think I prepared the dip switch for the autofire, but now I don't quite know how to actually force the autofire to happen. In Calibre 50 it was easier to read the code and find out where this was being read from/set to, but here I'm a bit lost.

Still, assuming I haven't broken anything, see if this helps.

The default to the left stick will require a change in Libretro's fork, in retro_input.cpp.

https://github.com/libretro/FBNeo/blob/925fe1e34894e521dde3bf029876f660d4d758c3/src/burner/libretro/retro_input.cpp#L1861

Unsure of how to proceed in that regard @barbudreadmon ?

See if the code helps here. Thanks.

cps.zip

pjft commented 3 months ago

--- just to add, thanks for sharing that video, it was really nice watching all the internals and him going through how the rotary dial worked! Watching him playing it with the rotary dial seems to suggest that we're a lot better off with the analog twin-stick controls :)

dinkc64 commented 3 months ago

Haha right on, but well - nothing beats a game of tempest with a real spinner! :)

I added your latest changes to mine, hooked up autofire & button+dpad rotate, cleaned a few things up.

There's a problem though, the game has 2 spinner modes: 1 when shooting, and the other while not shooting.
The second -- not shooting mode, is real slow to move, where as spinner + shooting updates almost instantly. I can get one working perfect, and the other goes bonkers. Err, I can get both working perfectly, but not together. Guess I'll sleep on it and see what happens tomorrow! :)

best regards,

dinkc64 commented 3 months ago

Here's the latest code, make sure to get the update to cps_run.cpp which was added to git earlier, it allows for accessing the compiled inputs & modifying them in the frame callback (for the autofire thing) If no firing is happening (or move+shoots mode is off in dips), movement of the spinner will be erratic <- it's normal and not fixed yet :)

withfire.zip

best regards,

pjft commented 3 months ago

Thanks! I'll look into this. I tested with the current (and live) builds, and the rotation speed seems to be the same for the user at least, regardless of whether you're firing or not, and since we're aiming with analog precision, I would imagine that - if push comes to shove - we do not need to have the slow movement mode. But I'll get back to you here - thank you!

barbudreadmon commented 3 months ago

Unsure of how to proceed in that regard @barbudreadmon ?

I'll look into this after the analog code is on git, it shouldn't be extremely hard to move that mapping

pjft commented 3 months ago

Oh, by all means - I'm happy to submit the PR and you merge it when the CPS code is merged as well.

Just making sure you were in the loop to know that it did require that change after the CPS code :)

Thanks!

pjft commented 3 months ago

Ok, so... I got it working, but there are a few... weird things. But I've been at it for a while and I'm as puzzled as you probably are, so I went back to the old code and tried to follow your changes until things worked.

1) The code from yesterday worked well with the two speeds, when firing and without firing. Both the one you sent as well as the one I sent. The main difference was in RotateDoTick and a code that you had commented out in another file. I restored those and they now seem to be working - I only tested 1 player, so please confirm whether things are working as expected. Same for the button rotate logic. 2) I am finding a very weird behavior, that I don't like at all, as it leaves me uncomfortable in that maybe we are incorrectly accessing or setting memory? For some reason, in the code you sent today, the Coin DIP switch defaults changed to 4 coins 1 credit rather than the previous default 1 coin 1 credit. It might be a very simple, obvious and naive thing, but since back in the Calibre 50 developments I know I messed up memory addresses and what not, causing game corruption, could you please double check:

a) that my proposal for the DIP switch changes effectively isn't misbehaving (addresses chosen, etc?) b) that whatever happened between the code I sent you yesterday without the autofire, and the code you returned, it was expected that the defaults would change?

I might be acting paranoid here, but that rang an alarm in my head when I was inserting coins and it took a lot longer for the Credits to show up. The truth is, I am very much unfamiliar with a lot of what's expected in the backend, so I'm very wary of breaking things in subtle ways that we only notice further down the line! :)

Either way, here's the code. I messed with a lot of things back and forth, but I think this is what you need to get it working there. If not, let me know - I might have missed another file.

cps_working.zip

Also, @barbudreadmon , the code we need to add is just the last two conditions in retro_input.cpp:

// Forgotten Worlds
        if ((parentrom && strcmp(parentrom, "forgottn") == 0) ||
                (drvname && strcmp(drvname, "forgottn") == 0)
        ) {
                if (strcmp("Turn (analog)", description) == 0) {
                        GameInpAnalog2RetroInpAnalog(pgi, nPlayer, RETRO_DEVICE_ID_ANALOG_X, RETRO_DEVICE_INDEX_ANALOG_RIGHT, description);
                }
                if (strcmp("Attack", description) == 0) {
                        GameInpDigital2RetroInpKey(pgi, nPlayer, RETRO_DEVICE_ID_JOYPAD_R, description);
                }
                if (strcmp("Turn - (digital)", description) == 0) {
                        GameInpDigital2RetroInpKey(pgi, nPlayer, RETRO_DEVICE_ID_JOYPAD_B, description);
                }
                if (strcmp("Turn + (digital)", description) == 0) {
                        GameInpDigital2RetroInpKey(pgi, nPlayer, RETRO_DEVICE_ID_JOYPAD_A, description);
                }
                if (strcmp("Aim X", description) == 0) {
                        GameInpAnalog2RetroInpAnalog(pgi, nPlayer, RETRO_DEVICE_ID_ANALOG_X, RETRO_DEVICE_INDEX_ANALOG_RIGHT, description);
                }
                if (strcmp("Aim Y", description) == 0) {
                        GameInpAnalog2RetroInpAnalog(pgi, nPlayer, RETRO_DEVICE_ID_ANALOG_Y, RETRO_DEVICE_INDEX_ANALOG_RIGHT, description);
                }
        }

If you confirm that this sounds right (at least it works here) I'm happy to send over a PR.

Thank you so much for all the investment here, this is looking great! :)

dinkc64 commented 3 months ago

Regarding the weirdness: you had the dip offset +1 (0x1b), so it was accessing the wrong entry, I corrected it to 0x1a after wondering why the dip wouldn't set, and also used an unused bit in fFakeDIP :) I'll respond with more soon

dinkc64 commented 3 months ago

So the problem might not be immediately obvious, because it only happens when you make a huge movement, for example: going from one direction to the opposite - it'll most likely get it wrong.
You'll notice also that it is jumpy in moving with this version(that you posted), as it attempts to verify that the value in memory is moving before sending the next step of movements.

dinkc64 commented 3 months ago

Sorry about the multiple posts :)

So, the one from last night(withfire.zip): you can go absolutely bonkers with the second stick, and it'll respond in a more smoothly & accurate fashion, but only when fireing. I'll try to add the memory verification part for when moving without fireing, and without for w/fire, and see how that goes.

pjft commented 3 months ago

Good morning! No need to apologize at all :)

Hm, I see. Well, give the current code a try. At least, from what I test, and going fairly random with the analog stick, it seems to respond as intended. I did very easily replicate the "wonkiness" when not firing in the code you sent earlier, but not with the current one, I think.

If you aren't experiencing that, then I need to see what might be different here. :)

On my end (I'm running the libretro fork) these are the modified files:

    modified:   src/burn/drv/capcom/cps_run.cpp
    modified:   src/burn/drv/capcom/cps_rw.cpp
    modified:   src/burn/drv/capcom/d_cps1.cpp
    modified:   src/burner/libretro/retro_input.cpp

So let me know how it goes. :)

Yes, I had set it to the previous offset because I also had added a "DIP E" entry in the configuration. When we removed it, you're right, the offset needs to change. Still, it's not clear to me why the default value changed. But I may be making too much of a big deal out of it.

pjft commented 3 months ago

For what it's worth, the only thing I observe is that, if you're moving 180 degrees, it will at times do one slight additional rotation before going back and settling in the right position. I think that's ok, though I get that it's not ideal. :)

Also, my code has unnecessary std::cout and iostream included, so do remove them 😅 bprintf doesn't work from inside RetroArch, so that's how I get print logs.

dinkc64 commented 3 months ago

I get it, but, I won't give up until its at least mostly perfect, stay tuned :)

dinkc64 commented 3 months ago

A source of the problem is the memory addresses, they take up to 10 frames to start registering movement, even though the player is moving. I found another address which updates in 3 frames, but its in some weird encoding (address&7ff / 0x40 gets 0-x31, hmmm) Been searching all afternoon falls asleep on desk

pjft commented 3 months ago

Oh my. I get the frustration!

I was trying to tweak things here as well - not at the source, but at the symptom level - but it's almost as if the distance requested when larger than needs to be shortened. I adjusted the rotation value to be just 1 per cycle, to rotate really slow, and you're right: even when it's slow, it will still first slightly over-shoot, before settling in on the right location.

I'll try to come up with some workarounds here on the symptoms front.

Thanks!

pjft commented 3 months ago

Ok, so I've been just messing around with some parameters but I can't quite get anything to work. This is painful!

So, when we're shooting, each frame has a rotation, except when it gets near the destination, and then it slows down. But when we're not shooting, it seems that every 8 frames it rotates, but it's sets of (5 then 3) frames. Unclear if that's caused by us, or by the actual hardware. But the truth is that it does indeed over-rotate, but it's weird:

Maybe this pattern helps you somewhat? I don't know.

pjft commented 3 months ago

.........sigh.

Ok, hear me out. Before we start reconsidering our life choices in tackling this game, I was actually getting my kid to go through a playthrough of the game and, at some point later in the game he was losing so much that for some reason after several continues he started without the satellite. At that moment, all the rotation logic went berserk.

I was a bit puzzled about it, but it seems that it is possible to lose the satellite. In fact, if you run forgottnu, the US version starts without the satellite and it's an easy way to replicate that behavior.

......

I suspect that it is reading things from a different address or something, because it will never stop rotating once it starts and you keep holding a direction.

I'm sorry for not bringing solutions, just... observations from trying to troubleshoot this!

dinkc64 commented 3 months ago

Well, remember - we control the machine, not the other way :) Anything can be done, it's just a matter of finding a solution that works with what we have.

Thanks for the input, I'll see what can be done!

dinkc64 commented 3 months ago

I need a favor, can you rotate Joy2Rotate for me? the (value -2) 4 is equal to the movement of the player in forgottn, I'd rather have it value 4 :) (pointing right being 0 instead of up). I tried doing this 3 times and screwed it up each time.

`` (i can never get the code-paste thing to work on github, either.. grr.)

static UINT8 Joy2Rotate(UINT8 *joy) { if (joy[3] && joy[1]) return 7; // up left if (joy[3] && joy[0]) return 1; // up right

if (joy[2] && joy[1]) return 5;    // down left
if (joy[2] && joy[0]) return 3;    // down right

if (joy[3]) return 0;    // up
if (joy[2]) return 4;    // down
if (joy[1]) return 6;    // left
if (joy[0]) return 2;    // right

return 0xff;

}

dinkc64 commented 3 months ago

And here we go, I'm finally happy with this: (note: cps_rw.cpp actually changed this time as well)

perfect.zip

notes: this is based off "withfire.zip" from the other day, if there is something you've added that you want to keep, it'll need to be re-merged :)

note2self: add a "w/o fire" button, to inhibit fire-ing when "shoots and moves" is selected in the dips. (no fireing + movement re-orients the satelite) (or do a mutual exclusion with the main fire button?)

best regards,

pjft commented 3 months ago

Oh, that was fast! So you no longer need the changes to Joy2Rotate then? :)

I don't think there's anything I'd want re-added, I'll check the code once again but I trust you there. The only reason I had changed things was in my attempt to reconstruct things.

I agree we need the button to inhibit fire - I noticed it during the playthrough. Maybe the fire button could do it, as long as we keep the double-tap to use the bomb?

dinkc64 commented 3 months ago

yes, def. need joy2rotate rotated :) have to run, back tonight!