/usr/src/castle-game-engine-6.4/services/castlegameservice.pas is in castle-game-engine-src 6.4+dfsg1-2.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 | {
Copyright 2015-2017 Michalis Kamburelis.
This file is part of "Castle Game Engine".
"Castle Game Engine" is free software; see the file COPYING.txt,
included in this distribution, for details about the copyright.
"Castle Game Engine" is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
----------------------------------------------------------------------------
}
{ Integration with game service (Google Play Games or Apple Game Center) (TGameService). }
unit CastleGameService;
{$I castleconf.inc}
interface
uses Classes,
CastleStringUtils, CastleTimeUtils;
type
{ Event for @link(TGameService.OnPlayerBestScoreReceived). }
TPlayerBestScoreEvent = procedure (Sender: TObject; const LeaderboardId: string; const Score: Int64) of object;
{ User choice at "save game" dialog displayed by @link(TGameService.ShowSaveGames).
Used as a parameter for @link(TGameService.OnSaveGameChosen) event. }
TSaveGameChoice = (sgCancel, sgNew, sgExisting);
{ Event for @link(TGameService.OnSaveGameChosen). }
TSaveGameChosenEvent = procedure (Sender: TObject; const Choice: TSaveGameChoice; const SaveGameName: string) of object;
{ Event for @link(TGameService.OnSaveGameLoaded).
@param Success Whether we loaded the savegame successfully.
@param Content The savegame content, if Success. If not Success, this is the error message. }
TSaveGameLoadedEvent = procedure (Sender: TObject; const Success: boolean; const Content: string) of object;
{ Status of TGameService sign-in. }
TGameServiceStatus = (
// Not signed-in to the service.
gsSignedOut,
// Not signed-in to the service, but during signing-in.
gsSigningIn,
// Signed-in to the service. All game service methods work now.
gsSignedIn,
// During signing-out from the service. You should not call other methods on the service now.
gsSigningOut
);
{ Integration with a game service,
that can be used to show achievements, leaderboards, and store save games "in the cloud".
This integrates with
@unorderedList(
@itemSpacing Compact
@item Google Play Games on Android
@item Apple Game Center on iOS.
)
Usage:
@orderedList(
@item(Include the necessary integration code in your Android / iOS project.
For Android, declare your project type as "integrated" and add
the "google_play_games" service inside CastleEngineManifest.xml.
See https://github.com/castle-engine/castle-engine/wiki/Android-Project-Services-Integrated-with-Castle-Game-Engine .
For iOS, add the "apple_game_center" service inside CastleEngineManifest.xml.
See https://github.com/castle-engine/castle-engine/wiki/iOS-Project-Services-Integrated-with-Castle-Game-Engine .
Build your project with the Castle Game Engine build tool:
https://github.com/castle-engine/castle-engine/wiki/Build-Tool .)
@item(Create an instance of this class. Only a single instance of this class is allowed.)
@item(Call @link(TGameService.Initialize).
You usually do it from @link(TCastleApplication.OnInitialize).
The @link(TGameService.Initialize) must be called before calling any other method
of this class.)
@item(Use this to manage achievements, leaderboards and so on.)
@item(The user must be "signed in" to the game service.
The methods that display some user-interface will automatically attempt
to sign-in the user if needed. These include @link(ShowAchievements),
@link(ShowLeaderboard), @link(ShowSaveGames).
So you don't have to do anything before you call them.
All other methods will @italic(not sign-in the user automatically).
For example, @link(Achievement) or @link(SubmitScore) or
@link(SaveGameLoad) or @link(SaveGameSave).
You should always make sure that the user is signed-in before calling them.
To do this, pass AutoStartSignInFlow parameter as @true to @link(Initialize)
or call the @link(RequestSignedIn RequestSignedIn(true)).
And then wait for the @link(Status) property to change to gsSignedIn
(you can register @link(OnStatusChanged) to be notified about changes).
)
)
}
TGameService = class(TComponent)
private
FOnPlayerBestScoreReceived: TPlayerBestScoreEvent;
FInitialized,
FInitializedAutoStartSignInFlow,
FInitializedSaveGames: boolean;
FOnSaveGameChosen: TSaveGameChosenEvent;
FOnSaveGameLoaded: TSaveGameLoadedEvent;
FOnStatusChanged: TNotifyEvent;
FStatus: TGameServiceStatus;
function MessageReceived(const Received: TCastleStringList): boolean;
procedure ReinitializeJavaActivity(Sender: TObject);
protected
procedure DoSignedInChanged; virtual; deprecated 'use DoStatusChanged';
procedure DoStatusChanged; virtual;
procedure DoPlayerBestScoreReceived(const LeaderboardId: string; const Score: Int64); virtual;
procedure DoSaveGameChosen(const Choice: TSaveGameChoice; const SaveGameName: string); virtual;
procedure DoSaveGameLoaded(const Success: boolean; const Content: string); virtual;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
{ Connect to the service, optionally try to sign-in player.
If the player already connected this application with
Google Play Games (Android) or Apple Game Center (iOS),
(s)he will be signed-in automatically.
If not, player will be asked to sign-in (which means asking to accept permissions
on Android) only if AutoStartSignInFlow.
So if you want to avoid the initial Google Play Games dialog on Android,
just always pass AutoStartSignInFlow=false.
Calling this when already initialized is harmless (will not do anything).
In theory, you can call this at any point in your application.
But you probably want to call it early, e.g. from Application.OnInitialize,
otherwise user will not be signed-in automatically.
Most calls (like sending the leaderboars or achievements) will be ignored until
you call this.
@param(SaveGames Indicates whether you want to use save games feature.
You can then use @link(ShowSaveGames), @link(SaveGameSave), @link(SaveGameLoad)
methods. See also the description of this feature in Google Play Games:
https://developers.google.com/games/services/common/concepts/savedgames.) }
procedure Initialize(const AutoStartSignInFlow: boolean = true;
const SaveGames: boolean = false);
{ Was the @link(Initialize) called. }
property Initialized: boolean read FInitialized;
{ Current status of signing-in. }
property Status: TGameServiceStatus read FStatus;
{ Is user currently signed-in. Just a shortcut for @code(Status = gsSignedIn) check. }
function SignedIn: boolean;
{ Report the given achievement as achieved.
For Google Play Games (Android):
Use Google Developer Console to define achievements for your game,
and use their ids with this method. The achievement ids are autogenerated by Google,
they look like @code(CgkIvYuzpZsIEAIQAQ).
For Apple Game Center (iOS):
Use iTunes Connect to define achievements for your game,
and use their ids with this method. The achievement ids are chosen by you,
so you can choose something readable like 'boss_defeated'.
}
procedure Achievement(const AchievementId: string);
{ Report a score in given leaderboard.
Use Google Developer Console (Android) or iTunes Connect (iOS)
to create leaderboards for your game, use their ids here.
TODO: Not implemented for Apple Game Center (iOS) yet. }
procedure SubmitScore(const LeaderboardId: string; const Score: Int64);
{ Get the best score, if available, for given leaderboard.
This may (after some time, asynchronously) call
OnPlayerBestScoreReceived with this score.
Note that no error is signalled if loading the score fails for any reason.
This includes failing to load because user is not connected
to the game service (this method doesn't connect automatically;
wait for OnStatusChanged before calling this, if you need).
TODO: Not implemented for Apple Game Center (iOS) yet. }
procedure RequestPlayerBestScore(const LeaderboardId: string);
{ Request sign-in or sign-out.
The operation will be done asynchronously (it will in most cases require
some network communication).
Watch for changes to the @link(Status) property.
You can register an event on @link(OnStatusChanged) to be notified about this. }
procedure RequestSignedIn(const Value: boolean);
{ Show the user achievements, using the default UI.
Automatically connects (signs-in) player to game services,
if not connected yet. }
procedure ShowAchievements;
{ Show the given leaderboard, using the default UI.
The leaderboard should be created in the Games configuration
of the Google Developer Console (Android) or iTunes Connect (iOS).
Automatically connects (signs-in) player to game services,
if not connected yet. }
procedure ShowLeaderboard(const LeaderboardId: string);
{ Show the existing @italic(saved games) stored in the cloud
for this user. This can be used to offer user a choice in which slot
to save the game, or from which slot to load the game.
Note that it's not necessary to use this method to manage savegames.
E.g. if you want, you can just choose a constant savegame name for your game,
and use SaveGameSave and SaveGameLoad with this name. This would imply
that each game service user (Google Play Games user or Apple Game Center user)
has only one savegame in the cloud for your game.
The user may choose an existing savegame, or indicate creation
of a new savegame (if parameter AllowAddButton is @true). In response,
the callback OnSaveGameChosen will be called.
It will either
@orderedList(
@item(indicate the name of an existing savegame user has chosen,)
@item(or that user wants to create a new savegame,)
@item(or that user cancelled the dialog.)
)
Using this requires initializing the game service first,
by calling the @link(Initialize) with @code(SaveGames) parameter set to @true.
Just like @link(ShowAchievements) and @link(ShowLeaderboard),
this method automatically signs-in player to game service,
if not signed-in yet.
Note that @italic(the OnSaveGameChosen callback
is not called if the user cancels the sign-in operation), and therefore
cancels the savegame choice this way. The same remark applies to all
reasons why sign-in may fail (network problems etc.).
If this is a problem for your logic (e.g. if you want to "wait"
until OnSaveGameChosen is called), then never call ShowSaveGames
when @link(SignedIn) is @false. Instead, call RequestSignedIn, wait until
SignedIn changed to @true (which may be "never", in case of network problems
or user cancelling!), and only then call ShowSaveGames.
TODO: Not implemented for Apple Game Center (iOS) yet.
@param(Title Dialog title to display.)
@param(AllowAddButton Enable user to choose "new save game".)
@param(AllowDelete Enable user to delete savegames from the dialog.)
@param(MaxNumberOfSaveGamesToShow Maximum number of savegames to show.
Use -1 to just show all savegames.)
}
procedure ShowSaveGames(const Title: string; const AllowAddButton, AllowDelete: boolean;
const MaxNumberOfSaveGamesToShow: Integer);
{ Save a savegame identified by the given name.
See the SaveGameLoad documentation about the conflict resolution
and valid savegame names.
The Contents should be a valid UTF-8 string. For implementation
reasons, you should not save arbitrary binary data this way, for now
(or it could result in exceptions about being unable to encode/decode UTF-8
sequences).
Description and PlayedTime are shown to player in ShowSaveGames.
PlayedTime may also be used for conflict resolution (if the savegame
on the server was modified in the meantime, without loading it in this game).
No callback is called in response, the game is saved in the background.
This does not connect player to game service, if it's not connected
already. An error when saving is not reported back to Pascal, for now.
(The assumption here is that you will keep a local savegame anyway,
in case user does not connect to game service. So inability to save
the savegame to the cloud is not alarming, and does not require any special
reaction. Please submit a request if you'd like to have a callback
about it.) }
procedure SaveGameSave(const SaveGameName, Contents, Description: string;
const PlayedTime: TFloatTime);
{ Load a savegame identified by the given name.
If the savegame does not exist, it will be automatically created.
If the server requires conflict resolution,
the savegame with longest playtime is used (internally: we use
RESOLUTION_POLICY_LONGEST_PLAYTIME flag with Google Play Games).
For this to work, you must provide a proper PlayedTime parameter
when saving all your savegames through @link(SaveGameSave).
Valid savegame names are defined by Google Play Games:
Must be between 1 and 100 non-URL-reserved characters (a-z, A-Z, 0-9,
or the symbols "-", ".", "_", or "~").
In response, the callback OnSaveGameLoaded will be @italic(always) called,
with the loaded savegame contents (as a string),
or the error message.
This does not connect player to game service (like Google Play Games),
if it's not connected already.
An error will be reported to OnSaveGameLoaded callback
if trying to load fails for any reason.
This includes the case when loading fails because user is not connected
to game service yet (this method @italic(does not) connect user
automatically; wait for OnStatusChanged before calling this method,
if you need it). }
procedure SaveGameLoad(const SaveGameName: string);
published
{ Event called when @link(Status) changed, for example because
@link(RequestSignedIn) was called, or because user signs-in automatically
(which may happen if you used AutoStartSignInFlow with @link(Initialize),
or if user was signed-in in this application previously). }
property OnStatusChanged: TNotifyEvent read FOnStatusChanged write FOnStatusChanged;
property OnSignedInChanged: TNotifyEvent read FOnStatusChanged write FOnStatusChanged stored false;
deprecated 'use OnStatusChanged';
{ Event received in response to @link(RequestPlayerBestScore). }
property OnPlayerBestScoreReceived: TPlayerBestScoreEvent read FOnPlayerBestScoreReceived write FOnPlayerBestScoreReceived;
{ Event received in response to @link(ShowSaveGames). }
property OnSaveGameChosen: TSaveGameChosenEvent read FOnSaveGameChosen write FOnSaveGameChosen;
{ Event received in response to @link(SaveGameLoad).
See TSaveGameLoadedEvent for documentation of parameters. }
property OnSaveGameLoaded: TSaveGameLoadedEvent read FOnSaveGameLoaded write FOnSaveGameLoaded;
end;
implementation
{$warnings off}
{ use deprecated units below, only to have them compiled together with Lazarus
castle_base.lpk package }
uses SysUtils,
CastleUtils, CastleMessaging, CastleApplicationProperties, CastleLog,
// this is deprecated
CastleGooglePlayGames, CastleShaders, CastleGenericLists, CastleWarnings;
{$warnings on}
constructor TGameService.Create(AOwner: TComponent);
begin
inherited;
Messaging.OnReceive.Add(@MessageReceived);
ApplicationProperties.OnInitializeJavaActivity.Add(@ReinitializeJavaActivity);
end;
destructor TGameService.Destroy;
begin
if Messaging <> nil then
Messaging.OnReceive.Remove(@MessageReceived);
if ApplicationProperties(false) <> nil then
ApplicationProperties(false).OnInitializeJavaActivity.Remove(@ReinitializeJavaActivity);
inherited;
end;
procedure TGameService.ReinitializeJavaActivity(Sender: TObject);
begin
{ in case Java activity got killed and is created again, reinitialize services }
if FInitialized then
begin
FStatus := gsSignedOut;
Initialize(FInitializedAutoStartSignInFlow, FInitializedSaveGames);
end;
end;
procedure TGameService.DoStatusChanged;
begin
if Assigned(OnStatusChanged) then
OnStatusChanged(Self);
{$warnings off} // calling deprecated to keep it working
DoSignedInChanged;
{$warnings on}
end;
procedure TGameService.DoSignedInChanged;
begin
end;
function TGameService.SignedIn: boolean;
begin
Result := FStatus = gsSignedIn;
end;
procedure TGameService.DoPlayerBestScoreReceived(const LeaderboardId: string; const Score: Int64);
begin
if Assigned(OnPlayerBestScoreReceived) then
OnPlayerBestScoreReceived(Self, LeaderboardId, Score);
end;
procedure TGameService.DoSaveGameChosen(const Choice: TSaveGameChoice; const SaveGameName: string);
begin
if Assigned(OnSaveGameChosen) then
OnSaveGameChosen(Self, Choice, SaveGameName);
end;
procedure TGameService.DoSaveGameLoaded(const Success: boolean; const Content: string);
begin
if Assigned(OnSaveGameLoaded) then
OnSaveGameLoaded(Self, Success, Content);
end;
function TGameService.MessageReceived(const Received: TCastleStringList): boolean;
var
StatusInt: Int64;
begin
Result := false;
if (Received.Count = 3) and
(Received[0] = 'best-score') then
begin
DoPlayerBestScoreReceived(Received[1], StrToInt64(Received[2]));
Result := true;
end else
if (Received.Count = 2) and
(Received[0] = 'game-service-status') then
begin
StatusInt := StrToIntDef(Received[1], -1);
if (StatusInt >= Ord(Low(TGameServiceStatus))) and
(StatusInt <= Ord(High(TGameServiceStatus))) then
begin
FStatus := TGameServiceStatus(StatusInt);
DoStatusChanged;
end else
WritelnWarning('Invalid game-service-status parameter "%s"', [Received[1]]);
Result := true;
end else
if (Received.Count = 2) and
(Received[0] = 'chosen-save-game') then
begin
DoSaveGameChosen(sgExisting, Received[1]);
Result := true;
end;
if (Received.Count = 1) and
(Received[0] = 'chosen-save-game-new') then
begin
DoSaveGameChosen(sgNew, '');
Result := true;
end;
if (Received.Count = 1) and
(Received[0] = 'chosen-save-game-cancel') then
begin
DoSaveGameChosen(sgCancel, '');
Result := true;
end;
if (Received.Count = 3) and
(Received[0] = 'save-game-loaded') then
begin
DoSaveGameLoaded(StrToBool(Received[1]), Received[2]);
Result := true;
end;
end;
procedure TGameService.Initialize(const AutoStartSignInFlow: boolean;
const SaveGames: boolean);
begin
{ at first Initialize call, remember AutoStartSignInFlow }
if not FInitialized then
begin
FInitializedAutoStartSignInFlow := AutoStartSignInFlow;
FInitializedSaveGames := SaveGames;
end;
FInitialized := true;
Messaging.Send(['game-service-initialize',
TMessaging.BoolToStr(AutoStartSignInFlow),
TMessaging.BoolToStr(SaveGames)
]);
end;
procedure TGameService.Achievement(const AchievementId: string);
begin
Messaging.Send(['achievement', AchievementId]);
end;
procedure TGameService.SubmitScore(const LeaderboardId: string; const Score: Int64);
begin
Messaging.Send(['submit-score', LeaderboardId, IntToStr(Score)]);
end;
procedure TGameService.RequestPlayerBestScore(const LeaderboardId: string);
begin
Messaging.Send(['request-player-best-score', LeaderboardId]);
end;
procedure TGameService.RequestSignedIn(const Value: boolean);
begin
Messaging.Send(['game-service-sign-in', TMessaging.BoolToStr(Value)]);
end;
procedure TGameService.ShowAchievements;
begin
Messaging.Send(['show', 'achievements']);
end;
procedure TGameService.ShowLeaderboard(const LeaderboardId: string);
begin
Messaging.Send(['show', 'leaderboard', LeaderboardId]);
end;
procedure TGameService.ShowSaveGames(const Title: string; const AllowAddButton, AllowDelete: boolean;
const MaxNumberOfSaveGamesToShow: Integer);
begin
Messaging.Send(['show', 'save-games', Title,
TMessaging.BoolToStr(AllowAddButton),
TMessaging.BoolToStr(AllowDelete),
IntToStr(MaxNumberOfSaveGamesToShow)
]);
end;
procedure TGameService.SaveGameLoad(const SaveGameName: string);
begin
Messaging.Send(['save-game-load', SaveGameName]);
end;
procedure TGameService.SaveGameSave(const SaveGameName, Contents, Description: string;
const PlayedTime: TFloatTime);
begin
Messaging.Send(['save-game-save', SaveGameName, Contents, Description, TMessaging.TimeToStr(PlayedTime)]);
end;
end.
|