From 7babcfaf6bd1342854f92cb696499df575df3a0c Mon Sep 17 00:00:00 2001 From: KarimAkra Date: Mon, 14 Apr 2025 19:39:43 +0300 Subject: [PATCH] Implement extension-admob for mobile ads forgor that videos and fixed banner size make this a define keep thes demo units when using TESTING_ADS only always enable it on debug builds (?) Update AdMobUtil.hx admob features Update PlayState.hx Update AdMobUtil.hx Update AdMobUtil.hx Show privacy options form if required. Update Main.hx Update Main.hx Update PlayState.hx Update AdMobUtil.hx Update AdMobUtil.hx Update AdMobUtil.hx Co-Authored-By: Mihai Alexandru <77043862+majigsaw77@users.noreply.github.com> Co-Authored-By: sector-a <82838084+sector-a@users.noreply.github.com> --- hmm.json | 7 + project.hxp | 47 ++++++- source/Main.hx | 4 + source/funkin/mobile/util/AdMobUtil.hx | 154 +++++++++++++++++++++ source/funkin/play/PlayState.hx | 38 +++++ source/funkin/util/Constants.hx | 14 ++ source/funkin/util/logging/CrashHandler.hx | 13 +- 7 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 source/funkin/mobile/util/AdMobUtil.hx diff --git a/hmm.json b/hmm.json index 73136485a..2e3fc11bd 100644 --- a/hmm.json +++ b/hmm.json @@ -208,6 +208,13 @@ "dir": null, "ref": "bdb191fe7cf745c02a980749906dbf22719e200b", "url": "https://github.com/fponticelli/thx.semver" + }, + { + "name": "extension-admob", + "type": "git", + "dir": null, + "ref": null, + "url": "https://github.com/mcagabe19-stuff/extension-admob" } ] } diff --git a/project.hxp b/project.hxp index 3ce4dcda1..740fdb0e1 100644 --- a/project.hxp +++ b/project.hxp @@ -280,12 +280,24 @@ class Project extends HXProject { */ static final FEATURE_GHOST_TAPPING:FeatureFlag = "FEATURE_GHOST_TAPPING"; - /** + /** * `-DEXCLUDE_ARMV7` * If this flag is enabled, armv7 won't be compiled when building android. */ static final EXCLUDE_ARMV7:FeatureFlag = "EXCLUDE_ARMV7"; + /** + * `-DDISABLE_ADMOB_ADS` + * If this flag is enabled, ADs from AdMob will be disabled. + */ + static final DISABLE_ADMOB_ADS:FeatureFlag = "DISABLE_ADMOB_ADS"; + + /** + * `-DTESTING_ADS` + * If this flag is enabled, ADs will use testing mode. + */ + static final TESTING_ADS:FeatureFlag = "TESTING_ADS"; + // // CONFIGURATION FUNCTIONS // @@ -306,6 +318,11 @@ class Project extends HXProject { configureHaxelibs(); configureAssets(); configureIcons(); + + if (DISABLE_ADMOB_ADS.isDisabled(this)) + { + configureAdMobKeys(); + } } /** @@ -510,6 +527,8 @@ class Project extends HXProject { // TODO: Thanks to new changes to FlxTouch and such, this became useless. Find a way to make it work again -Zack. MOUSE_TO_TOUCH.apply(this, false); EXCLUDE_ARMV7.apply(this, false); + DISABLE_ADMOB_ADS.apply(this, !isMobile()); + TESTING_ADS.apply(this, isDebug()); // Should be true unless explicitly requested. HARDCODED_CREDITS.apply(this, true); @@ -741,6 +760,10 @@ class Project extends HXProject { if (FEATURE_PARTIAL_SOUNDS.isEnabled(this)) { addHaxelib('FlxPartialSound'); // Partial sound } + + if (DISABLE_ADMOB_ADS.isDisabled(this)) { + addHaxelib('extension-admob'); // Ads Extension + } } function configureAssets() { @@ -813,6 +836,22 @@ class Project extends HXProject { addIcon("art/iconOG.png"); } + /** + * Configure the platforms' Ad Mob application keys. + */ + function configureAdMobKeys() { + if (isAndroid()) + { + setEnv("ADMOB_APPID", ""); + } + + if (isIOS()) + { + // This ID is not from us, it's a test ID provided by AdMob. not really sure if it works. + setEnv("ADMOB_APPID", ""); + } + } + // // HELPER FUNCTIONS // Easy functions to make the code more readable. @@ -916,6 +955,12 @@ class Project extends HXProject { return this.defines.exists(name); } + public function setEnv(name:String, value:String = '') { + this.defines.set(name, value); + this.environment.set(name, value); + this.setenv(name, value); + } + /** * Add a library to the list of dependencies for the project. * @param name The name of the library to add. diff --git a/source/Main.hx b/source/Main.hx index ae4eaaf36..8cb96eaae 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -128,6 +128,10 @@ class Main extends Sprite @:privateAccess game._customSoundTray = funkin.ui.options.FunkinSoundTray; + #if NO_DISABLE_ADMOB_ADS + funkin.mobile.util.AdMobUtil.init(); + #end + addChild(game); #if FEATURE_DEBUG_FUNCTIONS diff --git a/source/funkin/mobile/util/AdMobUtil.hx b/source/funkin/mobile/util/AdMobUtil.hx new file mode 100644 index 000000000..cbfbfe158 --- /dev/null +++ b/source/funkin/mobile/util/AdMobUtil.hx @@ -0,0 +1,154 @@ +package funkin.mobile.util; + +import extension.admob.Admob; +import extension.admob.AdmobEvent; + +/** + * Utility class for managing AdMob advertisements in a mobile application. + * This class provides functions to initialize AdMob, manage ad events, + * and control the display of different ad types, including banners, + * interstitial ads, and rewarded ads. + */ +class AdMobUtil +{ + /** + * Production ad unit IDs (to be set for actual ad campaigns). + */ + #if !TESTING_ADS + private static final BANNER_AD_UNIT_ID:String = ""; + private static final INTERSTITIAL_AD_UNIT_ID:String = ""; + private static final INTERSTITIAL_VIDEO_AD_UNIT_ID:String = ""; + private static final REWARDED_AD_UNIT_ID:String = ""; + #else + + /** + * Test ad unit ID for banner ads, used during development. + */ + private static final BANNER_AD_UNIT_ID:String = #if android "ca-app-pub-3940256099942544/6300978111" #elseif ios "ca-app-pub-3940256099942544/2934735716" #else "" #end; + + /** + * Test ad unit ID for interstitial ads, used during development. + */ + private static final INTERSTITIAL_AD_UNIT_ID:String = #if android "ca-app-pub-3940256099942544/1033173712" #elseif ios "ca-app-pub-3940256099942544/4411468910" #else "" #end; + + /** + * Test ad unit ID for interstitial video ads, used during development. + */ + private static final INTERSTITIAL_VIDEO_AD_UNIT_ID:String = #if android "ca-app-pub-3940256099942544/8691691433" #elseif ios "ca-app-pub-3940256099942544/5135589807" #else "" #end; + + /** + * Test ad unit ID for rewarded ads, used during development. + */ + private static final REWARDED_AD_UNIT_ID:String = #if android "ca-app-pub-3940256099942544/5224354917" #elseif ios "ca-app-pub-3940256099942544/1712485313" #else "" #end; + #end + + /** + * Initializes the AdMob SDK and sets up event listeners for interstitial and rewarded ads. + * The listeners display ads automatically when they are loaded. + */ + public static inline function init():Void + { + Admob.status.addEventListener(AdmobEvent.INTERSTITIAL_LOADED, (ae:AdmobEvent) -> Admob.showInterstitial()); + Admob.status.addEventListener(AdmobEvent.REWARDED_LOADED, (ar:AdmobEvent) -> Admob.showRewarded()); + Admob.init(#if TESTING_ADS true #else false #end); + } + + /** + * Loads an interstitial ad. Depending on the forceVideo parameter or random selection, + * it loads either a video or a standard interstitial ad. + * @param forceVideo If true, forces loading an interstitial video ad. Otherwise, it uses a 50% chance. + */ + public static inline function loadInterstitial(forceVideo:Bool = false):Void + { + Admob.loadInterstitial((forceVideo || FlxG.random.bool(50)) ? AdMobUtil.INTERSTITIAL_VIDEO_AD_UNIT_ID : AdMobUtil.INTERSTITIAL_AD_UNIT_ID); + } + + /** + * Adds a banner ad at the specified size and alignment. + * @param size The size of the banner ad, defaulting to the standard banner size. + * @param align The alignment of the banner ad, defaulting to the bottom of the screen. + */ + public static inline function addBanner(size:Int = Admob.BANNER_SIZE_BANNER, align:Int = Admob.BANNER_ALIGN_BOTTOM):Void + { + Admob.showBanner(AdMobUtil.BANNER_AD_UNIT_ID, size, align); + } + + /** + * Removes the currently displayed banner ad, if any. + */ + public static inline function removeBanner():Void + { + Admob.hideBanner(); + } + + /** + * Loads a rewarded ad, preparing it to be displayed once loaded. + * Rewarded ads typically grant rewards upon user completion, like game items or bonuses. + */ + public static inline function loadRewarded():Void + { + Admob.loadRewarded(AdMobUtil.REWARDED_AD_UNIT_ID); + } + + /** + * Sets the volume level for ads with sound, allowing control over ad audio playback. + * @param volume A Float representing the desired volume (0 = mute, 1 = full volume). + */ + public static inline function setVolume(volume:Float):Void + { + Admob.setVolume(volume); + } + + /** + * Checks whether consent for a specific advertising purpose has been granted. + * @param purpose The purpose for which consent is required. + * @return An Int indicating consent status (-1 for no consent, 1 for granted). + */ + public static inline function hasConsentForPurpose(purpose:Int):Int + { + return Admob.hasConsentForPurpose(purpose); + } + + /** + * Checks if the user has given consent for all ad purposes. + * This is typically required for GDPR compliance, where each purpose (0-9) needs to be individually consented. + * @return Bool indicating whether the user has consented to all purposes. + */ + public static function hasFullConsent():Bool + { + for (purpose in 0...10) + { + if (Admob.hasConsentForPurpose(purpose) != 1) return false; + } + + return true; + } + + /** + * Retrieves the current user's consent status as a string. + * Useful for GDPR compliance to understand if ads can be personalized. + * @return A String with the consent status. + */ + public static inline function getConsent():String + { + return Admob.getConsent(); + } + + /** + * Determines if showing a privacy options form is required based on regional laws. + * @return A Bool indicating if a privacy options form is required (true if required, false if not required). + */ + public static inline function isPrivacyOptionsRequired():Bool + { + return Admob.isPrivacyOptionsRequired() == 1; + } + + /** + * Displays the privacy options form to the user, allowing them to adjust consent. + * Useful for GDPR and other privacy regulations compliance. + */ + public static inline function showPrivacyOptionsForm():Void + { + Admob.showPrivacyOptionsForm(); + } +} diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c4842d1f3..83ca710f7 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -58,6 +58,9 @@ import haxe.Int64; #if mobile import funkin.mobile.util.TouchUtil; import funkin.mobile.ui.FunkinHitbox; +#if NO_DISABLE_ADMOB_ADS +import funkin.mobile.util.AdMobUtil; +#end #end #if FEATURE_DISCORD_RPC import funkin.api.discord.DiscordClient; @@ -987,6 +990,11 @@ class PlayState extends MusicBeatSubState // Attempt to pause the game. if ((controls.PAUSE || androidPause || pauseButtonCheck) && isInCountdown && mayPauseGame && !justUnpaused) { + // Add banner ad when game is paused. + #if NO_DISABLE_ADMOB_ADS + AdMobUtil.addBanner(extension.admob.Admob.BANNER_SIZE_BANNER, extension.admob.Admob.BANNER_ALIGN_TOP); + #end + #if mobile pauseButton.alpha = 0; #end @@ -1173,6 +1181,12 @@ class PlayState extends MusicBeatSubState function moveToGameOver():Void { + // Shows a video ad on mobile devices each 3 blueballs + #if NO_DISABLE_ADMOB_ADS + Constants.GLOBAL_BLUEBALL_COUNTER++; + if (Constants.GLOBAL_BLUEBALL_COUNTER > 0 && Constants.GLOBAL_BLUEBALL_COUNTER % 3 == 0) AdMobUtil.loadInterstitial(true); + #end + // Reset and update a bunch of values in advance for the transition back from the game over substate. playerStrumline.clean(); opponentStrumline.clean(); @@ -1323,6 +1337,11 @@ class PlayState extends MusicBeatSubState */ public override function closeSubState():Void { + // Remove banner ad when game is unpaused. + #if NO_DISABLE_ADMOB_ADS + AdMobUtil.removeBanner(); + #end + if (Std.isOfType(subState, PauseSubState)) { var event:ScriptEvent = new ScriptEvent(RESUME, true); @@ -1605,6 +1624,11 @@ class PlayState extends MusicBeatSubState { performCleanup(); + // Remove banner ad when playstate is destroyed. + #if NO_DISABLE_ADMOB_ADS + AdMobUtil.removeBanner(); + #end + super.destroy(); } @@ -3417,6 +3441,20 @@ class PlayState extends MusicBeatSubState var talliesToUse:Tallies = PlayStatePlaylist.isStoryMode ? Highscore.talliesLevel : Highscore.tallies; + // Shows a video ad on mobile devices each 3 blueballs + #if NO_DISABLE_ADMOB_ADS + if (PlayStatePlaylist.isStoryMode) + { + AdMobUtil.loadInterstitial(true); + } + else + { + Constants.GLOBAL_FREEPLAY_VICTORY_COUNTER++; + if ((Constants.GLOBAL_FREEPLAY_VICTORY_COUNTER > 0 + && Constants.GLOBAL_FREEPLAY_VICTORY_COUNTER % 3 == 0)) AdMobUtil.loadInterstitial(true); + } + #end + var res:ResultState = new ResultState( { storyMode: PlayStatePlaylist.isStoryMode, diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index cb5ab10c5..d56215b8a 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -587,4 +587,18 @@ class Constants * 0.04 = 4% of distance per frame. */ public static final DEFAULT_CAMERA_FOLLOW_RATE:Float = 0.04; + + #if NO_DISABLE_ADMOB_ADS + /** + * A Counter that is increased by 1 each blueball. + * Only used for video ads each 3 blueballs on mobile. + */ + public static var GLOBAL_BLUEBALL_COUNTER:Int = 0; + + /** + * A Counter that is increased by 1 each victory only in freep;ay songs. + * Only used for video ads each 3 victories on mobile. + */ + public static var GLOBAL_FREEPLAY_VICTORY_COUNTER:Int = 0; + #end } diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index 911c93a36..ec3b8510c 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -51,9 +51,16 @@ class CrashHandler { errorSignal.dispatch(generateErrorMessage(error)); - #if sys - logError(error); - #end + try + { + #if sys + logError(error); + #end + } + catch (e:Dynamic) + { + trace('Error while logging error: ' + e); + } displayError(error); }