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); }