Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Event Bus implementation & Event-based multiplayer updates #6826

Merged
merged 6 commits into from
May 21, 2022

Conversation

Azzurite
Copy link
Collaborator

@Azzurite Azzurite commented May 15, 2022

This is still just another PR in preparation for the feature I actually want to add :D

In total, this is basically just a refactoring, there should be basically no observable changes, except where otherwise noted.

A central event bus is added. It is based on standard kotlin classes that extend Event. This imo makes it very nice to work with since you can add any data with the event you like, immediately with no boilerplate code. You also can see all events just by telling your IDE to list all implementing classes. In addition, it was requested that it not be asynchronous/threaded for better debuggability, so you are only supposed to use it on the main render thread. This thread was chosen because when you listen for events, you mostly want to do something with the UI immediately based on the event. Otherwise I hope it's pretty simple & self-explanatory.

(This "non-multithreaded requirement" also means I can't show off very nice coroutine features :D the whole EventBus file could be shrinked to 10 lines with added asynchronous support, at the expense of being able to step through from send->receive in the debugger)

The other part of this PR is a major refactor of the multiplayer logic to be centralized in their own classes in com.unciv.logic.multiplayer instead of being strewn around various Screens. And instead of using the "throw away everything and rebuild based on the newest data"-approach that has mostly been used so far, it instead now uses events from the event bus to listen for changes and only update the specific components that need updating.

(This is not really noticeable in the MultiplayerScreen because not much is happening there, but when you extend this approach to more performance-critical parts of the game, this will get us major performance benefits. We currently "if something changed, throw away things and rebuild based on the newest data" a lot of things when a WorldScreen.update happens - if all those things would not need a "has updated" check and only update the small parts that actually change, the whole game would be lightning-fast)

More possibly interesting comments as code comments down below:

@Azzurite Azzurite force-pushed the parallel-multiplayer branch from 078a5a6 to f6a4d5f Compare May 15, 2022 22:49
@@ -127,4 +129,5 @@ dependencies {
// run `./gradlew build --scan` to see details
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.work:work-runtime-ktx:2.6.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
Copy link
Collaborator Author

@Azzurite Azzurite May 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This increases the apk size by 3.431kB, but gives us almost all newer Java APIs even on old devices. I mainly included this for java.time (kotlin.time seems to basically only be for performance testing/debugging).

I can remove this if this size increase is not acceptable, it does increase the apk size by roughly 30% from around 11.5 to around 15 MB, but I would rather not :D me likey newey featurey

Copy link
Contributor

@touhidurrr touhidurrr May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember arguing over desugaring once. When Unciv wanted to bump its minimum android sdk version. But it was shot down.

delay(getRefreshInterval().toMillis())

// TODO will be used later
// requestUpdate()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This'll be used when my ingame multiplayer status display is done.

Comment on lines +270 to +271
fun GameInfoPreview.isUsersTurn() = getCivilization(currentPlayer).playerId == UncivGame.Current.settings.userId
fun GameInfo.isUsersTurn() = getCivilization(currentPlayer).playerId == UncivGame.Current.settings.userId
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of putting these into the actual classes, but they are solely multiplayer functions. I think it's good for them to be here.



/** @see getUpdateThrottleInterval */
private const val DROPBOX_THROTTLE_INTERVAL = 8L
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that when a user spams the "Refresh list" button, they will only get an update every 8 seconds of doing that.

I think this is more than acceptable with our rate limiting issues there.

}
private class GameList(
onSelected: (String) -> Unit
) : VerticalGroup() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know Table are being used mostly, and I now understand why. But in this case, a VerticalGroup makes much more sense, because it is able to remove/add and correctly layout actors all the time. With a table, removing/editing a cell is possible, but annoying, it doesn't happen automatically. Also it has the nice benefit of allowing us to do children.sort() to have the games always listed in alphabetical order.

val turnIndicator = createIndicator("OtherIcons/ExclamationMark")
val errorIndicator = createIndicator("StatIcons/Malcontent")
val refreshIndicator = createIndicator("EmojiIcons/Turn")
val statusIndicators = HorizontalGroup()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with the VerticalGroup comment above. Much better remove/add handling.


private fun createIndicator(imagePath: String): Actor {
val image = ImageGetter.getImage(imagePath)
image.setSizeForced(50f)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can imagine this was one of the main problems when trying to use anything other than Table. This makes it nice and dandy though. (See later comment)

Comment on lines 173 to 174
* Either [Image] or [com.badlogic.gdx.scenes.scene2d.utils.Drawable] is badly written, as the [Image.drawable] always has its texture size as min size,
* and [Image] pref size is always equal to its [Image.drawable]'s min size.
Copy link
Collaborator Author

@Azzurite Azzurite May 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the crux of the issue with Images & putting them in anything else than Tables. All the WidgetGroups like the horizontal/vertical one use the child actor's prefWidth/Height to set the size. Because of the design flaw outlined in this method comment, doing Image.setSize does absolutely nothing since the Drawables minWidth/Height is used as prefWidth/Height, and the actual width/height properties are completely ignored for prefWidth/Height... utter stupidity.

Table actually ignores the prefWidth/Height of any actors within cells if you do Cell.size(..), which is why that works so nicely. But there's no reason why the other WidgetGroups shouldn't be used, they do have their place & time.

Comment on lines -676 to -682
} catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable {
val cantUploadNewGamePopup = Popup(this@WorldScreen)
cantUploadNewGamePopup.addGoodSizedLabel("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds").row()
cantUploadNewGamePopup.addCloseButton()
cantUploadNewGamePopup.open()
}
Copy link
Collaborator Author

@Azzurite Azzurite May 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to reduced code duplication, this is also a bugfix: in the other catch (ex: Exception) branch is where isPlayersTurn and shouldUpdate are set to true, but not in this branch.

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 15, 2022

Also,

private suspend fun loadLatestMultiplayerState() {

is not adjusted on purpose, since that will also be changed with the ingame multiplayer status display.

Comment on lines +127 to +129
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId).asPreview()
fileHandle = GameSaver.saveGame(gamePreview, saveFileName)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added on 2021-12-13, can this be removed? How long until old saves on Dropbox get deleted?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the open problems around here: No auto cleanup on "Unciv's" Dropbox.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I manually remove a lot of them every time the dropbox fills up. Currently, all save games up to (but excluding) 2021-10-31 are removed from the dropbox

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes there was some python script and idea to make it a github action...

@itanasi
Copy link
Contributor

itanasi commented May 17, 2022

This would have been a nice way to track which cities need to re-evaluate in my PR #6650

@Azzurite
Copy link
Collaborator Author

This would have been a nice way to track which cities need to re-evaluate

And this is exactly why I want to add this. It's so incredibly useful.

@Azzurite Azzurite changed the base branch from master to SomeTroglodyte-patch-1 May 17, 2022 15:48
@Azzurite Azzurite changed the base branch from SomeTroglodyte-patch-1 to master May 17, 2022 15:48
@Azzurite Azzurite force-pushed the parallel-multiplayer branch from f6a4d5f to 56323f3 Compare May 17, 2022 15:49
@Azzurite Azzurite marked this pull request as ready for review May 17, 2022 15:49
@Azzurite
Copy link
Collaborator Author

I rebased this branch on master - no conflicts

@SomeTroglodyte
Copy link
Collaborator

Six pages essentially a monologue so far... OK I'll read the beast.

open class Event

OK, this is key. In fact, I must find an excuse to disparage a 16-char oneliner.... ... .. . OK: Probability for Name conflicts with std libraries too high? Auto-Imports of Group, Array, Timer etc all run into problems that maybe the wrong one is chosen... Extra namespace "EventBus.Event" (need for qualifier can be import-tweaked away if needed - per-file)? "UncivEvent" (too long)?. "Efent" (:rofl:)? We'll probably just have to be careful. Oh, and please end all new files with a LF for prettier github rendering.

java.lang.ref.WeakReference

There's no kotlin equivalent? Even if it turns out to be just a typealias...

private fun <T: Event> receive

This does not actually receive, right? It's more a listen to or register handler or somesuch. Oh, private, the meat is elsewhere. never mind - yes, the dox of the actual meat make it clear. (Besides, isn't the 'recieve' typo obligatory by now?)

class MultiplayerGameUpdateErrored(...) : Event()

'Errored' - uh, okay, can't think of a better name either. And - why is Event an open class instead of an interface anyway? Would save Gigabytes of () all over the place...

At the UI stuff I started to skip stuff, trust will do.
I like the EventBus design and can't wait to see all our Stats updates riding it.
So...

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 18, 2022

Name conflicts

I never understood this argument ever. The type system will tell you if you used the wrong type, since as soon as you try to interface with existing code, it'll tell you Type mismatch and you'll figure it out from there.

There's no kotlin equivalent?

I pressed Ctrl+Space, and that was the only one showing up. So I guess not.
image

isn't the 'recieve' typo obligatory

You should know by now that for me, no typo is obligatory :D I even use "well" instead of "good" when being asked "How are you?"

why is Event an open class instead of an interface anyway?

Because of a very good reason: brainfart :D I first did the "standard" stuff with members like EventType and EventData but then deleted all that and made it class-based and didn't notice that the open class does now not make sense.

@Azzurite
Copy link
Collaborator Author

'Errored' - uh, okay, can't think of a better name either

Failed?

@SomeTroglodyte
Copy link
Collaborator

when being asked "How are you?"

Good ™️ for you. When asked that I always faint. 👽 👨‍⚕️

Name conflicts

We had precedences. Java math where using kotlin's consistently would be better, and one with just a tiny little more weight I forget? And for some Gdx classes I always think "does everybody on the project know not to pick the first offer"? Not important.

There's no kotlin equivalent?

That one had a link. Duckduck instead of Studio. May be out of context, didn't really dive in.

@Azzurite
Copy link
Collaborator Author

That link is to https://kotlinlang.org/docs/native-overview.html

@SomeTroglodyte
Copy link
Collaborator

Ah, out of context. Got it. 🤦‍♀️

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 18, 2022

After adding the possibility to filter events, I forgot to add that to the EventReceiver... so the filters got garbage collected. I fixed this with the most recent commit.

@Azzurite
Copy link
Collaborator Author

Found another issue - the setSizeForced approach does not work. I missed that all our drawables are pooled. So we can't set the size of one without affecting all the others. I'm working on a fix.

@SomeTroglodyte
Copy link
Collaborator

setSizeForced

Not really part of Event Bus, right? .... Split it off?

@Azzurite Azzurite marked this pull request as draft May 20, 2022 16:40
# Conflicts:
#	core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt
@Azzurite Azzurite marked this pull request as ready for review May 21, 2022 21:44
@Azzurite
Copy link
Collaborator Author

@SomeTroglodyte I resolved the conflict, it was just an unused method removal from the warnings cleanup.

@Azzurite
Copy link
Collaborator Author

@yairm210 did you want to look at this still? I implemented your request of the event bus staying mostly async-free, i.e. it's only ran from a single thread. Unfortunately if you already have code that is asynchronous you can't get around some debug issues where you can't quite step through everything, because you already are starting other threads.

Otherwise I don't think anyone else will be looking at this soon so I'd merge it :D

@SomeTroglodyte
Copy link
Collaborator

I see the merge was easy, yes. I reaffirm I think this will be a boon - we'll just have to get people to actually use it. Recalc stats, reassign population,...

Merge with just our two votes? Or request a review from someone specific first?

@Azzurite
Copy link
Collaborator Author

Yeah I think I'll merge, I want that other stuff that is based on this in here faster :) if @yairm210 has any concerns we can still make changes after :)

@Azzurite Azzurite merged commit 244f947 into yairm210:master May 21, 2022
@Azzurite Azzurite deleted the parallel-multiplayer branch May 21, 2022 22:05
@yairm210
Copy link
Owner

I think that once we start implementing things using this, we should add tests to validate previous behavior, and ensure that that behavior remains after refactor

@GGGuenni
Copy link
Collaborator

GGGuenni commented May 22, 2022

This has 11 Call requires API level 26 (current min is 21) errors in OnlineMultiplayer.kt. All related to java.time. Doesn't this result in unciv being unable to run on Lollipop, Marshmallow, and Nougat?

but gives us almost all newer Java APIs even on old devices. I mainly included this for java.time

Looks like I didn't read properly ¯\_(ツ)_/¯ So is there a way to suppress these without suppressing them all manually?
Good old invalidate caches fixed it again.

@touhidurrr
Copy link
Contributor

touhidurrr commented Oct 11, 2022 via email

@touhidurrr
Copy link
Contributor

touhidurrr commented Oct 11, 2022 via email

@itanasi
Copy link
Contributor

itanasi commented Oct 11, 2022

Whatever you define as an event

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants