RichLÖVE Mobile 0.10.2 - AdMob+UnityAds+PlayGamesServices+GameCenter

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
Marty
Citizen
Posts: 89
Joined: Mon Dec 04, 2017 1:47 am
Location: Germany

RichLÖVE Mobile 0.10.2 - AdMob+UnityAds+PlayGamesServices+GameCenter

Post by Marty »

Hi,

I decided to create forks of bio1712's AdMob supported forks of LÖVE and started adding more native functionality that is useful for mobile platforms. I'd like to share it with you and I'm welcoming anybody who wants to test all out of it.

The reason why I started this project is I wanted to add UnityAds support to the AdMob fork, because UnityAds can be more efficient for game developers than AdMob when it comes to video ads. I also included a common API to communicate with Google Play Games Services and Game Center for synchronised saved games, leaderboard ranking and achievements. I'm looking forward to add more modules with other native libraries such as In-App-Billing/-Purchases.

Feel free to use this to enhance and monetise your game in a reasonable matter. Do not start excessive ad bombing! This exists to give indies a way to earn a little with free games, not to destroy the positive vibe of LÖVE. Thank you!

Repositories: Android | iOS

Modules:
  • [love.ads] Google AdMob (created by bio1712) - Displays banner, interstitial and reward video ads using the native Ad-API of Google, apps.admob.com
  • [love.uads] UnityAds - Displays video and reward video ads using the native Ad-API of Unity3D, operate.dashboard.unity3d.com
  • [love.mobsvc] Mobile Services (Google Play Games Services / Apple Game Center) - Provides a common API for Saved Games, Leaderboards and Achievements on Android (using Play Games Services) and on iOS (using Game Center), developers.google.com/games/services / developer.apple.com/game-center

API:

Google AdMob

______Please visit the forum thread of bio1712 for an API reference of the love.ads module: Android iOS

UnityAds

______Functions
  • love.uads.isReady(string placementId)
    string placementId : The placement ID of your ad.
    Checks if the video ad with the given placement ID is loaded. The loading of all video ads is triggered by the start of your game, automatically.
    Returns:
    bool isReady: true if the video ad is ready to be shown.
  • love.uads.show(string placementId)
    string placementId : The placement ID of the video ad.
    Plays the video ad with the given placement ID.
______Callbacks
  • love.unityAdsReady(string placementId)
    string placementId : The placement ID of the video ad.
    Fired when a video ad is loaded, successfully.
  • love.unityAdsDidStart(string placementId)
    string placementId : The placement ID of the video ad.
    Fired when a video ad has been started.
  • love.unityAdsDidFinish(string placementId, string finishState)
    string placementId : The placement ID of the video ad.
    string finishState : The finish state when the video ad has been closed by the user. Possible values: Error, Skipped, Completed
    Fired when a video ad has been stopped.
  • love.unityAdsDidError(string error, string message)
    string error : The type of the error. Possible values: NotInitialized, InitializedFailed, InvalidArgument, VideoPlayerError, InitSanityCheckFail, AdBlockerDetected, FileIoError, DeviceIdError, ShowError, InternalError
    string message : The message of the error.
    Fired when a video ad cannot be load.
Mobile Services (Google Play Games Services / Apple Game Center)

______Important information
  • General
  • These mobile services are limited to their own environments. This API does not provide a way to sync data between Android and iOS devices. Since people often live in only one environment, this should not be a dealbreaker, although I'm looking forward to bring up a Firebase solution to make sharing progress between all mobile devices possible.
  • On Android this API uses Google Play Games Services in the background. Technically this API could provide a way more features such as the logout from the Play Games Services within the game, however due to Apples limitations on all that, a lot of things has been dropped, however simple stuff has been preserved as optional Android-only feature, like storing descriptions in the Saved Games feature.
  • On iOS this API uses GameKit in the background. It comes with more limitations and some odds, so that I was not able to end up with an API that acts in an entirely same fashion on both platforms.
  • Many functions work async in this API, however except for the signIn and isSignedIn function each function will not open a new thread if the user is not logged in for any reason. Those will just return, immediately.
  • Quests (Android) / Challanges (iOS) are not provided through this API, sorry.
  • Please be aware that each function that opens a new thread can cause memory leaks over time, because threads can halt for quite a while if no or only weak internet connection is available. Also a function should never be called twice so that it runs simultaneously. For a guide line how to call a specific function more than once in row, please take a look at my best practice section in my second post.
  • Warning: Because this API does not make use of polling it will require to use love.event and love.thread, so please require those. Also to pass data back to the main thread, it uses the love.handlers table. please do not use any handlers with the name pattern love.handlers.mobSvc*.
    Signing In/Out
  • On Android the API will check if Google Play Games Services is installed on the device. If not, the user will be asked to install the app. If he/she does not by canceling the request, any function will not perform any operation. If the Google Play Games Services app is installed a silent sign-in will be tried first. If this fails a proper intent will be displayed that forces the user to sign in. The user can cancel the operation. If the user signs in, he/she will be asked to give permissions to the Play Games Services app as well as to Google Drive if the MOBSVC_ANDROID_PLAY_GAMES_PERMISSION_GDRIVE flag is set to enable the Saved Games feature. This is enabled by default. Although a logout function would be possible on Android, on iOS it would not, so I didn't implement it. The user can logout his account from your app in the Google Settings in any case.
  • On iOS the API will try to login into Game Center, silently. If this fails a proper ViewController will be initialized, that allows the user to login. If the user cancels this operation there is no way to trigger the signIn again, so you should try this only once at the begin of your game in any case. GameKit does not offer a way to logout. Warning: If the player never logged into Game Center on the device, GameKit refuses to return a proper ViewController that can be initialized.
    Saved Games
  • On Android you can use this feature to store saved games of the user in Google Drive across all his/her Android devices. To allow sharing across devices the user needs to be logged into the same Google account, obviously. This feature is very helpful, because Android does not store app data in Google Drive, automatically. It's also great, because it allows the player to continue his/her journey after re-installing your game. You can also attach a description on each saved game, like the in-game location of the player at the time of saving.
  • On iOS you can use this feature to prevent the loss of the progress if the user uninstalls your game. However, the user needs to login into the same iCloud to sync the saved games across devices. Be aware, LÖVE also stores files you save locally into the Application Support directory of the app, so it's synced across devices that are logged into the same iCloud account if the user has syncing of this app's data enabled. This is the reason why you should store the timestamp within the saved game data itself, too, so you can always make sure not to overwrite a newer saved game that comes through iCloud on the current device. To know more about this, please read my best practice section in the second post. Also, storing descriptions among saved games is not possible, because GameKit does not support it.
  • This API requires you to upload and download a string representation of the saved game data. There is a simple way to serialize and deserialize a Lua table that I use by myself, too.
  • Warning: Saved Games can take a while before they are synced between devices, so never overwrite them on game start. You should always give the player a way to see his progress before he/she continues and triggers any save points, so the player can decide to end the app and wait for the save game to sync, before he/she proceeds.
    Leaderboards
  • It's obvious that you have to use similar settings for each leaderboard that you setup in both environments. You must not use any setting that is not possible or allowed on the other environment.
  • Depending on the wished score format you have to provide the correct format parameter when uploading a new score, please check the proper enum and function for that down below. The value you pass always represents the smallest unit of the format you passed to the function, not the setting you made in the backend of Google / Apple.
  • On Android you must not select a currency that has 0 or 2 decimal digits, because this is the only format Apple supports on currency values on their environment.
  • On iOS the Score Submission Type cannot be "Most Recent Score", because Google does not support this on their environment. Combined leaderboards must not be used, either.
    Achievements
  • Due to the different nature of both environments when it comes to achievements, this API requires you to think in incremental steps only. It's only required for you to keep track of the progress of each achievement if you wish to display the progress in your game.
  • On Android you decide if your achievement is incremental or not and when it's incremental you can define how many steps it has. This API simplifies this by just saying your achievement has only one step if it's not incremental, since an incremental achievement starts at 2 steps.
  • On iOS there are no steps on achievements, because Apple wants to have a simple update (absolute percentage value) of the progress of the achievement. This API will optain the current percentage value and increase it depending on the steps to progress and the maximum steps of your achievement. This means you cannot decrease a progress of an achievement as well as you always have to provide the number of steps to increase and the maximum number of possible steps your achievement has. Since achievements on Android cannot be unlocked more than once, do not allow it to be achievable multiple times on iOS either. Warning: The Apple servers take a few seconds to update the percentComplete value on an achievement after incrementing the progress on an achievement. So please avoid incrementing the same achievement several times in row without a minimum delay of 10 seconds in between, otherwise it will fail to update the achievement properly.
______Objects
  • SavedGameMetadata
    string name : Unique name of the saved game. This is used to identify the saved game.
    string description : Contains the description of the saved game (Android only).
    number modifiedTime : Linux epoch timestamp of the saved game. Use os.time() that returns the current timestamp to compare it to the current time.
    Provides meta data of a saved game, that is stored in the cloud of Google or Apple.
______Enums
  • LeaderboardFormat ("none")
    - none : The format of the leaderboard is numeric (Android) / integer or fixed point number (iOS). The score value is represented in the smallest possible unit, i.e. to pass the value 12.34 (2 placeholders) it would require to use 1234 as value.
    - minutes : The format of the leaderboard is time (Android) / elapsed time to the minute (iOS) and the value represents minutes.
    - seconds : The format of the leaderboard is time (Android) / elapsed time to the second (iOS) and the value represents seconds.
    - milliseconds : The format of the leaderboard is time (Android) / elapsed time to the hundreth of a second (iOS) and the value represents 1/1000th of a second. The API will divide this value by 10 on the iOS port to match the format of iOS.
    - currencyunits : The format of the leaderboard is currency (Android) / money whole numbers (iOS).
    - currencycents : The format of the leaderboard is currency (Android) / money to 2 decimals (iOS). On Android only 2 decimal digits are supported.
    Represents the format of the values in the leaderboard. It also represents the format of any given value to this API.
______Functions
  • love.mobsvc.signIn([function callback<string playerId>])
    function callback (optional): Function to be called on successful login with the player ID as argument.
    Opens a new LÖVE thread and tries to login the player into the mobile services. If the user cannot be logged in, silently, it will open an Intent / ViewController to ask for the credentials to login. On Android it will ask for the required permissions. On iOS no ViewController will open when the user never logged into Game Center before (Apple rejects giving any ViewController in this circumstance). If the user exits the Intent / ViewController without logging in, this second thread will halt forever. If the user logged in, it will call the passed callback function with the received playerId that helps to identify the user in the main thread. After that it calls the static love.mobileServiceSignedIn callback in the main thread.
  • love.mobsvc.isSignedIn()
    Will check if the current player is logged in.
    Returns:
    bool isSignedIn: true if the current player is signed in.
  • love.mobsvc.downloadSavedGamesMetadata([ function callback<array savedGamesMetadata{ SavedGameMetadata item1, SavedGameMetadata item2, ... }> ])
    function callback (optional): Function to be called on successful download of the saved games metadata with the received saved games metadata in an indexed array table as argument.
    Opens a new LÖVE thread and downloads metadata of all previously uploaded saved games and contructs a indexed array table containing all the data tables objects (SavedGameMetadata) and passes it to the given callback in the main thread. If there are name-conflicting saved games, it will load only the last saved saved game metadata with the most recent modifiedTime for each name. The saved game on the first index is the most recent one with the highest modifiedTime. After passing the result in the provided callback function it will call the static love.mobileServiceSavedGameMetadataDownloaded with the same indexed array as argument in the main thread.
  • love.mobsvc.downloadSavedGameMetadata(string name [, function callback<SavedGameMetadata savedGameMetadata> ])
    string name : Name of the saved game to download the metadata from.
    function callback (optional): Function to be called on successful download of the saved game metadata with the received saved game metadata as argument.
    Opens a new LÖVE thread and downloads one metadata with the given name of the previously uploaded saved games and contructs a data table object (SavedGameMetadata) and passes it to the given callback in the main thread. If there are more saved games of the same name, it will load only the last saved saved game metadata with the most recent modifiedTime of the given name. After passing the result in the provided callback function it will call the static love.mobileServiceSavedGameMetadataDownloaded with the same SavedGameMetadata as argument in the main thread.
  • love.mobsvc.downloadSavedGameData(string name [, function callback<string data> ])
    string name : Name of the saved game to download the serialized data from.
    function callback (optional): Function to be called on successful download of the saved game data with the received serialized saved game data as argument.
    Opens a new LÖVE thread and downloads saved game data with the given name of the previously uploaded saved games and passes it to the given callback in the main thread. If there are more saved games of the same name, it will load only the last saved saved game metadata with the most recent modifiedTime of the given name. After passing the result in the provided callback function it will call the static love.mobileServiceSavedGameMetadataDownloaded with the name and same indexed array as argument in the main thread.
  • love.mobsvc.uploadSavedGameData(string name, string data, string description [, function callback<bool success> ])
    string name : Name of the saved game to upload.
    string data : Serialized data of the saved game to upload.
    string description : Description of the saved game, can be an empty string. (Android only)
    function callback (optional): Function to be called after upload of the saved game with a success flag that is true if the operation was successful.
    Opens a new LÖVE thread and uploads the serialized saved game data with the given name to the cloud. If there is already a saved game with the same name it will be overwritten. After passing the result in the provided callback function it will call the static love.mobileServiceSavedGameDataUploaded with the same name and success flag as arguments in the main thread.
  • love.mobsvc.deleteSavedGame(string name [, function callback<bool success> ])
    string name : Name of the saved game to delete.
    function callback (optional): Function to be called after delete of the saved game with a success flag that is true if the operation was successful.
    Opens a new LÖVE thread and deletes the saved game with the given name in the cloud. After passing the result in the provided callback function it will call the static love.mobileServiceSavedGameDeleted with the same name and success flag as arguments in the main thread.
  • love.mobsvc.showLeaderboard(string androidLeaderboardId, string iosLeaderboardId)
    string androidLeaderboardId : Unique ID of the android leaderboard.
    string iosLeaderboardId : Unique ID of the iOS leaderboard.
    Opens the given leaderboard in main thread. Will unfocus the game and pause the main game loop.
  • love.mobsvc.uploadLeaderboardScore(string androidLeaderboardId, string iosLeaderboardId, number score, LeaderboardFormat format [, function callback<bool success> ])
    string androidLeaderboardId : Unique ID of the android leaderboard.
    string iosLeaderboardId : Unique ID of the iOS leaderboard.
    string score : Score value to upload in the correct format. Please check the LeaderboardFormat enum for more information. The score will not be counted if it's lower as the current score.
    LeaderboardFormat format : Format of the leaderboard and the passed value.
    function callback (optional): Function to be called after upload of the score to the leaderboard with a success flag that is true if the operation was successful.
    Opens a new LÖVE thread and uploads the given score to the leaderboard. After passing the result in the provided callback function it will call the static love.mobileServiceLeaderboardScoreUploaded with the leaderboard IDs and success flag as arguments in the main thread.
  • love.mobsvc.showAchievements()
    Opens the achievements in main thread. Will unfocus the game and pause the main game loop.
  • love.mobsvc.incrementAchievementProgress(string androidAchievementId, string iosAchievementId, number steps, number maxSteps [, function callback<bool success, bool unlocked> ])
    string androidAchievementId : Unique ID of the android achievement.
    string iosAchievementId : Unique ID of the iOS achievement.
    string steps : Number of steps to increment the progress.
    string maxSteps : Number of steps the achievement has in total. This is important, because the API needs to convert this to an absolute percent value on iOS.
    function callback (optional): Function to be called after incrementing the achievement progress with a success and unlocked flag. The success flag determines if the incrementing was successful. The unlocked flag determines if the achievement is unlocked now, so you can show a badge within the game.
    Opens a new LÖVE thread and increments the achievement with the given achivement IDs. After passing the result in the provided callback function it will call the static love.mobileServiceAchievementProgressIncremented with the achievement IDs and success and unlocked flag as arguments in the main thread.
______Callbacks
  • love.mobileServiceSignedIn(string playerId)
    string playerId : The unique player ID of the user who signed into the mobile services.
    Fired when the player has logged into the mobile services using love.mobsvc.signIn, successfully.
  • love.mobileServiceSavedGamesMetadataDownloaded(array savedGamesMetadata{ SavedGameMetadata item1, SavedGameMetadata item2, ... })
    array savedGamesMetadata : Index based table that contains all SavedGameMetadata, the most recent comes first.
    Fired when all saved games metadata has been downloaded using love.mobsvc.downloadSavedGamesMetadata, successfully.
  • love.mobileServiceSavedGameMetadataDownloaded(SavedGameMetadata savedGameMetadata)
    SavedGameMetadata savedGameMetadata : Saved game metadata, that contains name, modifiedTime and description of the saved game.
    Fired when a saved game's metadata has been downloaded using love.mobsvc.downloadSavedGameMetadata, successfully.
  • love.mobileServiceSavedGameDataDownloaded(string name, string data)
    string name : The name of the saved game.
    string data : The serialized data of the saved game.
    Fired when a saved game has been downloaded using love.mobsvc.downloadSavedGameData, successfully.
  • love.mobileServiceSavedGameDataUploaded(string name, bool success)
    string name : The name of the saved game.
    bool success : Determines if the operation was successful.
    Fired when a saved game has been uploaded using love.mobsvc.uploadSavedGameData.
  • love.mobileServiceSavedGameDeleted(string name, bool success)
    string name : The name of the saved game.
    bool success : Determines if the operation was successful.
    Fired when a saved game has been deleted using love.mobsvc.deleteSavedGame.
  • love.mobileServiceLeaderboardScoreUploaded(string androidLeaderboardId, string iosLeaderboardId, bool success)
    string androidLeaderboardId : Unique ID of the android leaderboard.
    string iosLeaderboardId : Unique ID of the iOS leaderboard.
    bool success : Determines if the operation was successful.
    Fired when a score has been updated using love.mobsvc.uploadLeaderboardScore.
  • love.mobileServiceAchievementProgressIncremented(string androidAchievementId, string iosAchievementId, bool success, bool unlocked)
    string androidAchievementId : Unique ID of the android achievement.
    string iosAchievementId : Unique ID of the iOS achievement.
    bool success : Determines if the operation was successful.
    bool unlocked : Determines if the achievement is unlocked, now.
    Fired when an achievement progress has been updated using love.mobsvc.incrementAchievementProgress.
The repositories contain a sample game.love that allow you to test the modules and learn how to use the functions above.

More:

Override love.run

If you override love.run in your game for any reason (like getting a fixed tick rate), you have to make sure to do some additional things that are part of the modified boot.lua in the ports, because these would not be used, anymore.

Simply require this Lua file and use it like so:

Code: Select all

richbootcp = require "richbootcp"

function love.load()
  richbootcp.boot()
end

function love.update(dt)
  richbootcp.update(dt)
end
After this all RichLÖVE modules work again, even with a custom love.run implementation.

Additional wrapper for one-liner

I also made a wrapper for AdMob and UnityAds that allows to trigger ads with less code. As callback for rewards it uses the beholder lib. This is purely optional.
Last edited by Marty on Fri Jan 18, 2019 7:27 pm, edited 14 times in total.
Visual Studio Code TemplateRichLÖVE Mobile (AdMob+UnityAds+PlayGamesServices+GameCenter)Add me on Discord

───▄▀▀▀▄▄▄▄▄▄▄▀▀▀▄───
───█▒▒░░░░░░░░░▒▒█───
────█░░░░░░░░░█────
▄▄──█░░░▀█▀░░░█──▄▄
█░░█▀▄░░░░░░░▄▀█░░█
User avatar
Marty
Citizen
Posts: 89
Joined: Mon Dec 04, 2017 1:47 am
Location: Germany

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds

Post by Marty »

Best practices

I'm sorry, so far I had not time to populate this section for you. In the meantime you shall work out it yourself with the given documentation above, it's not too hard. Good luck!
Last edited by Marty on Sun Nov 11, 2018 6:05 pm, edited 1 time in total.
Visual Studio Code TemplateRichLÖVE Mobile (AdMob+UnityAds+PlayGamesServices+GameCenter)Add me on Discord

───▄▀▀▀▄▄▄▄▄▄▄▀▀▀▄───
───█▒▒░░░░░░░░░▒▒█───
────█░░░░░░░░░█────
▄▄──█░░░▀█▀░░░█──▄▄
█░░█▀▄░░░░░░░▄▀█░░█
Nelvin
Party member
Posts: 124
Joined: Mon Sep 12, 2016 7:52 am
Location: Germany

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds

Post by Nelvin »

Thanks for sharing your progress ... have you started working on some additional modules (IAP) already?
cosme12
Prole
Posts: 8
Joined: Wed Oct 19, 2016 7:02 pm

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds

Post by cosme12 »

Finally something that works in the second try only! Thanks for supporting the indie community modiX!! :awesome:
gianmichele
Citizen
Posts: 67
Joined: Tue Jan 14, 2014 11:03 pm

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds

Post by gianmichele »

This is great! Any chance of updating it to love 11.1 ?
User avatar
Marty
Citizen
Posts: 89
Joined: Mon Dec 04, 2017 1:47 am
Location: Germany

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds

Post by Marty »

Hi there, thanks for your interests.

Nelvin wrote: Thu Sep 27, 2018 4:56 pm Thanks for sharing your progress ... have you started working on some additional modules (IAP) already?
I've started implementing a common API for Game-Center / Google Play Games Services for sharing save games across devices, having achievements and score leaderboards. IAP is right after this on my list.

cosme12 wrote: Sun Sep 30, 2018 2:08 pm Finally something that works in the second try only! Thanks for supporting the indie community modiX!! :awesome:
I'm glad it works almost immediately for you. I'm looking forward to make it easier for mobile developers to use LÖVE in a more serious matter to increase popularity for this awesome framework. Thanks for testing it! :ultrahappy:

gianmichele wrote: Sun Sep 30, 2018 11:14 pm This is great! Any chance of updating it to love 11.1 ?
I'm planning to update this to 11.1 indeed, once I have all modules in it I need for my game that targets 0.10.2 (I won't update it). Perhaps I will loose the dependency to the AdMob fork on my 11.1 edition and implement a common free API for ~50 ad-providers. This all will take a while though. Please stay patient.
Visual Studio Code TemplateRichLÖVE Mobile (AdMob+UnityAds+PlayGamesServices+GameCenter)Add me on Discord

───▄▀▀▀▄▄▄▄▄▄▄▀▀▀▄───
───█▒▒░░░░░░░░░▒▒█───
────█░░░░░░░░░█────
▄▄──█░░░▀█▀░░░█──▄▄
█░░█▀▄░░░░░░░▄▀█░░█
User avatar
Marty
Citizen
Posts: 89
Joined: Mon Dec 04, 2017 1:47 am
Location: Germany

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds+PlayGamesServices+GameCenter

Post by Marty »

I added support for Google Play Games Services (Android) and Game Center (iOS) to this port. It features synchronised saved games (GDrive, iCloud), public leaderboard ranking and achievements for your mobile game.
Visual Studio Code TemplateRichLÖVE Mobile (AdMob+UnityAds+PlayGamesServices+GameCenter)Add me on Discord

───▄▀▀▀▄▄▄▄▄▄▄▀▀▀▄───
───█▒▒░░░░░░░░░▒▒█───
────█░░░░░░░░░█────
▄▄──█░░░▀█▀░░░█──▄▄
█░░█▀▄░░░░░░░▄▀█░░█
Nelvin
Party member
Posts: 124
Joined: Mon Sep 12, 2016 7:52 am
Location: Germany

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds+PlayGamesServices+GameCenter

Post by Nelvin »

This is getting better and more interesting with each update ... really looking forward to give these versions a try.
User avatar
SiENcE
Party member
Posts: 806
Joined: Thu Jul 24, 2008 2:25 pm
Location: Berlin/Germany
Contact:

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds+PlayGamesServices+GameCenter

Post by SiENcE »

Just wow!

This is a great thing for löve going mobile :) ! Thanks Marty!

I've to dig deeper when my game is out.
Cruhan
Prole
Posts: 4
Joined: Sat Mar 18, 2017 3:45 pm

Re: RichLÖVE Mobile 0.10.2 - AdMob+UnityAds+PlayGamesServices+GameCenter

Post by Cruhan »

if I understand correctly this is 10.2 so it doesn't use metal on ios?
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 1 guest